class Canon::PrettyPrinter::Html

See lutaml/canon#133, lutaml/canon#135.
+<?xml …?>+ prefix.
save flag; the NO_DECLARATION flag suppresses the
elements self-closed, non-void paired) via the AS_XHTML
into a fixture heredoc. Output is XHTML-shaped (void
so the user can read or paste the formatted output directly
(the +CANON_<FORMAT>_DIFF_SHOW_PRETTYPRINT_RECEIVED+ surface)
save flag. Used by +DiffFormatter#prettyprint_for_display+
actually-indented XHTML-shaped output via libxml’s FORMAT
2. Fixture-ready mode (+fixture_ready: true+): emits
equivalence to the input.
do not require actual indentation; they require structural
diff-pipeline apply_pretty_print stage, etc). These callers
structural normaliser (the canon round-trip tests, the
behaviour for callers that use the pretty-printer as a
1. Default mode (+fixture_ready: false+): retains the existing
Two modes:
Pretty printer for HTML with consistent indentation.

def expand_non_void_self_closing(html)

void tags like +
+ and ++ pass through unchanged.
that is not an HTML5 void element. ++ is illegal HTML;
Rewrite ++ into ++ for every element name
def expand_non_void_self_closing(html)
  html.gsub(%r{<([A-Za-z][A-Za-z0-9:_-]*)((?:\s+[^<>"]*(?:"[^"]*"[^<>"]*)*)?)/>}) do
    name = ::Regexp.last_match(1)
    attrs = ::Regexp.last_match(2)
    if HtmlVoidElements.void?(name)
      "<#{name}#{attrs}/>"
    else
      "<#{name}#{attrs}></#{name}>"
    end
  end
end

def fixture_ready_save_options

def fixture_ready_save_options
  Nokogiri::XML::Node::SaveOptions::FORMAT |
    Nokogiri::XML::Node::SaveOptions::AS_XHTML |
    Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
end

def format(html_string)

def format(html_string)
  return format_fixture_ready(html_string) if @fixture_ready
  if xhtml?(html_string)
    format_as_xhtml(html_string)
  else
    format_as_html(html_string)
  end
end

def format_as_html(html_string)

def format_as_html(html_string)
  doc = Nokogiri::HTML5(html_string)
  if @indent_type == "tab"
    doc.to_html(indent: 1, indent_text: "\t", encoding: "UTF-8")
  else
    doc.to_html(indent: @indent, encoding: "UTF-8")
  end
end

def format_as_xhtml(html_string)

def format_as_xhtml(html_string)
  doc = Nokogiri::XML(html_string, &:noblanks)
  out = if @indent_type == "tab"
          doc.to_xml(indent: 1, indent_text: "\t", encoding: "UTF-8")
        else
          doc.to_xml(indent: @indent, encoding: "UTF-8")
        end
  expand_non_void_self_closing(out)
end

def format_fixture_ready(html_string)

suppresses the ++ prefix.
elements self-closed, non-void paired); +NO_DECLARATION+
indentation; +AS_XHTML+ produces well-shaped output (void
+FORMAT+ + +AS_XHTML+ + +NO_DECLARATION+. +FORMAT+ inserts
input shapes), then write through libxml's XML writer with
get permissive recovery on real-world Word / XHTML5 / HTML5
Fixture-ready serialisation: parse with Nokogiri::HTML5 (so we
def format_fixture_ready(html_string)
  doc = Nokogiri::HTML5(html_string)
  strip_structural_whitespace!(doc)
  io = StringIO.new
  if @indent_type == "tab"
    doc.write_to(io, save_with: fixture_ready_save_options,
                     indent: 1, indent_text: "\t")
  else
    doc.write_to(io, save_with: fixture_ready_save_options,
                     indent: @indent)
  end
  io.string
end

def initialize(indent: 2, indent_type: "space", fixture_ready: false)

def initialize(indent: 2, indent_type: "space", fixture_ready: false)
  @indent = indent.to_i
  @indent_type = indent_type
  @fixture_ready = fixture_ready
end

def parent_has_real_text?(parent)

def parent_has_real_text?(parent)
  parent.children.any? do |c|
    c.text? && !c.content.strip.empty?
  end
end

def strip_structural_whitespace!(doc)

+

foo bar baz

+ are left alone.
whitespace-preserving element. Mixed-content runs like
parent is structural (no real text content) and not a
do it manually here: drop whitespace-only text nodes whose
strip these inter-sibling text nodes pre-serialisation, so we
not accept the +noblanks+ option that the XML parser uses to
non-whitespace-only text node child). +Nokogiri::HTML5+ does
the children of any element it sees as mixed content (any
libxml's +FORMAT+ save flag does not insert indentation around
def strip_structural_whitespace!(doc)
  to_remove = []
  doc.traverse do |node|
    next unless node.text?
    next unless node.content.strip.empty?
    parent = node.parent
    next if parent.nil?
    next if WHITESPACE_PRESERVING_ELEMENTS.include?(parent.name)
    next if parent_has_real_text?(parent)
    to_remove << node
  end
  to_remove.each(&:remove)
end

def xhtml?(html_string)

def xhtml?(html_string)
  html_string.include?("XHTML") ||
    html_string.include?('xmlns="http://www.w3.org/1999/xhtml"')
end