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)
+ and +
that is not an HTML5 void element. ++ is illegal HTML;
Rewrite +
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)
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