]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
diagnostics: rework experimental-html output [PR116792]
authorDavid Malcolm <dmalcolm@redhat.com>
Tue, 27 May 2025 16:22:26 +0000 (12:22 -0400)
committerDavid Malcolm <dmalcolm@redhat.com>
Tue, 27 May 2025 16:31:19 +0000 (12:31 -0400)
This patch reworks the HTML output from the the option
  -fdiagnostics-add-output=experimental-html
so that for source quoting and path printing, rather than simply adding
the textual output inside <pre> elements, it breaks up the output
into HTML tags reflecting the structure of the output, using CSS, SVG
and a little javascript to help the user navigate the diagnostics and
the events within any paths.

This uses ideas from the patch I posted in:
  https://gcc.gnu.org/pipermail/gcc-patches/2020-November/558603.html
but reworks the above patch so that:
* rather than printing source to a pretty_printer, the HTML is created
  by building a DOM tree in memory, using a new xml::printer class.  This
  should be less error-prone than the pretty_printer approach, since it
  ought to solve escaping issues.  Instead of a text vs html boolean,
  the code is generalized via templates with to_text vs to_html sinks.
  This templatization applies both to path-printing and
  diagnostic-show-locus.cc.
* the HTML output can have multiple diagnostics and multiple paths rather
  than just a single path.  The javascript keyboard controls now cycle
  through all diagnostics and all events within them

An example of the output can be seen at:
  https://dmalcolm.fedorapeople.org/gcc/2025-05-27/malloc-1.c.html
where the keys "j" and "k" cycle through diagnostics and events
within them.

gcc/ChangeLog:
PR other/116792
* diagnostic-format-html.cc: Define INCLUDE_STRING.
Include "xml.h", "xml-printer.h", and "json.h".
(html_generation_options::html_generation_options): New.
(namespace xml): Move decls to xml.h and convert from using
label_text to std::string.
(xml::text::write_as_xml): Reimplement indentation so it is done
by this node, rather than the parent.
(xml::node_with_children::add_text): Convert from label_text to
std::string.  Consolidate runs of text into a single node.
(xml::document::write_as_xml): Reimplement indentation.
(xml::element::write_as_xml): Reimplement indentation so it is
done by this node, rather than the parent.  Convert from
label_text to std::string.  Update attribute-printing to new
representation to preserve insertion order.
(xml::element::set_attr): Convert from label_text to std::string.
Record insertion order.
(xml::raw::write_as_xml): New.
(xml::printer::printer): New.
(xml::printer::push_tag): New.
(xml::printer::push_tag_with_class): New.
(xml::printer::pop_tag): New.
(xml::printer::set_attr): New.
(xml::printer::add_text): New.
(xml::printer::add_raw): New.
(xml::printer::push_element): New.
(xml::printer::append): New.
(xml::printer::get_insertion_point): New.
(html_builder::add_focus_id): New.
(html_builder::m_html_gen_opts): New field.
(html_builder::m_head_element): New field.
(html_builder::m_next_diag_id): New field.
(html_builder::m_ui_focus_ids): New field.
(make_div): Convert from label_text to std::string.
(make_span): Likewise.
(HTML_STYLE): New.
(HTML_SCRIPT): New.
(html_builder::html_builder): Fix indentation.  Add
"html_gen_opts" param.  Initialize new fields.  Reimplement
using xml::printer.  Optionally add style and script tags.
(class html_path_label_writer): New.
(html_builder::make_element_for_diagnostic): Convert from
label_text to std::string. Set "id" on "gcc-diagnostic" and
"gcc-message" <div> elements; add the latter to the focus ids.
Use diagnostic_context::maybe_show_locus_as_html rather than
html_builder::make_element_for_source.  Use print_path_as_html
rather than html_builder::make_element_for_path.
(html_builder::make_element_for_source): Drop.
(html_builder::make_element_for_path): Drop.
(html_builder::make_element_for_patch): Convert from label_text to
std::string.
(html_builder::make_metadata_element): Likewise.  Use
xml::printer.
(html_builder::make_element_for_metadata): Convert from label_text
to std::string.
(html_builder::emit_diagram): Expand comment.
(html_builder::flush_to_file): Write out initializer for
"focus_ids" into javascript.
(html_output_format::html_output_format): Add param
"html_gen_opts" and use it to initialize m_builder.
(html_file_output_format::html_file_output_format): Likewise, to
initialize base class.
(make_html_sink): Likewise, to pass to ctor.
(selftest::test_html_diagnostic_context::test_html_diagnostic_context):
Set up html_generation_options.
(selftest::html_buffered_output_format::html_buffered_output_format):
Add html_gen_opts param.
(selftest::test_simple_log): Add id attributes to expected text
for "gcc-diagnostic" and "gcc-message" elements.  Update
whitespace for indentation fixes.
(selftest::test_metadata): Update whitespace for indentation
fixes.
(selftest::test_printer): New selftest.
(selftest::test_attribute_ordering): New selftest.
(selftest::diagnostic_format_html_cc_tests): Call the new
selftests.
* diagnostic-format-html.h (struct html_generation_options): New.
(make_html_sink): Add "html_gen_opts" param.
(print_path_as_html): New decl.
* diagnostic-path-output.cc: Define INCLUDE_MAP.  Add includes of
"diagnostic-format-html.h", "xml.h", and "xml-printer.h".
(path_print_policy::path_print_policy): Add ctor.
(path_print_policy::get_diagram_theme): Fix whitespace.
(struct stack_frame): New.
(begin_html_stack_frame): New function.
(end_html_stack_frame): New function.
(emit_svg_arrow): New function.
(event_range::print): Rename to...
(event_range::print_as_text): ...this.  Update call to
diagnostic_start_span.
(event_range::print_as_html): New, based on the above, but ported
from pretty_printer to xml::printer.
(thread_event_printer::print_swimlane_for_event_range): Rename
to...
(thread_event_printer::print_swimlane_for_event_range_as_text):
...this.  Update for renaming of event_range::print to
event_range::print_as_text.
(thread_event_printer::print_swimlane_for_event_range_as_html):
New.
(print_path_summary_as_text): Update for "_as_text" renaming.
(print_path_summary_as_html): New.
(print_path_as_html): New.
* diagnostic-show-locus.cc: Add defines of INCLUDE_MAP and
INCLUDE_STRING.  Add includes of "xml.h" and "xml-printer.h".
(struct char_display_policy): Replace "m_print_cb" with
"m_print_text_cb" and "m_print_html_cb".
(struct to_text): New.
(struct to_html): New.
(get_printer): New.
(default_diagnostic_start_span_fn<to_text>): New.
(default_diagnostic_start_span_fn<to_html>): New.
(class layout): Update "friend class layout_printer;" for
template.
(enum class margin_kind): New.
(class layout_printer): Convert into a template.
(layout_printer::m_pp): Replace field with...
(layout_printer::m_sink): ...this.
(layout_printer::m_colorizer): Drop field in favor of a pointer
in the "to_text" sink.
(default_print_decoded_ch): Convert into a template.
(escape_as_bytes_print): Likewise.
(escape_as_unicode_print): Likewise.
(make_char_policy): Update to use both text and html callbacks.
(layout_printer::print_gap_in_line_numbering): Replace with...
(layout_printer<to_text>::print_gap_in_line_numbering): ...this
(layout_printer<to_html>::print_gap_in_line_numbering): ...and
this.
(layout_printer::print_source_line): Convert to template, using
m_sink.
(layout_printer::print_leftmost_column): Likewise.
(layout_printer::start_annotation_line): Likewise.
(layout_printer<to_text>::end_line): New.
(layout_printer<to_html>::end_line): New.
(layout_printer::print_annotation_line): Convert to template,
using m_sink.
(class line_label): Add field m_original_range_idx.
(layout_printer<to_text>::begin_label): New.
(layout_printer<to_html>::begin_label): New.
(layout_printer<to_text>::end_label): New.
(layout_printer<to_html>::end_label): New.
(layout_printer::print_any_labels): Convert to template, using
m_sink.
(layout_printer::print_leading_fixits): Likewise.
(layout_printer::print_trailing_fixits): Likewise.
(layout_printer::print_newline): Drop.
(layout_printer::move_to_column): Convert to template, using
m_sink.
(layout_printer::show_ruler): Likewise.
(layout_printer::print_line): Likewise.
(layout_printer::print_any_right_to_left_edge_lines): Likewise.
(layout_printer::layout_printer): Likewise.
(diagnostic_context::maybe_show_locus_as_html): New.
(diagnostic_source_print_policy::diagnostic_source_print_policy):
Update for split of start_span_cb into text vs html variants.
(diagnostic_source_print_policy::print): Update for use of
templates; use to_text.
(diagnostic_source_print_policy::print_as_html): New.
(layout_printer::print): Convert to template, using m_sink.
(selftest::make_element_for_locus): New.
(selftest::make_raw_html_for_locus): New.
(selftest::test_layout_x_offset_display_utf8): Update for use of
templates.
(selftest::test_layout_x_offset_display_tab): Likewise.
(selftest::test_one_liner_caret_and_range): Add test coverage of
HTML output.
(selftest::test_one_liner_labels): Likewise.
* diagnostic.cc (diagnostic_context::initialize): Update for split
of start_span_cb into text vs html variants.
(default_diagnostic_start_span_fn): Move to
diagnostic-show-locus.cc, converting to template.
* diagnostic.h (class xml::printer): New forward decl.
(diagnostic_start_span_fn): Replace typedef with "using",
converting to a template.
(struct to_text): New forward decl.
(struct to_html): New forward decl.
(get_printer): New decl.
(diagnostic_location_print_policy::print_text_span_start): New
decl.
(diagnostic_location_print_policy::print_html_span_start): New
decl.
(class html_label_writer): New.
(diagnostic_source_print_policy::print_as_html): New decl.
(diagnostic_source_print_policy::get_start_span_fn): Replace
with...
(diagnostic_source_print_policy::get_text_start_span_fn): ...this
(diagnostic_source_print_policy::get_html_start_span_fn): ...and
this
(diagnostic_source_print_policy::m_start_span_cb): Replace with...
(diagnostic_source_print_policy::m_text_start_span_cb): ...this
(diagnostic_source_print_policy::m_html_start_span_cb): ...and
this.
(diagnostic_context::maybe_show_locus_as_html): New decl.
(diagnostic_context::m_text_callbacks::m_start_span): Replace
with...
(diagnostic_context::m_text_callbacks::m_text_start_span): ...this
(diagnostic_context::m_text_callbacks::m_html_start_span): ...and
this.
(diagnostic_start_span): Update for template change.
(diagnostic_show_locus_as_html): New inline function.
(default_diagnostic_start_span_fn): Convert to template.
* doc/invoke.texi (experimental-html): Add "css" and "javascript"
keys.
* opts-diagnostic.cc (html_scheme_handler::make_sink): Likewise.
* selftest-diagnostic.cc
(selftest::test_diagnostic_context::start_span_cb): Update for
template changes.
* selftest-diagnostic.h
(selftest::test_diagnostic_context::start_span_cb): Likewise.
* xml-printer.h: New file.
* xml.h: New file, based on material in diagnostic-format-html.cc,
but using std::string rather than label_text.
(xml::element::m_key_insertion_order): New field.
(struct xml::raw): New.

gcc/fortran/ChangeLog
PR other/116792
* error.cc (gfc_diagnostic_start_span): Update for diagnostic.h
changes.

gcc/testsuite/ChangeLog:
PR other/116792
* gcc.dg/html-output/missing-semicolon.c: Add ":javascript=no" to
html output.
* gcc.dg/html-output/missing-semicolon.py: Move repeated
definitions into lib/htmltest.py.
* gcc.dg/plugin/diagnostic_group_plugin.cc: Update for template
changes.
* gcc.dg/plugin/diagnostic-test-metadata-html.c: Add
":javascript=no" to html output.  Add
"-fdiagnostics-show-line-numbers".
* gcc.dg/plugin/diagnostic-test-metadata-html.py: Move repeated
definitions into lib/htmltest.py.  Add checks of annotated source.
* gcc.dg/plugin/diagnostic-test-paths-2.c: Add ":javascript=no" to
html output.
* gcc.dg/plugin/diagnostic-test-paths-2.py: Move repeated
definitions into lib/htmltest.py.  Add checks of execution path.
* gcc.dg/plugin/diagnostic-test-paths-4.c: Add
-fdiagnostics-add-output=experimental-html:javascript=no.  Add
invocation ot diagnostic-test-paths-4.py.
* gcc.dg/plugin/diagnostic-test-paths-4.py: New test script.
* gcc.dg/plugin/diagnostic-test-show-locus-bw-line-numbers.c: Add
-fdiagnostics-add-output=experimental-html:javascript=no.  Add
invocation of diagnostic-test-show-locus.py.
* gcc.dg/plugin/diagnostic-test-show-locus.py: New test script.
* lib/htmltest.py: New test support script.

Signed-off-by: David Malcolm <dmalcolm@redhat.com>
25 files changed:
gcc/diagnostic-format-html.cc
gcc/diagnostic-format-html.h
gcc/diagnostic-path-output.cc
gcc/diagnostic-show-locus.cc
gcc/diagnostic.cc
gcc/diagnostic.h
gcc/doc/invoke.texi
gcc/fortran/error.cc
gcc/opts-diagnostic.cc
gcc/selftest-diagnostic.cc
gcc/selftest-diagnostic.h
gcc/testsuite/gcc.dg/html-output/missing-semicolon.c
gcc/testsuite/gcc.dg/html-output/missing-semicolon.py
gcc/testsuite/gcc.dg/plugin/diagnostic-test-metadata-html.c
gcc/testsuite/gcc.dg/plugin/diagnostic-test-metadata-html.py
gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-2.c
gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-2.py
gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-4.c
gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-4.py [new file with mode: 0644]
gcc/testsuite/gcc.dg/plugin/diagnostic-test-show-locus-bw-line-numbers.c
gcc/testsuite/gcc.dg/plugin/diagnostic-test-show-locus.py [new file with mode: 0644]
gcc/testsuite/gcc.dg/plugin/diagnostic_group_plugin.cc
gcc/testsuite/lib/htmltest.py
gcc/xml-printer.h [new file with mode: 0644]
gcc/xml.h [new file with mode: 0644]

index 6bb1caf41d120f0e07571fc5b50efc847abbacda..f2b255bf9cd2088a3593e28d7d1ce059e328e82f 100644 (file)
@@ -20,6 +20,7 @@ along with GCC; see the file COPYING3.  If not see
 
 #include "config.h"
 #define INCLUDE_MAP
+#define INCLUDE_STRING
 #define INCLUDE_VECTOR
 #include "system.h"
 #include "coretypes.h"
@@ -36,6 +37,17 @@ along with GCC; see the file COPYING3.  If not see
 #include "pretty-print-urlifier.h"
 #include "edit-context.h"
 #include "intl.h"
+#include "xml.h"
+#include "xml-printer.h"
+#include "json.h"
+
+// struct html_generation_options
+
+html_generation_options::html_generation_options ()
+: m_css (true),
+  m_javascript (true)
+{
+}
 
 namespace xml {
 
@@ -46,57 +58,6 @@ namespace xml {
 #  pragma GCC diagnostic ignored "-Wformat-diag"
 #endif
 
-struct node
-{
-  virtual ~node () {}
-  virtual void write_as_xml (pretty_printer *pp,
-                            int depth, bool indent) const = 0;
-  void dump (FILE *out) const;
-  void DEBUG_FUNCTION dump () const { dump (stderr); }
-};
-
-struct text : public node
-{
-  text (label_text str)
-  : m_str (std::move (str))
-  {}
-
-  void write_as_xml (pretty_printer *pp,
-                    int depth, bool indent) const final override;
-
-  label_text m_str;
-};
-
-struct node_with_children : public node
-{
-  void add_child (std::unique_ptr<node> node);
-  void add_text (label_text str);
-
-  std::vector<std::unique_ptr<node>> m_children;
-};
-
-struct document : public node_with_children
-{
-  void write_as_xml (pretty_printer *pp,
-                    int depth, bool indent) const final override;
-};
-
-struct element : public node_with_children
-{
-  element (const char *kind, bool preserve_whitespace)
-  : m_kind (kind),
-    m_preserve_whitespace (preserve_whitespace)
-  {}
-
-  void write_as_xml (pretty_printer *pp,
-                    int depth, bool indent) const final override;
-
-  void set_attr (const char *name, label_text value);
-
-  const char *m_kind;
-  bool m_preserve_whitespace;
-  std::map<const char *, label_text> m_attributes;
-};
 
 /* Implementation.  */
 
@@ -146,9 +107,16 @@ node::dump (FILE *out) const
 /* struct text : public node.  */
 
 void
-text::write_as_xml (pretty_printer *pp, int /*depth*/, bool /*indent*/) const
+text::write_as_xml (pretty_printer *pp, int depth, bool indent) const
 {
-  write_escaped_text (pp, m_str.get ());
+  if (indent)
+    {
+      for (int i = 0; i < depth; ++i)
+       pp_string (pp, "  ");
+    }
+  write_escaped_text (pp, m_str.c_str ());
+  if (indent)
+    pp_newline (pp);
 }
 
 /* struct node_with_children : public node.  */
@@ -161,9 +129,15 @@ node_with_children::add_child (std::unique_ptr<node> node)
 }
 
 void
-node_with_children::add_text (label_text str)
+node_with_children::add_text (std::string str)
 {
-  gcc_assert (str.get ());
+  // Consolidate runs of text
+  if (!m_children.empty ())
+    if (text *t = m_children.back ()->dyn_cast_text ())
+      {
+       t->m_str += std::move (str);
+       return;
+      }
   add_child (std::make_unique <text> (std::move (str)));
 }
 
@@ -177,6 +151,8 @@ document::write_as_xml (pretty_printer *pp, int depth, bool indent) const
   pp_string (pp, "<!DOCTYPE html\n"
             "     PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n"
             "     \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
+  if (indent)
+    pp_newline (pp);
   for (auto &iter : m_children)
     iter->write_as_xml (pp, depth, indent);
 }
@@ -188,48 +164,139 @@ element::write_as_xml (pretty_printer *pp, int depth, bool indent) const
 {
   if (indent)
     {
-      pp_newline (pp);
       for (int i = 0; i < depth; ++i)
        pp_string (pp, "  ");
     }
 
-  if (m_preserve_whitespace)
-    indent = false;
-
-  pp_printf (pp, "<%s", m_kind);
-  for (auto &attr : m_attributes)
+  pp_printf (pp, "<%s", m_kind.c_str ());
+  for (auto &key : m_key_insertion_order)
     {
-      pp_printf (pp, " %s=\"", attr.first);
-      write_escaped_text (pp, attr.second.get ());
-      pp_string (pp, "\"");
+      auto iter = m_attributes.find (key);
+      if (iter != m_attributes.end ())
+       {
+         pp_printf (pp, " %s=\"", key.c_str ());
+         write_escaped_text (pp, iter->second.c_str ());
+         pp_string (pp, "\"");
+       }
     }
   if (m_children.empty ())
-    pp_string (pp, " />");
+    pp_string (pp, "/>");
   else
     {
+      const bool indent_children = m_preserve_whitespace ? false : indent;
       pp_string (pp, ">");
+      if (indent_children)
+       pp_newline (pp);
       for (auto &child : m_children)
-       child->write_as_xml (pp, depth + 1, indent);
-      if (indent)
+       child->write_as_xml (pp, depth + 1, indent_children);
+      if (indent_children)
        {
-         pp_newline (pp);
          for (int i = 0; i < depth; ++i)
            pp_string (pp, "  ");
        }
-      pp_printf (pp, "</%s>", m_kind);
+      pp_printf (pp, "</%s>", m_kind.c_str ());
     }
+
+  if (indent)
+    pp_newline (pp);
 }
 
 void
-element::set_attr (const char *name, label_text value)
+element::set_attr (const char *name, std::string value)
 {
+  auto iter = m_attributes.find (name);
+  if (iter == m_attributes.end ())
+    m_key_insertion_order.push_back (name);
   m_attributes[name] = std::move (value);
 }
 
+// struct raw : public node
+
+void
+raw::write_as_xml (pretty_printer *pp,
+                  int /*depth*/, bool /*indent*/) const
+{
+  pp_string (pp, m_xml_src.c_str ());
+}
+
 #if __GNUC__ >= 10
 #  pragma GCC diagnostic pop
 #endif
 
+// class printer
+
+printer::printer (element &insertion_point)
+{
+  m_open_tags.push_back (&insertion_point);
+}
+
+void
+printer::push_tag (std::string name,
+                  bool preserve_whitespace)
+{
+  push_element
+    (std::make_unique<element> (std::move (name),
+                               preserve_whitespace));
+}
+
+void
+printer::push_tag_with_class (std::string name, std::string class_,
+                             bool preserve_whitespace)
+{
+  auto new_element
+    = std::make_unique<element> (std::move (name),
+                                preserve_whitespace);
+  new_element->set_attr ("class", class_);
+  push_element (std::move (new_element));
+}
+
+void
+printer::pop_tag ()
+{
+  m_open_tags.pop_back ();
+}
+
+void
+printer::set_attr (const char *name, std::string value)
+{
+  m_open_tags.back ()->set_attr (name, value);
+}
+
+void
+printer::add_text (std::string text)
+{
+  element *parent = m_open_tags.back ();
+  parent->add_text (std::move (text));
+}
+
+void
+printer::add_raw (std::string text)
+{
+  element *parent = m_open_tags.back ();
+  parent->add_child (std::make_unique<xml::raw> (std::move (text)));
+}
+
+void
+printer::push_element (std::unique_ptr<element> new_element)
+{
+  element *parent = m_open_tags.back ();
+  m_open_tags.push_back (new_element.get ());
+  parent->add_child (std::move (new_element));
+}
+
+void
+printer::append (std::unique_ptr<node> new_node)
+{
+  element *parent = m_open_tags.back ();
+  parent->add_child (std::move (new_node));
+}
+
+element *
+printer::get_insertion_point () const
+{
+  return m_open_tags.back ();
+}
+
 } // namespace xml
 
 class html_builder;
@@ -284,7 +351,8 @@ public:
 
   html_builder (diagnostic_context &context,
                pretty_printer &pp,
-               const line_maps *line_maps);
+               const line_maps *line_maps,
+               const html_generation_options &html_gen_opts);
 
   void on_report_diagnostic (const diagnostic_info &diagnostic,
                             diagnostic_t orig_diag_kind,
@@ -309,15 +377,14 @@ public:
   std::unique_ptr<xml::element>
   make_element_for_metadata (const diagnostic_metadata &metadata);
 
-  std::unique_ptr<xml::element>
-  make_element_for_source (const diagnostic_info &diagnostic);
-
-  std::unique_ptr<xml::element>
-  make_element_for_path (const diagnostic_path &path);
-
   std::unique_ptr<xml::element>
   make_element_for_patch (const diagnostic_info &diagnostic);
 
+  void add_focus_id (std::string focus_id)
+  {
+    m_ui_focus_ids.append_string (focus_id.c_str ());
+  }
+
 private:
   std::unique_ptr<xml::element>
   make_element_for_diagnostic (const diagnostic_info &diagnostic,
@@ -330,14 +397,18 @@ private:
   diagnostic_context &m_context;
   pretty_printer *m_printer;
   const line_maps *m_line_maps;
+  html_generation_options m_html_gen_opts;
 
   std::unique_ptr<xml::document> m_document;
+  xml::element *m_head_element;
   xml::element *m_diagnostics_element;
   std::unique_ptr<xml::element> m_cur_diagnostic_element;
+  int m_next_diag_id; // for handing out unique IDs
+  json::array m_ui_focus_ids;
 };
 
 static std::unique_ptr<xml::element>
-make_div (label_text class_)
+make_div (std::string class_)
 {
   auto div = std::make_unique<xml::element> ("div", false);
   div->set_attr ("class", std::move (class_));
@@ -345,7 +416,7 @@ make_div (label_text class_)
 }
 
 static std::unique_ptr<xml::element>
-make_span (label_text class_)
+make_span (std::string class_)
 {
   auto span = std::make_unique<xml::element> ("span", true);
   span->set_attr ("class", std::move (class_));
@@ -400,45 +471,151 @@ diagnostic_html_format_buffer::flush ()
 
 /* class html_builder.  */
 
+/* Style information for writing out HTML paths.
+   Colors taken from https://www.patternfly.org/v3/styles/color-palette/ */
+
+static const char * const HTML_STYLE
+  = ("  <style>\n"
+     "    .linenum { color: white;\n"
+     "               background-color: #0088ce;\n"
+     "               white-space: pre;\n"
+     "               border-right: 1px solid black; }\n"
+     "    .ruler { color: red;\n"
+     "              white-space: pre; }\n"
+     "    .source { color: blue;\n"
+     "              white-space: pre; }\n"
+     "    .annotation { color: green;\n"
+     "                  white-space: pre; }\n"
+     "    .linenum-gap { text-align: center;\n"
+     "                   border-top: 1px solid black;\n"
+     "                   border-right: 1px solid black;\n"
+     "                   background-color: #ededed; }\n"
+     "    .source-gap { border-bottom: 1px dashed black;\n"
+     "                  border-top: 1px dashed black;\n"
+     "                  background-color: #ededed; }\n"
+     "    .no-locus-event { font-family: monospace;\n"
+     "                      color: green;\n"
+     "                      white-space: pre; }\n"
+     "    .funcname { font-weight: bold; }\n"
+     "    .events-hdr { color: white;\n"
+     "                  background-color: #030303; }\n"
+     "    .event-range {  border: 1px solid black;\n"
+     "                    padding: 0px; }\n"
+     "    .event-range-with-margin { border-spacing: 0; }\n"
+     "    .locus { font-family: monospace;\n"
+     "             border-spacing: 0px; }\n"
+     "    .selected { color: white;\n"
+     "                background-color: #0088ce; }\n"
+     "    .stack-frame-with-margin { border-spacing: 0; }\n"
+     "    .stack-frame {  padding: 5px;\n"
+     "                    box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.5); }\n"
+     "    .frame-funcname { text-align: right;\n"
+     "                      font-style: italic; } \n"
+     "  </style>\n");
+
+/* A little JavaScript for ease of navigation.
+   Keys j/k move forward and backward cyclically through a list
+   of focus ids (written out in another <script> tag as the HTML
+   is flushed).  */
+
+const char * const HTML_SCRIPT
+  = ("  var current_focus_idx = 0;\n"
+     "\n"
+     "  function get_focus_span (focus_idx)\n"
+     "  {\n"
+     "      const element_id = focus_ids[focus_idx];\n"
+     "      return document.getElementById(element_id);\n"
+     "  }\n"
+     "  function unhighlight_current_focus_idx ()\n"
+     "  {\n"
+     "      get_focus_span (current_focus_idx).classList.remove ('selected');\n"
+     "  }\n"
+     "  function highlight_current_focus_idx ()\n"
+     "  {\n"
+     "      const el = get_focus_span (current_focus_idx);\n"
+     "      el.classList.add ('selected');\n"
+     "      // Center the element on the screen\n"
+     "      const top_y = el.getBoundingClientRect ().top + window.pageYOffset;\n"
+     "      const middle = top_y - (window.innerHeight / 2);\n"
+     "      window.scrollTo (0, middle);\n"
+     "  }\n"
+     "  function select_prev_focus_idx ()\n"
+     "  {\n"
+     "      unhighlight_current_focus_idx ();\n"
+     "      if (current_focus_idx > 0)\n"
+     "          current_focus_idx -= 1;\n"
+     "      else\n"
+     "          current_focus_idx = focus_ids.length - 1;\n"
+     "      highlight_current_focus_idx ();\n"
+     "  }\n"
+     "  function select_next_focus_idx ()\n"
+     "  {\n"
+     "      unhighlight_current_focus_idx ();\n"
+     "      if (current_focus_idx < focus_ids.length - 1)\n"
+     "          current_focus_idx += 1;\n"
+     "      else\n"
+     "          current_focus_idx = 0;\n"
+     "      highlight_current_focus_idx ();\n"
+     "  }\n"
+     "  document.addEventListener('keydown', function (ev) {\n"
+     "      if (ev.key == 'j')\n"
+     "          select_next_focus_idx ();\n"
+     "      else if (ev.key == 'k')\n"
+     "          select_prev_focus_idx ();\n"
+     "  });\n"
+     "  highlight_current_focus_idx ();\n");
+
 /* html_builder's ctor.  */
 
 html_builder::html_builder (diagnostic_context &context,
-                             pretty_printer &pp,
-                             const line_maps *line_maps)
+                           pretty_printer &pp,
+                           const line_maps *line_maps,
+                           const html_generation_options &html_gen_opts)
 : m_context (context),
   m_printer (&pp),
-  m_line_maps (line_maps)
+  m_line_maps (line_maps),
+  m_html_gen_opts (html_gen_opts),
+  m_head_element (nullptr),
+  m_diagnostics_element (nullptr),
+  m_next_diag_id (0)
 {
   gcc_assert (m_line_maps);
 
   m_document = std::make_unique<xml::document> ();
   {
     auto html_element = std::make_unique<xml::element> ("html", false);
-    html_element->set_attr
-      ("xmlns",
-       label_text::borrow ("http://www.w3.org/1999/xhtml"));
+    html_element->set_attr ("xmlns",
+                           "http://www.w3.org/1999/xhtml");
+    xml::printer xp (*html_element.get ());
+    m_document->add_child (std::move (html_element));
+
     {
+      xml::auto_print_element head (xp, "head");
+      m_head_element = xp.get_insertion_point ();
       {
-       auto head_element = std::make_unique<xml::element> ("head", false);
+       xml::auto_print_element title (xp, "title", true);
+       xp.add_text ("Title goes here");
+      }
+      if (m_html_gen_opts.m_css)
+       xp.add_raw (HTML_STYLE);
+      if (m_html_gen_opts.m_javascript)
        {
-         auto title_element = std::make_unique<xml::element> ("title", true);
-         label_text title (label_text::borrow ("Title goes here")); // TODO
-         title_element->add_text (std::move (title));
-         head_element->add_child (std::move (title_element));
+         xp.push_tag ("script");
+         /* Escaping rules are different for HTML <script> elements,
+            so add the script "raw" for now.  */
+         xp.add_raw (HTML_SCRIPT);
+         xp.pop_tag (); // script
        }
-       html_element->add_child (std::move (head_element));
+    }
 
-       auto body_element = std::make_unique<xml::element> ("body", false);
-       {
-         auto diagnostics_element
-           = make_div (label_text::borrow ("gcc-diagnostic-list"));
-         m_diagnostics_element = diagnostics_element.get ();
-         body_element->add_child (std::move (diagnostics_element));
-       }
-       html_element->add_child (std::move (body_element));
+    {
+      xml::auto_print_element body (xp, "body");
+      {
+       auto diagnostics_element = make_div ("gcc-diagnostic-list");
+       m_diagnostics_element = diagnostics_element.get ();
+       xp.append (std::move (diagnostics_element));
       }
     }
-    m_document->add_child (std::move (html_element));
   }
 }
 
@@ -477,6 +654,45 @@ html_builder::on_report_diagnostic (const diagnostic_info &diagnostic,
     }
 }
 
+/* Custom subclass of html_label_writer.
+   Wrap labels within a <span> element, supplying them with event IDs.
+   Add the IDs to the list of focus IDs.  */
+
+class html_path_label_writer : public html_label_writer
+{
+public:
+  html_path_label_writer (xml::printer &xp,
+                         html_builder &builder,
+                         const std::string &event_id_prefix)
+  : m_xp (xp),
+    m_html_builder (builder),
+    m_event_id_prefix (event_id_prefix),
+    m_next_event_idx (0)
+  {
+  }
+
+  void begin_label () final override
+  {
+    m_xp.push_tag_with_class ("span", "event", true);
+    pretty_printer pp;
+    pp_printf (&pp, "%s%i",
+              m_event_id_prefix.c_str (), m_next_event_idx++);
+    m_xp.set_attr ("id", pp_formatted_text (&pp));
+    m_html_builder.add_focus_id (pp_formatted_text (&pp));
+  }
+
+  void end_label () final override
+  {
+    m_xp.pop_tag (); // span
+  }
+
+private:
+  xml::printer &m_xp;
+  html_builder &m_html_builder;
+  const std::string &m_event_id_prefix;
+  int m_next_event_idx;
+};
+
 std::unique_ptr<xml::element>
 html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
                                           diagnostic_t orig_diag_kind)
@@ -506,8 +722,7 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
              pp_token_text *sub = as_a <pp_token_text *> (iter);
              /* The value might be in the obstack, so we may need to
                 copy it.  */
-             insertion_element ().add_text
-               (label_text::take (xstrdup (sub->m_value.get ())));
+             insertion_element ().add_text (sub->m_value.get ());
            }
            break;
 
@@ -518,14 +733,14 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
 
          case pp_token::kind::begin_quote:
            {
-             insertion_element ().add_text (label_text::borrow (open_quote));
-             push_element (make_span (label_text::borrow ("gcc-quoted-text")));
+             insertion_element ().add_text (open_quote);
+             push_element (make_span ("gcc-quoted-text"));
            }
            break;
          case pp_token::kind::end_quote:
            {
              pop_element ();
-             insertion_element ().add_text (label_text::borrow (close_quote));
+             insertion_element ().add_text (close_quote);
            }
            break;
 
@@ -533,7 +748,7 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
            {
              pp_token_begin_url *sub = as_a <pp_token_begin_url *> (iter);
              auto anchor = std::make_unique<xml::element> ("a", true);
-             anchor->set_attr ("href", std::move (sub->m_value));
+             anchor->set_attr ("href", sub->m_value.get ());
              push_element (std::move (anchor));
            }
            break;
@@ -565,11 +780,24 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
     std::vector<xml::element *> m_open_elements;
   };
 
-  auto diag_element = make_div (label_text::borrow ("gcc-diagnostic"));
+  auto diag_element = make_div ("gcc-diagnostic");
+
+  const int diag_idx = m_next_diag_id++;
+  std::string diag_id;
+  {
+    pretty_printer pp;
+    pp_printf (&pp, "gcc-diag-%i", diag_idx);
+    diag_id = pp_formatted_text (&pp);
+  }
+  diag_element->set_attr ("id", diag_id);
 
   // TODO: might be nice to emulate the text output format, but colorize it
 
-  auto message_span = make_span (label_text::borrow ("gcc-message"));
+  auto message_span = make_span ("gcc-message");
+  std::string message_span_id (diag_id + "-message");
+  message_span->set_attr ("id", message_span_id);
+  add_focus_id (message_span_id);
+
   html_token_printer tok_printer (*this, *message_span.get ());
   m_printer->set_token_printer (&tok_printer);
   pp_output_formatted_text (m_printer, m_context.get_urlifier ());
@@ -579,7 +807,7 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
 
   if (diagnostic.metadata)
     {
-      diag_element->add_text (label_text::borrow (" "));
+      diag_element->add_text (" ");
       diag_element->add_child
        (make_element_for_metadata (*diagnostic.metadata));
     }
@@ -592,32 +820,47 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
       label_text option_url = label_text::take
        (m_context.make_option_url (diagnostic.option_id));
 
-      diag_element->add_text (label_text::borrow (" "));
-      auto option_span = make_span (label_text::borrow ("gcc-option"));
-      option_span->add_text (label_text::borrow ("["));
+      diag_element->add_text (" ");
+      auto option_span = make_span ("gcc-option");
+      option_span->add_text ("[");
       {
        if (option_url.get ())
          {
            auto anchor = std::make_unique<xml::element> ("a", true);
-           anchor->set_attr ("href", std::move (option_url));
-           anchor->add_text (std::move (option_text));
+           anchor->set_attr ("href", option_url.get ());
+           anchor->add_text (option_text.get ());
            option_span->add_child (std::move (anchor));
          }
        else
-         option_span->add_text (std::move (option_text));
-       option_span->add_text (label_text::borrow ("]"));
+         option_span->add_text (option_text.get ());
+       option_span->add_text ("]");
       }
       diag_element->add_child (std::move (option_span));
     }
 
   /* Source (and fix-it hints).  */
-  if (auto source_element = make_element_for_source (diagnostic))
-    diag_element->add_child (std::move (source_element));
+  {
+    xml::printer xp (*diag_element);
+    m_context.m_last_location = UNKNOWN_LOCATION;
+    m_context.maybe_show_locus_as_html (*diagnostic.richloc,
+                                       m_context.m_source_printing,
+                                       diagnostic.kind,
+                                       xp,
+                                       nullptr,
+                                       nullptr);
+  }
 
   /* Execution path.  */
   if (auto path = diagnostic.richloc->get_path ())
-    if (auto path_element = make_element_for_path (*path))
-      diag_element->add_child (std::move (path_element));
+    {
+      xml::printer xp (*diag_element);
+      std::string event_id_prefix (diag_id + "-event-");
+      html_path_label_writer event_label_writer (xp, *this,
+                                                event_id_prefix);
+      diagnostic_source_print_policy dspp (m_context);
+      print_path_as_html (xp, *path, m_context, &event_label_writer,
+                         dspp);
+    }
 
   if (auto patch_element = make_element_for_patch (diagnostic))
     diag_element->add_child (std::move (patch_element));
@@ -625,62 +868,24 @@ html_builder::make_element_for_diagnostic (const diagnostic_info &diagnostic,
   return diag_element;
 }
 
-std::unique_ptr<xml::element>
-html_builder::make_element_for_source (const diagnostic_info &diagnostic)
-{
-  // TODO: ideally we'd like to capture elements within the following:
-  m_context.m_last_location = UNKNOWN_LOCATION;
-  pp_clear_output_area (m_printer);
-  diagnostic_show_locus (&m_context,
-                        m_context.m_source_printing,
-                        diagnostic.richloc, diagnostic.kind,
-                        m_printer);
-  auto text = label_text::take (xstrdup (pp_formatted_text (m_printer)));
-  pp_clear_output_area (m_printer);
-
-  if (strlen (text.get ()) == 0)
-    return nullptr;
-
-  auto pre = std::make_unique<xml::element> ("pre", true);
-  pre->set_attr ("class", label_text::borrow ("gcc-annotated-source"));
-  pre->add_text (std::move (text));
-  return pre;
-}
-
-std::unique_ptr<xml::element>
-html_builder::make_element_for_path (const diagnostic_path &path)
-{
-  m_context.m_last_location = UNKNOWN_LOCATION;
-  diagnostic_text_output_format text_format (m_context);
-  pp_show_color (text_format.get_printer ()) = false;
-  pp_buffer (text_format.get_printer ())->m_flush_p = false;
-  // TODO: ideally we'd like to capture elements within the following:
-  text_format.print_path (path);
-  auto text = label_text::take
-    (xstrdup (pp_formatted_text (text_format.get_printer ())));
-
-  if (strlen (text.get ()) == 0)
-    return nullptr;
-
-  auto pre = std::make_unique<xml::element> ("pre", true);
-  pre->set_attr ("class", label_text::borrow ("gcc-execution-path"));
-  pre->add_text (std::move (text));
-  return pre;
-}
-
 std::unique_ptr<xml::element>
 html_builder::make_element_for_patch (const diagnostic_info &diagnostic)
 {
   edit_context ec (m_context.get_file_cache ());
   ec.add_fixits (diagnostic.richloc);
   if (char *diff = ec.generate_diff (true))
-    if (strlen (diff) > 0)
-      {
-       auto element = std::make_unique<xml::element> ("pre", true);
-       element->set_attr ("class", label_text::borrow ("gcc-generated-patch"));
-       element->add_text (label_text::take (diff));
-       return element;
-      }
+    {
+      if (strlen (diff) > 0)
+       {
+         auto element = std::make_unique<xml::element> ("pre", true);
+         element->set_attr ("class", "gcc-generated-patch");
+         element->add_text (diff);
+         free (diff);
+         return element;
+       }
+      else
+       free (diff);
+    }
   return nullptr;
 }
 
@@ -688,22 +893,23 @@ std::unique_ptr<xml::element>
 html_builder::make_metadata_element (label_text label,
                                     label_text url)
 {
-  auto item = make_span (label_text::borrow ("gcc-metadata-item"));
-  item->add_text (label_text::borrow ("["));
+  auto item = make_span ("gcc-metadata-item");
+  xml::printer xp (*item.get ());
+  xp.add_text ("[");
   {
-    auto anchor = std::make_unique<xml::element> ("a", true);
-    anchor->set_attr ("href", std::move (url));
-    anchor->add_child (std::make_unique<xml::text> (std::move (label)));
-    item->add_child (std::move (anchor));
+    xp.push_tag ("a", true);
+    xp.set_attr ("href", url.get ());
+    xp.add_text (label.get ());
+    xp.pop_tag ();
   }
-  item->add_text (label_text::borrow ("]"));
+  xp.add_text ("]");
   return item;
 }
 
 std::unique_ptr<xml::element>
 html_builder::make_element_for_metadata (const diagnostic_metadata &metadata)
 {
-  auto span_metadata = make_span (label_text::borrow ("gcc-metadata"));
+  auto span_metadata = make_span ("gcc-metadata");
 
   int cwe = metadata.get_cwe ();
   if (cwe)
@@ -737,7 +943,7 @@ html_builder::emit_diagram (const diagnostic_diagram &/*diagram*/)
   /* We must be within the emission of a top-level diagnostic.  */
   gcc_assert (m_cur_diagnostic_element);
 
-  // TODO
+  // TODO: currently a no-op
 }
 
 /* Implementation of "end_group_cb" for HTML output.  */
@@ -757,6 +963,20 @@ html_builder::end_group ()
 void
 html_builder::flush_to_file (FILE *outf)
 {
+  if (m_html_gen_opts.m_javascript)
+    {
+      gcc_assert (m_head_element);
+      xml::printer xp (*m_head_element);
+      /* Add an initialization of the global js variable "focus_ids"
+        using the array of IDs we saved as we went.  */
+      xp.push_tag ("script");
+      pretty_printer pp;
+      pp_string (&pp, "focus_ids = ");
+      m_ui_focus_ids.print (&pp, true);
+      pp_string (&pp, ";\n");
+      xp.add_raw (pp_formatted_text (&pp));
+      xp.pop_tag (); // script
+    }
   auto top = m_document.get ();
   top->dump (outf);
   fprintf (outf, "\n");
@@ -842,9 +1062,10 @@ public:
 
 protected:
   html_output_format (diagnostic_context &context,
-                     const line_maps *line_maps)
+                     const line_maps *line_maps,
+                     const html_generation_options &html_gen_opts)
   : diagnostic_output_format (context),
-    m_builder (context, *get_printer (), line_maps),
+    m_builder (context, *get_printer (), line_maps, html_gen_opts),
     m_buffer (nullptr)
   {}
 
@@ -857,8 +1078,9 @@ class html_file_output_format : public html_output_format
 public:
   html_file_output_format (diagnostic_context &context,
                           const line_maps *line_maps,
+                          const html_generation_options &html_gen_opts,
                           diagnostic_output_file output_file)
-  : html_output_format (context, line_maps),
+  : html_output_format (context, line_maps, html_gen_opts),
     m_output_file (std::move (output_file))
   {
     gcc_assert (m_output_file.get_open_file ());
@@ -922,11 +1144,13 @@ diagnostic_output_format_open_html_file (diagnostic_context &context,
 std::unique_ptr<diagnostic_output_format>
 make_html_sink (diagnostic_context &context,
                const line_maps &line_maps,
+               const html_generation_options &html_gen_opts,
                diagnostic_output_file output_file)
 {
   auto sink
     = std::make_unique<html_file_output_format> (context,
                                                 &line_maps,
+                                                html_gen_opts,
                                                 std::move (output_file));
   sink->update_printer ();
   return sink;
@@ -945,8 +1169,12 @@ class test_html_diagnostic_context : public test_diagnostic_context
 public:
   test_html_diagnostic_context ()
   {
+    html_generation_options html_gen_opts;
+    html_gen_opts.m_css = false;
+    html_gen_opts.m_javascript = false;
     auto sink = std::make_unique<html_buffered_output_format> (*this,
-                                                              line_table);
+                                                              line_table,
+                                                              html_gen_opts);
     sink->update_printer ();
     m_format = sink.get (); // borrowed
 
@@ -968,8 +1196,9 @@ private:
   {
   public:
     html_buffered_output_format (diagnostic_context &context,
-                                const line_maps *line_maps)
-    : html_output_format (context, line_maps)
+                                const line_maps *line_maps,
+                                const html_generation_options &html_gen_opts)
+    : html_output_format (context, line_maps, html_gen_opts)
     {
     }
     bool machine_readable_stderr_p () const final override
@@ -1009,12 +1238,12 @@ test_simple_log ()
       "  </head>\n"
       "  <body>\n"
       "    <div class=\"gcc-diagnostic-list\">\n"
-      "      <div class=\"gcc-diagnostic\">\n"
-      "        <span class=\"gcc-message\">this is a test: `<span class=\"gcc-quoted-text\">foo</span>&apos;</span>\n"
+      "      <div class=\"gcc-diagnostic\" id=\"gcc-diag-0\">\n"
+      "        <span class=\"gcc-message\" id=\"gcc-diag-0-message\">this is a test: `<span class=\"gcc-quoted-text\">foo</span>&apos;</span>\n"
       "      </div>\n"
       "    </div>\n"
       "  </body>\n"
-      "</html>"));
+      "</html>\n"));
 }
 
 static void
@@ -1031,7 +1260,6 @@ test_metadata ()
     element->write_as_xml (&pp, 0, true);
     ASSERT_STREQ
       (pp_formatted_text (&pp),
-       "\n"
        "<span class=\"gcc-metadata\">"
        "<span class=\"gcc-metadata-item\">"
        "["
@@ -1040,7 +1268,7 @@ test_metadata ()
        "</a>"
        "]"
        "</span>"
-       "</span>");
+       "</span>\n");
   }
 
   {
@@ -1053,7 +1281,6 @@ test_metadata ()
     element->write_as_xml (&pp, 0, true);
     ASSERT_STREQ
       (pp_formatted_text (&pp),
-       "\n"
        "<span class=\"gcc-metadata\">"
        "<span class=\"gcc-metadata-item\">"
        "["
@@ -1062,10 +1289,71 @@ test_metadata ()
        "</a>"
        "]"
        "</span>"
-       "</span>");
+       "</span>\n");
   }
 }
 
+static void
+test_printer ()
+{
+  xml::element top ("top", false);
+  xml::printer xp (top);
+  xp.push_tag ("foo");
+  xp.add_text ("hello");
+  xp.push_tag ("bar");
+  xp.set_attr ("size", "3");
+  xp.set_attr ("color", "red");
+  xp.add_text ("world");
+  xp.push_tag ("baz");
+  xp.pop_tag ();
+  xp.pop_tag ();
+  xp.pop_tag ();
+
+  pretty_printer pp;
+  top.write_as_xml (&pp, 0, true);
+  ASSERT_STREQ
+    (pp_formatted_text (&pp),
+     "<top>\n"
+     "  <foo>\n"
+     "    hello\n"
+     "    <bar size=\"3\" color=\"red\">\n"
+     "      world\n"
+     "      <baz/>\n"
+     "    </bar>\n"
+     "  </foo>\n"
+     "</top>\n");
+}
+
+// Verify that element attributes preserve insertion order.
+
+static void
+test_attribute_ordering ()
+{
+  xml::element top ("top", false);
+  xml::printer xp (top);
+  xp.push_tag ("chronological");
+  xp.set_attr ("maldon", "991");
+  xp.set_attr ("hastings", "1066");
+  xp.set_attr ("edgehill", "1642");
+  xp.set_attr ("naseby", "1645");
+  xp.pop_tag ();
+  xp.push_tag ("alphabetical");
+  xp.set_attr ("edgehill", "1642");
+  xp.set_attr ("hastings", "1066");
+  xp.set_attr ("maldon", "991");
+  xp.set_attr ("naseby", "1645");
+  xp.pop_tag ();
+
+  pretty_printer pp;
+  top.write_as_xml (&pp, 0, true);
+  ASSERT_STREQ
+    (pp_formatted_text (&pp),
+     "<top>\n"
+     "  <chronological maldon=\"991\" hastings=\"1066\" edgehill=\"1642\" naseby=\"1645\"/>\n"
+     "  <alphabetical edgehill=\"1642\" hastings=\"1066\" maldon=\"991\" naseby=\"1645\"/>\n"
+     "</top>\n");
+}
+
 /* Run all of the selftests within this file.  */
 
 void
@@ -1074,6 +1362,8 @@ diagnostic_format_html_cc_tests ()
   auto_fix_quotes fix_quotes;
   test_simple_log ();
   test_metadata ();
+  test_printer ();
+  test_attribute_ordering ();
 }
 
 } // namespace selftest
index ff5edcaf174921187f2dc3465ffc2828ce6bfaec..9ca43f253f03d576e8f73f85795c82250487c1ea 100644 (file)
@@ -24,6 +24,14 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic-format.h"
 #include "diagnostic-output-file.h"
 
+struct html_generation_options
+{
+  html_generation_options ();
+
+  bool m_css;
+  bool m_javascript;
+};
+
 extern diagnostic_output_file
 diagnostic_output_format_open_html_file (diagnostic_context &context,
                                         line_maps *line_maps,
@@ -32,6 +40,14 @@ diagnostic_output_format_open_html_file (diagnostic_context &context,
 extern std::unique_ptr<diagnostic_output_format>
 make_html_sink (diagnostic_context &context,
                const line_maps &line_maps,
+               const html_generation_options &html_gen_opts,
                diagnostic_output_file output_file);
 
+extern void
+print_path_as_html (xml::printer &xp,
+                   const diagnostic_path &path,
+                   diagnostic_context &dc,
+                   html_label_writer *event_label_writer,
+                   const diagnostic_source_print_policy &dspp);
+
 #endif /* ! GCC_DIAGNOSTIC_FORMAT_HTML_H */
index 3fe6a0da0492ebb56f29474e1475ec8a38a4df45..4c17865f3c55024295bf35d4ace5117fbfe3664f 100644 (file)
@@ -20,6 +20,7 @@ along with GCC; see the file COPYING3.  If not see
 
 #include "config.h"
 #define INCLUDE_ALGORITHM
+#define INCLUDE_MAP
 #define INCLUDE_STRING
 #define INCLUDE_VECTOR
 #include "system.h"
@@ -38,6 +39,9 @@ along with GCC; see the file COPYING3.  If not see
 #include "selftest-diagnostic-path.h"
 #include "text-art/theme.h"
 #include "diagnostic-format-text.h"
+#include "diagnostic-format-html.h"
+#include "xml.h"
+#include "xml-printer.h"
 
 /* Disable warnings about missing quoting in GCC diagnostics for the print
    calls below.  */
@@ -60,10 +64,15 @@ public:
   {
   }
 
+  path_print_policy (const diagnostic_context &dc)
+  : m_source_policy (dc)
+  {
+  }
+
   text_art::theme *
   get_diagram_theme () const
   {
-    return  m_source_policy.get_diagram_theme ();
+    return m_source_policy.get_diagram_theme ();
   }
 
   const diagnostic_source_print_policy &
@@ -276,6 +285,119 @@ private:
   int m_max_depth;
 };
 
+/* A stack frame for use in HTML output, holding child stack frames,
+   and event ranges. */
+
+struct stack_frame
+{
+  stack_frame (std::unique_ptr<stack_frame> parent,
+              logical_location logical_loc,
+              int stack_depth)
+  : m_parent (std::move (parent)),
+    m_logical_loc (logical_loc),
+    m_stack_depth (stack_depth)
+  {}
+
+  std::unique_ptr<stack_frame> m_parent;
+  logical_location m_logical_loc;
+  const int m_stack_depth;
+};
+
+/* Begin emitting content relating to a new stack frame within PARENT.
+   Allocated a new stack_frame and return it.  */
+
+static std::unique_ptr<stack_frame>
+begin_html_stack_frame (xml::printer &xp,
+                       std::unique_ptr<stack_frame> parent,
+                       logical_location logical_loc,
+                       int stack_depth,
+                       const logical_location_manager *logical_loc_mgr)
+{
+  if (logical_loc)
+    {
+      gcc_assert (logical_loc_mgr);
+      xp.push_tag_with_class ("table", "stack-frame-with-margin", false);
+      xp.push_tag ("tr", false);
+      {
+       xp.push_tag_with_class ("td", "interprocmargin", false);
+       xp.set_attr ("style", "padding-left: 100px");
+       xp.pop_tag ();
+      }
+      xp.push_tag_with_class ("td", "stack-frame", false);
+      label_text funcname
+       = logical_loc_mgr->get_name_for_path_output (logical_loc);
+      if (funcname.get ())
+       {
+         xp.push_tag_with_class ("div", "frame-funcname", false);
+         xp.push_tag ("span", true);
+         xp.add_text (funcname.get ());
+         xp.pop_tag (); // span
+         xp.pop_tag (); // div
+       }
+    }
+  return std::make_unique<stack_frame> (std::move (parent),
+                                       logical_loc,
+                                       stack_depth);
+}
+
+/* Finish emitting content for FRAME and delete it.
+   Return parent.  */
+
+static std::unique_ptr<stack_frame>
+end_html_stack_frame (xml::printer &xp,
+                     std::unique_ptr<stack_frame> frame)
+{
+  auto parent = std::move (frame->m_parent);
+  if (frame->m_logical_loc)
+    {
+      xp.pop_tag (); // td
+      xp.pop_tag (); // tr
+      xp.pop_tag (); // table
+    }
+  return parent;
+}
+
+/* Append an HTML <div> element to XP containing an SVG arrow representing
+   a change in stack depth from OLD_DEPTH to NEW_DEPTH.  */
+
+static void
+emit_svg_arrow (xml::printer &xp, int old_depth, int new_depth)
+{
+  const int pixels_per_depth = 100;
+  const int min_depth = MIN (old_depth, new_depth);
+  const int base_x = 20;
+  const int excess = 30;
+  const int last_x
+    = base_x + (old_depth - min_depth) * pixels_per_depth;
+  const int this_x
+    = base_x + (new_depth - min_depth) * pixels_per_depth;
+  pretty_printer tmp_pp;
+  pretty_printer *pp = &tmp_pp;
+  pp_printf (pp, "<div class=\"%s\">\n",
+            old_depth < new_depth
+            ? "between-ranges-call" : "between-ranges-return");
+  pp_printf (pp, "  <svg height=\"30\" width=\"%i\">\n",
+            MAX (last_x, this_x) + excess);
+  pp_string
+    (pp,
+     "    <defs>\n"
+     "      <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"7\"\n"
+     "              refX=\"0\" refY=\"3.5\" orient=\"auto\" stroke=\"#0088ce\" fill=\"#0088ce\">\n"
+     "      <polygon points=\"0 0, 10 3.5, 0 7\"/>\n"
+     "      </marker>\n"
+     "    </defs>\n");
+  pp_printf (pp,
+            "    <polyline points=\"%i,0 %i,10 %i,10 %i,20\"\n",
+            last_x, last_x, this_x, this_x);
+  pp_string
+    (pp,
+     "              style=\"fill:none;stroke: #0088ce\"\n"
+     "              marker-end=\"url(#arrowhead)\"/>\n"
+     "  </svg>\n"
+     "</div>\n\n");
+  xp.add_raw (pp_formatted_text (pp));
+}
+
 /* A range of consecutive events within a diagnostic_path, all within the
    same thread, and with the same fndecl and stack_depth, and which are suitable
    to print with a single call to diagnostic_show_locus.  */
@@ -468,9 +590,9 @@ struct event_range
   /* Print the events in this range to PP, typically as a single
      call to diagnostic_show_locus.  */
 
-  void print (pretty_printer &pp,
-             diagnostic_text_output_format &text_output,
-             diagnostic_source_effect_info *effect_info)
+  void print_as_text (pretty_printer &pp,
+                     diagnostic_text_output_format &text_output,
+                     diagnostic_source_effect_info *effect_info)
   {
     location_t initial_loc = m_initial_event.get_location ();
 
@@ -487,7 +609,7 @@ struct event_range
        if (exploc.file != LOCATION_FILE (dc.m_last_location))
          {
            diagnostic_location_print_policy loc_policy (text_output);
-           diagnostic_start_span (&dc) (loc_policy, &pp, exploc);
+           loc_policy.print_text_span_start (dc, pp, exploc);
          }
       }
 
@@ -524,6 +646,65 @@ struct event_range
       }
   }
 
+  /* Print the events in this range to XP, typically as a single
+     call to diagnostic_show_locus_as_html.  */
+
+  void print_as_html (xml::printer &xp,
+                     diagnostic_context &dc,
+                     diagnostic_source_effect_info *effect_info,
+                     html_label_writer *event_label_writer)
+  {
+    location_t initial_loc = m_initial_event.get_location ();
+
+    /* Emit a span indicating the filename (and line/column) if the
+       line has changed relative to the last call to
+       diagnostic_show_locus.  */
+    if (dc.m_source_printing.enabled)
+      {
+       expanded_location exploc
+         = linemap_client_expand_location_to_spelling_point
+         (line_table, initial_loc, LOCATION_ASPECT_CARET);
+       if (exploc.file != LOCATION_FILE (dc.m_last_location))
+         {
+           diagnostic_location_print_policy loc_policy (dc);
+           loc_policy.print_html_span_start (dc, xp, exploc);
+         }
+      }
+
+    /* If we have an UNKNOWN_LOCATION (or BUILTINS_LOCATION) as the
+       primary location for an event, diagnostic_show_locus_as_html won't print
+       anything.
+
+       In particular the label for the event won't get printed.
+       Fail more gracefully in this case by showing the event
+       index and text, at no particular location.  */
+    if (get_pure_location (initial_loc) <= BUILTINS_LOCATION)
+      {
+       for (unsigned i = m_start_idx; i <= m_end_idx; i++)
+         {
+           const diagnostic_event &iter_event = m_path.get_event (i);
+           diagnostic_event_id_t event_id (i);
+           pretty_printer pp;
+           pp_printf (&pp, " %@: ", &event_id);
+           iter_event.print_desc (pp);
+           if (event_label_writer)
+             event_label_writer->begin_label ();
+           xp.add_text (pp_formatted_text (&pp));
+           if (event_label_writer)
+             event_label_writer->end_label ();
+         }
+       return;
+      }
+
+    /* Call diagnostic_show_locus_as_html to show the source,
+       showing events using labels.  */
+    diagnostic_show_locus_as_html (&dc, dc.m_source_printing,
+                                  &m_richloc, DK_DIAGNOSTIC_PATH, xp,
+                                  effect_info, event_label_writer);
+
+    // TODO: show macro expansions
+  }
+
   const diagnostic_path &m_path;
   const diagnostic_event &m_initial_event;
   logical_location m_logical_loc;
@@ -700,11 +881,11 @@ public:
   }
 
   void
-  print_swimlane_for_event_range (diagnostic_text_output_format &text_output,
-                                 pretty_printer *pp,
-                                 const logical_location_manager &logical_loc_mgr,
-                                 event_range *range,
-                                 diagnostic_source_effect_info *effect_info)
+  print_swimlane_for_event_range_as_text (diagnostic_text_output_format &text_output,
+                                         pretty_printer *pp,
+                                         const logical_location_manager &logical_loc_mgr,
+                                         event_range *range,
+                                         diagnostic_source_effect_info *effect_info)
   {
     gcc_assert (pp);
     const char *const line_color = "path";
@@ -785,7 +966,7 @@ public:
        }
        pp_set_prefix (pp, prefix);
        pp_prefixing_rule (pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE;
-       range->print (*pp, text_output, effect_info);
+       range->print_as_text (*pp, text_output, effect_info);
        pp_set_prefix (pp, saved_prefix);
 
        write_indent (pp, m_cur_indent + per_frame_indent);
@@ -795,7 +976,7 @@ public:
        pp_newline (pp);
       }
     else
-      range->print (*pp, text_output, effect_info);
+      range->print_as_text (*pp, text_output, effect_info);
 
     if (const event_range *next_range = get_any_next_range ())
       {
@@ -859,6 +1040,17 @@ public:
     m_num_printed++;
   }
 
+  void
+  print_swimlane_for_event_range_as_html (diagnostic_context &dc,
+                                         xml::printer &xp,
+                                         html_label_writer *event_label_writer,
+                                         event_range *range,
+                                         diagnostic_source_effect_info *effect_info)
+  {
+    range->print_as_html (xp, dc, effect_info, event_label_writer);
+    m_num_printed++;
+  }
+
   int get_cur_indent () const { return m_cur_indent; }
 
 private:
@@ -945,11 +1137,146 @@ print_path_summary_as_text (const path_summary &ps,
         of this range.  */
       diagnostic_source_effect_info effect_info;
       effect_info.m_leading_in_edge_column = last_out_edge_column;
-      tep.print_swimlane_for_event_range (text_output, pp,
-                                         ps.get_logical_location_manager (),
-                                         range, &effect_info);
+      tep.print_swimlane_for_event_range_as_text
+       (text_output, pp,
+        ps.get_logical_location_manager (),
+        range, &effect_info);
+      last_out_edge_column = effect_info.m_trailing_out_edge_column;
+    }
+}
+
+/* Print PS as HTML to XP, using DC and, if non-null EVENT_LABEL_WRITER.  */
+
+static void
+print_path_summary_as_html (const path_summary &ps,
+                           diagnostic_context &dc,
+                           xml::printer &xp,
+                           html_label_writer *event_label_writer,
+                           bool show_depths)
+{
+  std::vector<thread_event_printer> thread_event_printers;
+  for (auto t : ps.m_per_thread_summary)
+    thread_event_printers.push_back (thread_event_printer (*t, show_depths));
+
+  const logical_location_manager *logical_loc_mgr
+    = dc.get_logical_location_manager ();
+
+  xp.push_tag_with_class ("div", "event-ranges", false);
+
+  /* Group the ranges into stack frames.  */
+  std::unique_ptr<stack_frame> curr_frame;
+  unsigned i;
+  event_range *range;
+  int last_out_edge_column = -1;
+  FOR_EACH_VEC_ELT (ps.m_ranges, i, range)
+    {
+      const int swimlane_idx
+       = range->m_per_thread_summary.get_swimlane_index ();
+
+      const logical_location this_logical_loc = range->m_logical_loc;
+      const int this_depth = range->m_stack_depth;
+      if (curr_frame)
+       {
+         int old_stack_depth = curr_frame->m_stack_depth;
+         if (this_depth > curr_frame->m_stack_depth)
+           {
+             emit_svg_arrow (xp, old_stack_depth, this_depth);
+             curr_frame
+               = begin_html_stack_frame (xp,
+                                         std::move (curr_frame),
+                                         range->m_logical_loc,
+                                         range->m_stack_depth,
+                                         logical_loc_mgr);
+           }
+         else
+           {
+             while (this_depth < curr_frame->m_stack_depth
+                    || this_logical_loc != curr_frame->m_logical_loc)
+               {
+                 curr_frame = end_html_stack_frame (xp, std::move (curr_frame));
+                 if (curr_frame == NULL)
+                   {
+                     curr_frame
+                       = begin_html_stack_frame (xp,
+                                                 nullptr,
+                                                 range->m_logical_loc,
+                                                 range->m_stack_depth,
+                                                 logical_loc_mgr);
+                     break;
+                   }
+               }
+             emit_svg_arrow (xp, old_stack_depth, this_depth);
+           }
+       }
+      else
+       {
+         curr_frame = begin_html_stack_frame (xp,
+                                              NULL,
+                                              range->m_logical_loc,
+                                              range->m_stack_depth,
+                                              logical_loc_mgr);
+       }
+
+      xp.push_tag_with_class ("table", "event-range-with-margin", false);
+      xp.push_tag ("tr", false);
+      xp.push_tag_with_class ("td", "event-range", false);
+      xp.push_tag_with_class ("div", "events-hdr", true);
+      if (range->m_logical_loc)
+       {
+         gcc_assert (logical_loc_mgr);
+         label_text funcname
+           = logical_loc_mgr->get_name_for_path_output (range->m_logical_loc);
+         if (funcname.get ())
+           {
+             xp.push_tag_with_class ("span", "funcname", true);
+             xp.add_text (funcname.get ());
+             xp.pop_tag (); //span
+             xp.add_text (": ");
+           }
+       }
+      {
+       xp.push_tag_with_class ("span", "event-ids", true);
+       pretty_printer pp;
+       if (range->m_start_idx == range->m_end_idx)
+         pp_printf (&pp, "event %i",
+                    range->m_start_idx + 1);
+       else
+         pp_printf (&pp, "events %i-%i",
+                    range->m_start_idx + 1, range->m_end_idx + 1);
+       xp.add_text (pp_formatted_text (&pp));
+       xp.pop_tag (); // span
+      }
+      if (show_depths)
+       {
+         xp.add_text (" ");
+         xp.push_tag_with_class ("span", "depth", true);
+         pretty_printer pp;
+         pp_printf (&pp, "(depth %i)", range->m_stack_depth);
+         xp.add_text (pp_formatted_text (&pp));
+         xp.pop_tag (); //span
+       }
+      xp.pop_tag (); // div
+
+      /* Print a run of events.  */
+      thread_event_printer &tep = thread_event_printers[swimlane_idx];
+      /* Wire up any trailing out-edge from previous range to leading in-edge
+        of this range.  */
+      diagnostic_source_effect_info effect_info;
+      effect_info.m_leading_in_edge_column = last_out_edge_column;
+      tep.print_swimlane_for_event_range_as_html (dc, xp, event_label_writer,
+                                                 range, &effect_info);
       last_out_edge_column = effect_info.m_trailing_out_edge_column;
+
+      xp.pop_tag (); // td
+      xp.pop_tag (); // tr
+      xp.pop_tag (); // table
     }
+
+  /* Close outstanding frames.  */
+  while (curr_frame)
+    curr_frame = end_html_stack_frame (xp, std::move (curr_frame));
+
+  xp.pop_tag (); // div
 }
 
 } /* end of anonymous namespace for path-printing code.  */
@@ -1049,6 +1376,32 @@ diagnostic_text_output_format::print_path (const diagnostic_path &path)
     }
 }
 
+/* Print PATH as HTML to XP, using DC and DSPP for settings.
+   If non-null, use EVENT_LABEL_WRITER when writing events.  */
+
+void
+print_path_as_html (xml::printer &xp,
+                   const diagnostic_path &path,
+                   diagnostic_context &dc,
+                   html_label_writer *event_label_writer,
+                   const diagnostic_source_print_policy &dspp)
+{
+  path_print_policy policy (dc);
+  const bool check_rich_locations = true;
+  const bool colorize = false;
+  const diagnostic_source_printing_options &source_printing_opts
+    = dspp.get_options ();
+  const bool show_event_links = source_printing_opts.show_event_links_p;
+  path_summary summary (policy,
+                       *dc.get_reference_printer (),
+                       path,
+                       check_rich_locations,
+                       colorize,
+                       show_event_links);
+  print_path_summary_as_html (summary, dc, xp, event_label_writer,
+                             dc.show_path_depths_p ());
+}
+
 #if CHECKING_P
 
 namespace selftest {
index 898efe74acf53962d115fa3424a860bfce4edafa..397fffbb79e883ff4eb4e23f3aabf672be439600 100644 (file)
@@ -19,6 +19,8 @@ along with GCC; see the file COPYING3.  If not see
 <http://www.gnu.org/licenses/>.  */
 
 #include "config.h"
+#define INCLUDE_MAP
+#define INCLUDE_STRING
 #define INCLUDE_VECTOR
 #include "system.h"
 #include "coretypes.h"
@@ -37,6 +39,8 @@ along with GCC; see the file COPYING3.  If not see
 #include "text-art/types.h"
 #include "text-art/theme.h"
 #include "diagnostic-label-effects.h"
+#include "xml.h"
+#include "xml-printer.h"
 
 #ifdef HAVE_TERMIOS_H
 # include <termios.h>
@@ -70,7 +74,8 @@ struct point_state
   bool draw_caret_p;
 };
 
-/* A class to inject colorization codes when printing the diagnostic locus.
+/* A class to inject colorization codes when printing the diagnostic locus
+   as text.
 
    It has one kind of colorization for each of:
      - normal text
@@ -369,17 +374,348 @@ struct char_display_policy : public cpp_char_column_policy
  public:
   char_display_policy (int tabstop,
                       int (*width_cb) (cppchar_t c),
-                      void (*print_cb) (pretty_printer *pp,
-                                        const cpp_decoded_char &cp))
+                      void (*print_text_cb) (to_text &sink,
+                                             const cpp_decoded_char &cp),
+                      void (*print_html_cb) (to_html &sink,
+                                             const cpp_decoded_char &cp))
   : cpp_char_column_policy (tabstop, width_cb),
-    m_print_cb (print_cb)
+    m_print_text_cb (print_text_cb),
+    m_print_html_cb (print_html_cb)
   {
   }
 
-  void (*m_print_cb) (pretty_printer *pp,
-                     const cpp_decoded_char &cp);
+  void (*m_print_text_cb) (to_text &sink,
+                          const cpp_decoded_char &cp);
+  void (*m_print_html_cb) (to_html &sink,
+                          const cpp_decoded_char &cp);
 };
 
+template <typename Sink> class layout_printer;
+
+} // anonymous namespace
+
+/* This code is written generically to write either:
+   - text, to a pretty_printer, potentially with colorization codes, or
+   - html, to an xml::printer, with nested HTML tags.
+
+   This is handled via a "Sink" template, which is either to_text
+   or to_html.  */
+
+/* Writing text output.  */
+
+struct to_text
+{
+  friend class layout_printer<to_text>;
+
+  to_text (pretty_printer &pp,
+          colorizer &colorizer)
+  : m_pp (pp),
+    m_colorizer (&colorizer)
+  {
+    m_saved_rule = pp_prefixing_rule (&m_pp);
+    pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE;
+  }
+  to_text (pretty_printer &pp,
+          colorizer *colorizer)
+  : m_pp (pp),
+    m_colorizer (colorizer)
+  {
+    m_saved_rule = pp_prefixing_rule (&m_pp);
+    pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE;
+  }
+  ~to_text ()
+  {
+
+    pp_prefixing_rule (&m_pp) = m_saved_rule;
+  }
+
+  static bool is_text () { return true; }
+  static bool is_html () { return false; }
+
+  void emit_text_prefix ()
+  {
+    pp_emit_prefix (&m_pp);
+  }
+
+  void push_html_tag (std::string, bool)
+  {
+    // no-op for text
+  }
+  void push_html_tag_with_class (std::string, std::string, bool)
+  {
+    // no-op for text
+  }
+  void pop_html_tag (std::string)
+  {
+    // no-op for text
+  }
+
+  void add_html_tag_with_class (std::string, std::string, bool)
+  {
+    // no-op for text
+  }
+
+  void add_space ()
+  {
+    pp_space (&m_pp);
+  }
+
+  void add_character (cppchar_t ch)
+  {
+    pp_unicode_character (&m_pp, ch);
+  }
+
+  void add_utf8_byte (char b)
+  {
+    pp_character (&m_pp, b);
+  }
+
+  void add_text (const char *text)
+  {
+    pp_string (&m_pp, text);
+  }
+
+  void print_decoded_char (const char_display_policy &char_policy,
+                          cpp_decoded_char cp)
+  {
+    char_policy.m_print_text_cb (*this, cp);
+  }
+
+  void colorize_text_ensure_normal ()
+  {
+    gcc_assert (m_colorizer);
+    m_colorizer->set_normal_text ();
+  }
+
+  void colorize_text_for_range_idx (int range_idx)
+  {
+    gcc_assert (m_colorizer);
+    m_colorizer->set_range (range_idx);
+  }
+
+  void colorize_text_for_cfg_edge ()
+  {
+    gcc_assert (m_colorizer);
+    m_colorizer->set_cfg_edge ();
+  }
+
+  void colorize_text_for_fixit_insert ()
+  {
+    gcc_assert (m_colorizer);
+    m_colorizer->set_fixit_insert ();
+  }
+
+  void colorize_text_for_fixit_delete ()
+  {
+    gcc_assert (m_colorizer);
+    m_colorizer->set_fixit_delete ();
+  }
+
+  void
+  invoke_start_span_fn (const diagnostic_source_print_policy &source_policy,
+                       const diagnostic_location_print_policy &loc_policy,
+                       const expanded_location &exploc)
+  {
+    source_policy.get_text_start_span_fn () (loc_policy, *this, exploc);
+  }
+
+  // Text-specific functions
+  void add_newline ()
+  {
+    pp_newline (&m_pp);
+  }
+
+  pretty_printer &m_pp;
+private:
+  colorizer *m_colorizer;
+  diagnostic_prefixing_rule_t m_saved_rule;
+};
+
+/* Writing HTML output.  */
+
+struct to_html
+{
+  friend class layout_printer<to_html>;
+
+  to_html (xml::printer &xp,
+          html_label_writer *html_label_writer)
+  : m_xp (xp),
+    m_html_label_writer (html_label_writer)
+  {}
+
+  static bool is_text () { return false; }
+  static bool is_html () { return true; }
+
+  void emit_text_prefix ()
+  {
+    // no-op for HTML
+  }
+
+  void push_html_tag (std::string name,
+                     bool preserve_whitespace)
+  {
+    m_xp.push_tag (std::move (name), preserve_whitespace);
+  }
+
+  void push_html_tag_with_class (std::string name,
+                                std::string class_,
+                                bool preserve_whitespace)
+  {
+    m_xp.push_tag_with_class (std::move (name),
+                             std::move (class_),
+                             preserve_whitespace);
+  }
+
+  void pop_html_tag (std::string /*name*/)
+  {
+    m_xp.pop_tag ();
+  }
+
+  void add_html_tag_with_class (std::string name,
+                               std::string class_,
+                               bool preserve_whitespace)
+  {
+    auto element = std::make_unique<xml::element> (std::move (name),
+                                                  preserve_whitespace);
+    element->set_attr ("class", std::move (class_));
+    m_xp.append (std::move (element));
+  }
+
+  void add_raw_html (const char *src)
+  {
+    m_xp.add_raw (src);
+  }
+
+  void add_space ()
+  {
+    m_xp.add_text (" ");
+  }
+
+  void add_character (cppchar_t ch)
+  {
+    pp_clear_output_area (&m_scratch_pp);
+    pp_unicode_character (&m_scratch_pp, ch);
+    m_xp.add_text (pp_formatted_text (&m_scratch_pp));
+  }
+
+  void add_utf8_byte (char b)
+  {
+    m_xp.add_text (std::string (1, b));
+  }
+
+  void add_text (const char *text)
+  {
+    m_xp.add_text (text);
+  }
+
+  void print_decoded_char (const char_display_policy &char_policy,
+                          cpp_decoded_char cp)
+  {
+    char_policy.m_print_html_cb (*this, cp);
+  }
+
+  void colorize_text_ensure_normal ()
+  {
+    // no-op for HTML
+  }
+
+  void colorize_text_for_range_idx (int)
+  {
+    // no-op for HTML
+  }
+
+  void colorize_text_for_cfg_edge ()
+  {
+    // no-op for HTML
+  }
+
+  void colorize_text_for_fixit_insert ()
+  {
+    // no-op for HTML
+  }
+
+  void colorize_text_for_fixit_delete ()
+  {
+    // no-op for HTML
+  }
+
+  void
+  invoke_start_span_fn (const diagnostic_source_print_policy &source_policy,
+                       const diagnostic_location_print_policy &loc_policy,
+                       const expanded_location &exploc)
+  {
+    source_policy.get_html_start_span_fn () (loc_policy, *this, exploc);
+  }
+
+  xml::printer &m_xp;
+private:
+  html_label_writer *m_html_label_writer;
+  pretty_printer m_scratch_pp;
+};
+
+void
+diagnostic_location_print_policy::
+print_text_span_start (const diagnostic_context &dc,
+                      pretty_printer &pp,
+                      const expanded_location &exploc)
+{
+  to_text sink (pp, nullptr);
+  diagnostic_source_print_policy source_policy (dc);
+  source_policy.get_text_start_span_fn () (*this, sink, exploc);
+}
+
+void
+diagnostic_location_print_policy::
+print_html_span_start (const diagnostic_context &dc,
+                      xml::printer &xp,
+                      const expanded_location &exploc)
+{
+  to_html sink (xp, nullptr);
+  diagnostic_source_print_policy source_policy (dc);
+  source_policy.get_html_start_span_fn () (*this, sink, exploc);
+}
+
+pretty_printer *
+get_printer (to_text &sink)
+{
+  return &sink.m_pp;
+}
+
+template<>
+void
+default_diagnostic_start_span_fn<to_text> (const diagnostic_location_print_policy &loc_policy,
+                                          to_text &sink,
+                                          expanded_location exploc)
+{
+  const diagnostic_column_policy &column_policy
+    = loc_policy.get_column_policy ();
+  label_text text
+    = column_policy.get_location_text (exploc,
+                                      loc_policy.show_column_p (),
+                                      pp_show_color (&sink.m_pp));
+  pp_string (&sink.m_pp, text.get ());
+  pp_newline (&sink.m_pp);
+}
+
+template<>
+void
+default_diagnostic_start_span_fn<to_html> (const diagnostic_location_print_policy &loc_policy,
+                                          to_html &sink,
+                                          expanded_location exploc)
+{
+  const diagnostic_column_policy &column_policy
+    = loc_policy.get_column_policy ();
+  label_text text
+    = column_policy.get_location_text (exploc,
+                                      loc_policy.show_column_p (),
+                                      false);
+  sink.m_xp.push_tag_with_class ("span", "location", true);
+  sink.m_xp.add_text (text.get ());
+  sink.m_xp.pop_tag (); // span
+}
+
+namespace {
+
 /* A class to control the overall layout when printing a diagnostic.
 
    The layout is determined within the constructor.
@@ -392,7 +728,8 @@ struct char_display_policy : public cpp_char_column_policy
 class layout
 {
  public:
-  friend class layout_printer;
+  friend class layout_printer<to_text>;
+  friend class layout_printer<to_html>;
 
   layout (const diagnostic_source_print_policy &source_policy,
          const rich_location &richloc,
@@ -458,16 +795,24 @@ class layout
   bool m_escape_on_output;
 };
 
-/* A bundle of state for printing a particular layout
-   to a particular pretty_printer.  */
+class line_label;
 
+enum class margin_kind
+{
+  normal,
+  insertion,
+  ruler
+};
+
+/* A bundle of state for printing a particular layout
+   to a particular Sink (either to_text or to_html).  */
+template <typename Sink>
 class layout_printer
 {
 public:
-  layout_printer (pretty_printer &pp,
+  layout_printer (Sink &sink,
                  const layout &layout,
-                 const rich_location &richloc,
-                 diagnostic_t diagnostic_kind);
+                 bool is_diagnostic_path);
 
   void print (const diagnostic_source_print_policy &source_policy);
 
@@ -485,11 +830,13 @@ private:
   line_bounds print_source_line (linenum_type row, const char *line,
                                 int line_bytes);
   void print_leftmost_column ();
-  void start_annotation_line (char margin_char = ' ');
+  void start_annotation_line (enum margin_kind);
+  void end_line ();
   void print_annotation_line (linenum_type row, const line_bounds lbounds);
   void print_any_labels (linenum_type row);
+  void begin_label (const line_label &label);
+  void end_label ();
   void print_trailing_fixits (linenum_type row);
-  void print_newline ();
 
   void
   move_to_column (int *column, int dest_column, bool add_left_margin);
@@ -497,9 +844,8 @@ private:
   void print_any_right_to_left_edge_lines ();
 
 private:
-  pretty_printer &m_pp;
+  Sink &m_sink;
   const layout &m_layout;
-  colorizer m_colorizer;
   bool m_is_diagnostic_path;
 
   /* Fields for handling links between labels (e.g. for showing CFG edges
@@ -1178,8 +1524,9 @@ fixit_cmp (const void *p_a, const void *p_b)
 /* Callback for char_display_policy::m_print_cb for printing source chars
    when not escaping the source.  */
 
+template <class Sink>
 static void
-default_print_decoded_ch (pretty_printer *pp,
+default_print_decoded_ch (Sink &sink,
                          const cpp_decoded_char &decoded_ch)
 {
   for (const char *ptr = decoded_ch.m_start_byte;
@@ -1187,11 +1534,11 @@ default_print_decoded_ch (pretty_printer *pp,
     {
       if (*ptr == '\0' || *ptr == '\r')
        {
-         pp_space (pp);
+         sink.add_space ();
          continue;
        }
 
-      pp_character (pp, *ptr);
+      sink.add_utf8_byte (*ptr);
     }
 }
 
@@ -1219,8 +1566,9 @@ escape_as_bytes_width (cppchar_t ch)
 /* Callback for char_display_policy::m_print_cb for printing source chars
    when escaping with DIAGNOSTICS_ESCAPE_FORMAT_BYTES.  */
 
+template <typename Sink>
 static void
-escape_as_bytes_print (pretty_printer *pp,
+escape_as_bytes_print (Sink &sink,
                       const cpp_decoded_char &decoded_ch)
 {
   if (!decoded_ch.m_valid_ch)
@@ -1230,14 +1578,14 @@ escape_as_bytes_print (pretty_printer *pp,
        {
          char buf[16];
          sprintf (buf, "<%02x>", (unsigned char)*iter);
-         pp_string (pp, buf);
+         sink.add_text (buf);
        }
       return;
     }
 
   cppchar_t ch = decoded_ch.m_ch;
   if (ch < 0x80 && ISPRINT (ch))
-    pp_character (pp, ch);
+    sink.add_character (ch);
   else
     {
       for (const char *iter = decoded_ch.m_start_byte;
@@ -1245,7 +1593,7 @@ escape_as_bytes_print (pretty_printer *pp,
        {
          char buf[16];
          sprintf (buf, "<%02x>", (unsigned char)*iter);
-         pp_string (pp, buf);
+         sink.add_text (buf);
        }
     }
 }
@@ -1275,24 +1623,25 @@ escape_as_unicode_width (cppchar_t ch)
 /* Callback for char_display_policy::m_print_cb for printing source chars
    when escaping with DIAGNOSTICS_ESCAPE_FORMAT_UNICODE.  */
 
+template <typename Sink>
 static void
-escape_as_unicode_print (pretty_printer *pp,
+escape_as_unicode_print (Sink &sink,
                         const cpp_decoded_char &decoded_ch)
 {
   if (!decoded_ch.m_valid_ch)
     {
-      escape_as_bytes_print (pp, decoded_ch);
+      escape_as_bytes_print<Sink> (sink, decoded_ch);
       return;
     }
 
   cppchar_t ch = decoded_ch.m_ch;
   if (ch < 0x80 && ISPRINT (ch))
-    pp_character (pp, ch);
+    sink.add_character (ch);
   else
     {
       char buf[16];
       sprintf (buf, "<U+%04X>", ch);
-      pp_string (pp, buf);
+      sink.add_text (buf);
     }
 }
 
@@ -1306,7 +1655,8 @@ make_char_policy (const diagnostic_source_print_policy &source_policy,
   char_display_policy result
     (source_policy.get_column_policy ().get_tabstop (),
      cpp_wcwidth,
-     default_print_decoded_ch);
+     default_print_decoded_ch<to_text>,
+     default_print_decoded_ch<to_html>);
 
   /* If the diagnostic suggests escaping non-ASCII bytes, then
      use policy from user-supplied options.  */
@@ -1319,11 +1669,13 @@ make_char_policy (const diagnostic_source_print_policy &source_policy,
          gcc_unreachable ();
        case DIAGNOSTICS_ESCAPE_FORMAT_UNICODE:
          result.m_width_cb = escape_as_unicode_width;
-         result.m_print_cb = escape_as_unicode_print;
+         result.m_print_text_cb = escape_as_unicode_print<to_text>;
+         result.m_print_html_cb = escape_as_unicode_print<to_html>;
          break;
        case DIAGNOSTICS_ESCAPE_FORMAT_BYTES:
          result.m_width_cb = escape_as_bytes_width;
-         result.m_print_cb = escape_as_bytes_print;
+         result.m_print_text_cb = escape_as_bytes_print<to_text>;
+         result.m_print_html_cb = escape_as_bytes_print<to_html>;
          break;
        }
     }
@@ -1530,17 +1882,32 @@ layout::will_show_line_p (linenum_type row) const
 /* Print a line showing a gap in the line numbers, for showing the boundary
    between two line spans.  */
 
+template<>
 void
-layout_printer::print_gap_in_line_numbering ()
+layout_printer<to_text>::print_gap_in_line_numbering ()
 {
   gcc_assert (m_layout.m_options.show_line_numbers_p);
 
-  pp_emit_prefix (&m_pp);
+  m_sink.emit_text_prefix ();
 
   for (int i = 0; i < m_layout.get_linenum_width () + 1; i++)
-    pp_character (&m_pp, '.');
+    m_sink.add_character ('.');
 
-  pp_newline (&m_pp);
+  m_sink.add_newline ();
+}
+
+template<>
+void
+layout_printer<to_html>::print_gap_in_line_numbering ()
+{
+  gcc_assert (m_layout.m_options.show_line_numbers_p);
+
+  m_sink.add_raw_html
+    ("<tbody class=\"line-span-jump\">\n"
+     "<tr class=\"line-span-jump-row\">"
+     "<td class=\"linenum-gap\">[...]</td>"
+     "<td class=\"source-gap\"/></tr>\n"
+     "</tbody>\n");
 }
 
 /* Return true iff we should print a heading when starting the
@@ -1854,21 +2221,32 @@ layout::calculate_x_offset_display ()
    colorization and tab expansion, this function tracks the line position in
    both byte and display column units.  */
 
+template<typename Sink>
 line_bounds
-layout_printer::print_source_line (linenum_type row, const char *line, int line_bytes)
+layout_printer<Sink>::print_source_line (linenum_type row,
+                                        const char *line,
+                                        int line_bytes)
 {
-  m_colorizer.set_normal_text ();
-
-  pp_emit_prefix (&m_pp);
+  m_sink.colorize_text_ensure_normal ();
+  m_sink.push_html_tag ("tr", true);
+  m_sink.emit_text_prefix ();
   if (m_layout.m_options.show_line_numbers_p)
     {
+      m_sink.push_html_tag_with_class ("td", "linenum", true);
       int width = num_digits (row);
       for (int i = 0; i < m_layout.get_linenum_width () - width; i++)
-       pp_space (&m_pp);
-      pp_printf (&m_pp, "%i |", row);
+       m_sink.add_space ();
+      char buf[20];
+      sprintf (buf, "%i", row);
+      m_sink.add_text (buf);
+      if (Sink::is_text ())
+       m_sink.add_text (" |");
+      m_sink.pop_html_tag ("td");
     }
 
+  m_sink.push_html_tag_with_class ("td", "left-margin", true);
   print_leftmost_column ();
+  m_sink.pop_html_tag ("td");
 
   /* We will stop printing the source line at any trailing whitespace.  */
   line_bytes = get_line_bytes_without_trailing_whitespace (line,
@@ -1879,6 +2257,8 @@ layout_printer::print_source_line (linenum_type row, const char *line, int line_
      tab expansion, and for implementing m_x_offset_display.  */
   cpp_display_width_computation dw (line, line_bytes, m_layout.m_char_policy);
 
+  m_sink.push_html_tag_with_class ("td", "source", true);
+
   /* Skip the first m_x_offset_display display columns.  In case the leading
      portion that will be skipped ends with a character with wcwidth > 1, then
      it is possible we skipped too much, so account for that by padding with
@@ -1889,7 +2269,7 @@ layout_printer::print_source_line (linenum_type row, const char *line, int line_
   for (int skipped_display_cols
         = dw.advance_display_cols (m_layout.m_x_offset_display);
        skipped_display_cols > m_layout.m_x_offset_display; --skipped_display_cols)
-    pp_space (&m_pp);
+    m_sink.add_space ();
 
   /* Print the line and compute the line_bounds.  */
   line_bounds lbounds;
@@ -1917,9 +2297,9 @@ layout_printer::print_source_line (linenum_type row, const char *line, int line_
                                                    CU_BYTES,
                                                    &state);
          if (in_range_p)
-           m_colorizer.set_range (state.range_idx);
+           m_sink.colorize_text_for_range_idx (state.range_idx);
          else
-           m_colorizer.set_normal_text ();
+           m_sink.colorize_text_ensure_normal ();
        }
 
       /* Get the display width of the next character to be output, expanding
@@ -1933,7 +2313,7 @@ layout_printer::print_source_line (linenum_type row, const char *line, int line_
          /* The returned display width is the number of spaces into which the
             tab should be expanded.  */
          for (int i = 0; i != this_display_width; ++i)
-           pp_space (&m_pp);
+           m_sink.add_space ();
          continue;
        }
 
@@ -1947,10 +2327,10 @@ layout_printer::print_source_line (linenum_type row, const char *line, int line_
        }
 
       /* Output the character.  */
-      m_layout.m_char_policy.m_print_cb (&m_pp, cp);
+      m_sink.print_decoded_char (m_layout.m_char_policy, cp);
       c = dw.next_byte ();
     }
-  print_newline ();
+  end_line ();
   return lbounds;
 }
 
@@ -1975,8 +2355,9 @@ layout::should_print_annotation_line_p (linenum_type row) const
 /* Print the leftmost column after the margin, which is used for showing
    links between labels (e.g. for CFG edges in execution paths).  */
 
+template<typename Sink>
 void
-layout_printer::print_leftmost_column ()
+layout_printer<Sink>::print_leftmost_column ()
 {
   if (!get_options ().show_event_links_p)
     gcc_assert (m_link_lhs_state == link_lhs_state::none);
@@ -1986,76 +2367,124 @@ layout_printer::print_leftmost_column ()
     default:
       gcc_unreachable ();
     case link_lhs_state::none:
-      pp_space (&m_pp);
+      m_sink.add_space ();
       break;
     case link_lhs_state::rewinding_to_lhs:
       {
-       m_colorizer.set_cfg_edge ();
+       m_sink.colorize_text_for_cfg_edge ();
        const cppchar_t ch = get_theme ().get_cppchar
          (text_art::theme::cell_kind::CFG_FROM_LEFT_TO_DOWN);
-       pp_unicode_character (&m_pp, ch);
-       m_colorizer.set_normal_text ();
+       m_sink.add_character (ch);
+       m_sink.colorize_text_ensure_normal ();
       }
       break;
     case link_lhs_state::at_lhs:
       {
-       m_colorizer.set_cfg_edge ();
+       m_sink.colorize_text_for_cfg_edge ();
        const cppchar_t ch = get_theme ().get_cppchar
          (text_art::theme::cell_kind::CFG_DOWN);
-       pp_unicode_character (&m_pp, ch);
-       m_colorizer.set_normal_text ();
+       m_sink.add_character (ch);
+       m_sink.colorize_text_ensure_normal ();
       }
       break;
     case link_lhs_state::indenting_to_dest:
       {
-       m_colorizer.set_cfg_edge ();
+       m_sink.colorize_text_for_cfg_edge ();
        const cppchar_t ch = get_theme ().get_cppchar
          (text_art::theme::cell_kind::CFG_FROM_DOWN_TO_RIGHT);
-       pp_unicode_character (&m_pp, ch);
-       m_colorizer.set_normal_text ();
+       m_sink.add_character (ch);
+       m_sink.colorize_text_ensure_normal ();
       }
       break;
     }
 }
 
-/* Begin an annotation line.  If m_show_line_numbers_p, print the left
-   margin, which is empty for annotation lines.
+/* Begin an annotation line for either text or html output
+
+   If m_show_line_numbers_p, print the left margin, which is empty
+   for annotation lines.
    After any left margin, print a leftmost column, which is used for
-   showing links between labels (e.g. for CFG edges in execution paths).  */
+   showing links between labels (e.g. for CFG edges in execution paths).
+
+   For text sinks, this also first prints the text prefix.
+   For html sinks, this also pushes <tr> and <td> open tags, where the
+   <td> is for the coming annotations.  */
 
+template<typename Sink>
 void
-layout_printer::start_annotation_line (char margin_char)
+layout_printer<Sink>::start_annotation_line (enum margin_kind margin)
 {
-  pp_emit_prefix (&m_pp);
+  m_sink.emit_text_prefix ();
+  m_sink.push_html_tag ("tr", true);
+
+  char margin_char = (margin == margin_kind::insertion
+                     ? '+'
+                     : ' ');
+
   if (get_options ().show_line_numbers_p)
     {
       /* Print the margin.  If MARGIN_CHAR != ' ', then print up to 3
         of it, right-aligned, padded with spaces.  */
+      m_sink.push_html_tag_with_class ("td", "linenum", true);
       int i;
       for (i = 0; i < m_layout.m_linenum_width - 3; i++)
-       pp_space (&m_pp);
+       m_sink.add_space ();
       for (; i < m_layout.m_linenum_width; i++)
-       pp_character (&m_pp, margin_char);
-      pp_string (&m_pp, " |");
+       m_sink.add_character (margin_char);
+      if (Sink::is_text ())
+       m_sink.add_text (" |");
+      m_sink.pop_html_tag ("td");
     }
-  if (margin_char == ' ')
-    print_leftmost_column ();
+
+  m_sink.push_html_tag_with_class ("td", "left-margin", true);
+  if (margin == margin_kind::insertion)
+    m_sink.add_character (margin_char);
   else
-    pp_character (&m_pp, margin_char);
+    print_leftmost_column ();
+  m_sink.pop_html_tag ("td");
+
+  m_sink.push_html_tag_with_class ("td",
+                                  (margin == margin_kind::ruler
+                                   ? "ruler"
+                                   : "annotation"),
+                                  true);
+}
+
+/* End a source or annotation line: text implementation.
+   Reset any colorization and emit a newline.  */
+
+template<>
+void
+layout_printer<to_text>::end_line ()
+{
+  m_sink.colorize_text_ensure_normal ();
+  m_sink.add_newline ();
+}
+
+/* End a source or annotation line: HTML implementation.
+   Close the <td> and <tr> tags.  */
+
+template<>
+void
+layout_printer<to_html>::end_line ()
+{
+  m_sink.pop_html_tag ("td");
+  m_sink.pop_html_tag ("tr");
 }
 
 /* Print a line consisting of the caret/underlines for the given
    source line.  */
 
+template<typename Sink>
 void
-layout_printer::print_annotation_line (linenum_type row,
-                                      const line_bounds lbounds)
+layout_printer<Sink>::print_annotation_line (linenum_type row,
+                                            const line_bounds lbounds)
 {
   int x_bound = m_layout.get_x_bound_for_row (row,
                                              m_layout.m_exploc.m_display_col,
                                              lbounds.m_last_non_ws_disp_col);
 
-  start_annotation_line ();
+  start_annotation_line (margin_kind::normal);
 
   for (int column = 1 + m_layout.m_x_offset_display; column < x_bound; column++)
     {
@@ -2069,7 +2498,7 @@ layout_printer::print_annotation_line (linenum_type row,
       if (in_range_p)
        {
          /* Within a range.  Draw either the caret or an underline.  */
-         m_colorizer.set_range (state.range_idx);
+         m_sink.colorize_text_for_range_idx (state.range_idx);
          if (state.draw_caret_p)
            {
              /* Draw the caret.  */
@@ -2078,19 +2507,20 @@ layout_printer::print_annotation_line (linenum_type row,
                caret_char = get_options ().caret_chars[state.range_idx];
              else
                caret_char = '^';
-             pp_character (&m_pp, caret_char);
+             m_sink.add_character (caret_char);
            }
          else
-           pp_character (&m_pp, '~');
+           m_sink.add_character ('~');
        }
       else
        {
          /* Not in a range.  */
-         m_colorizer.set_normal_text ();
-         pp_character (&m_pp, ' ');
+         m_sink.colorize_text_ensure_normal ();
+         m_sink.add_character (' ');
        }
     }
-  print_newline ();
+
+  end_line ();
 }
 
 /* A version of label_text that can live inside a vec.
@@ -2126,11 +2556,13 @@ struct pod_label_text
 class line_label
 {
 public:
-  line_label (int state_idx, int column,
+  line_label (unsigned original_range_idx,
+             int state_idx, int column,
              label_text text,
              bool has_in_edge,
              bool has_out_edge)
-  : m_state_idx (state_idx), m_column (column),
+  : m_original_range_idx (original_range_idx),
+    m_state_idx (state_idx), m_column (column),
     m_text (std::move (text)), m_label_line (0), m_has_vbar (true),
     m_has_in_edge (has_in_edge),
     m_has_out_edge (has_out_edge)
@@ -2158,6 +2590,7 @@ public:
     return -compare (ll1->m_state_idx, ll2->m_state_idx);
   }
 
+  unsigned m_original_range_idx;
   int m_state_idx;
   int m_column;
   pod_label_text m_text;
@@ -2168,9 +2601,43 @@ public:
   bool m_has_out_edge;
 };
 
+template<>
+void
+layout_printer<to_text>::begin_label (const line_label &label)
+{
+  /* Colorize the text, unless it's for events in a
+     diagnostic_path.  */
+  if (!m_is_diagnostic_path)
+    m_sink.colorize_text_for_range_idx (label.m_state_idx);
+}
+
+template<>
+void
+layout_printer<to_html>::begin_label (const line_label &)
+{
+  if (m_sink.m_html_label_writer)
+    m_sink.m_html_label_writer->begin_label ();
+}
+
+template<>
+void
+layout_printer<to_text>::end_label ()
+{
+  m_sink.colorize_text_ensure_normal ();
+}
+
+template<>
+void
+layout_printer<to_html>::end_label ()
+{
+  if (m_sink.m_html_label_writer)
+    m_sink.m_html_label_writer->end_label ();
+}
+
 /* Print any labels in this row.  */
+template <typename Sink>
 void
-layout_printer::print_any_labels (linenum_type row)
+layout_printer<Sink>::print_any_labels (linenum_type row)
 {
   int i;
   auto_vec<line_label> labels;
@@ -2203,7 +2670,8 @@ layout_printer::print_any_labels (linenum_type row)
        if (text.get () == NULL)
          continue;
 
-       labels.safe_push (line_label (i, disp_col, std::move (text),
+       labels.safe_push (line_label (range->m_original_idx,
+                                     i, disp_col, std::move (text),
                                      range->has_in_edge (),
                                      range->has_out_edge ()));
       }
@@ -2288,7 +2756,7 @@ layout_printer::print_any_labels (linenum_type row)
            gcc_assert (get_options ().show_event_links_p);
            m_link_lhs_state = link_lhs_state::indenting_to_dest;
          }
-       start_annotation_line ();
+       start_annotation_line (margin_kind::normal);
 
        int column = 1 + m_layout.m_x_offset_display;
        line_label *label;
@@ -2310,32 +2778,31 @@ layout_printer::print_any_labels (linenum_type row)
                       . ^~~~~~~~~~~~~
                       . this text.  */
                    gcc_assert (get_options ().show_event_links_p);
-                   m_colorizer.set_cfg_edge ();
+                   m_sink.colorize_text_for_cfg_edge ();
                    const cppchar_t right= get_theme ().get_cppchar
                      (text_art::theme::cell_kind::CFG_RIGHT);
                    while (column < label->m_column - 1)
                      {
-                       pp_unicode_character (&m_pp, right);
+                       m_sink.add_character (right);
                        column++;
                      }
                    if (column == label->m_column - 1)
                      {
-                       pp_character (&m_pp, '>');
+                       m_sink.add_character ('>');
                        column++;
                      }
-                   m_colorizer.set_normal_text ();
+                   m_sink.colorize_text_ensure_normal ();
                    m_link_lhs_state = link_lhs_state::none;
                    label_line_with_in_edge = -1;
                  }
                else
                  move_to_column (&column, label->m_column, true);
                gcc_assert (column == label->m_column);
-               /* Colorize the text, unless it's for events in a
-                  diagnostic_path.  */
-               if (!m_is_diagnostic_path)
-                 m_colorizer.set_range (label->m_state_idx);
-               pp_string (&m_pp, label->m_text.m_buffer);
-               m_colorizer.set_normal_text ();
+
+               begin_label (*label);
+               m_sink.add_text (label->m_text.m_buffer);
+               end_label ();
+
                column += label->m_display_width;
                if (get_options ().show_event_links_p && label->m_has_out_edge)
                  {
@@ -2350,13 +2817,13 @@ layout_printer::print_any_labels (linenum_type row)
                      (text_art::theme::cell_kind::CFG_RIGHT);
                    const cppchar_t from_right_to_down= get_theme ().get_cppchar
                      (text_art::theme::cell_kind::CFG_FROM_RIGHT_TO_DOWN);
-                   m_colorizer.set_cfg_edge ();
-                   pp_space (&m_pp);
-                   pp_unicode_character (&m_pp, right);
-                   pp_unicode_character (&m_pp, '>');
-                   pp_unicode_character (&m_pp, right);
-                   pp_unicode_character (&m_pp, from_right_to_down);
-                   m_colorizer.set_normal_text ();
+                   m_sink.colorize_text_for_cfg_edge ();
+                   m_sink.add_space ();
+                   m_sink.add_character (right);
+                   m_sink.add_character ('>');
+                   m_sink.add_character (right);
+                   m_sink.add_character (from_right_to_down);
+                   m_sink.colorize_text_ensure_normal ();
                    column += 5;
                    m_link_rhs_column = column - 1;
                  }
@@ -2365,9 +2832,9 @@ layout_printer::print_any_labels (linenum_type row)
              {
                gcc_assert (column <= label->m_column);
                move_to_column (&column, label->m_column, true);
-               m_colorizer.set_range (label->m_state_idx);
-               pp_character (&m_pp, '|');
-               m_colorizer.set_normal_text ();
+               m_sink.colorize_text_for_range_idx (label->m_state_idx);
+               m_sink.add_character ('|');
+               m_sink.colorize_text_ensure_normal ();
                column++;
              }
          }
@@ -2377,14 +2844,14 @@ layout_printer::print_any_labels (linenum_type row)
        if (m_link_rhs_column != -1 && column < m_link_rhs_column)
          {
            move_to_column (&column, m_link_rhs_column, true);
-           m_colorizer.set_cfg_edge ();
+           m_sink.colorize_text_for_cfg_edge ();
            const cppchar_t down= get_theme ().get_cppchar
              (text_art::theme::cell_kind::CFG_DOWN);
-           pp_unicode_character (&m_pp, down);
-           m_colorizer.set_normal_text ();
+           m_sink.add_character (down);
+           m_sink.colorize_text_ensure_normal ();
          }
 
-       print_newline ();
+       end_line ();
       }
     }
 
@@ -2393,14 +2860,13 @@ layout_printer::print_any_labels (linenum_type row)
   if (m_link_rhs_column != -1)
     {
       int column = 1 + m_layout.m_x_offset_display;
-      start_annotation_line ();
+      start_annotation_line (margin_kind::normal);
       move_to_column (&column, m_link_rhs_column, true);
-      m_colorizer.set_cfg_edge ();
+      m_sink.colorize_text_for_cfg_edge ();
       const cppchar_t down= get_theme ().get_cppchar
        (text_art::theme::cell_kind::CFG_DOWN);
-      pp_unicode_character (&m_pp, down);
-      m_colorizer.set_normal_text ();
-      print_newline ();
+      m_sink.add_character (down);
+      end_line ();
     }
 
   /* Clean up.  */
@@ -2417,8 +2883,9 @@ layout_printer::print_any_labels (linenum_type row)
    They are printed on lines of their own, before the source line
    itself, with a leading '+'.  */
 
+template <typename Sink>
 void
-layout_printer::print_leading_fixits (linenum_type row)
+layout_printer<Sink>::print_leading_fixits (linenum_type row)
 {
   for (unsigned int i = 0; i < m_layout.m_fixit_hints.length (); i++)
     {
@@ -2438,16 +2905,15 @@ layout_printer::print_leading_fixits (linenum_type row)
             and the inserted line with "insert" colorization
             helps them stand out from each other, and from
             the surrounding text.  */
-         m_colorizer.set_normal_text ();
-         start_annotation_line ('+');
-         m_colorizer.set_fixit_insert ();
+         m_sink.colorize_text_ensure_normal ();
+         start_annotation_line (margin_kind::insertion);
+         m_sink.colorize_text_for_fixit_insert ();
          /* Print all but the trailing newline of the fix-it hint.
             We have to print the newline separately to avoid
             getting additional pp prefixes printed.  */
          for (size_t i = 0; i < hint->get_length () - 1; i++)
-           pp_character (&m_pp, hint->get_string ()[i]);
-         m_colorizer.set_normal_text ();
-         pp_newline (&m_pp);
+           m_sink.add_character (hint->get_string ()[i]);
+         end_line ();
        }
     }
 }
@@ -2877,8 +3343,9 @@ line_corrections::add_hint (const fixit_hint *hint)
    Fix-it hints that insert new lines are handled separately,
    in layout::print_leading_fixits.  */
 
+template<typename Sink>
 void
-layout_printer::print_trailing_fixits (linenum_type row)
+layout_printer<Sink>::print_trailing_fixits (linenum_type row)
 {
   /* Build a list of correction instances for the line,
      potentially consolidating hints (for the sake of readability).  */
@@ -2904,7 +3371,7 @@ layout_printer::print_trailing_fixits (linenum_type row)
   int column = 1 + m_layout.m_x_offset_display;
 
   if (!corrections.m_corrections.is_empty ())
-    start_annotation_line ();
+    start_annotation_line (margin_kind::normal);
 
   FOR_EACH_VEC_ELT (corrections.m_corrections, i, c)
     {
@@ -2914,9 +3381,9 @@ layout_printer::print_trailing_fixits (linenum_type row)
          /* This assumes the insertion just affects one line.  */
          int start_column = c->m_printed_columns.start;
          move_to_column (&column, start_column, true);
-         m_colorizer.set_fixit_insert ();
-         pp_string (&m_pp, c->m_text);
-         m_colorizer.set_normal_text ();
+         m_sink.colorize_text_for_fixit_insert ();
+         m_sink.add_text (c->m_text);
+         m_sink.colorize_text_ensure_normal ();
          column += c->m_display_cols;
        }
       else
@@ -2932,10 +3399,10 @@ layout_printer::print_trailing_fixits (linenum_type row)
              || c->m_byte_length == 0)
            {
              move_to_column (&column, start_column, true);
-             m_colorizer.set_fixit_delete ();
+             m_sink.colorize_text_for_fixit_delete ();
              for (; column <= finish_column; column++)
-               pp_character (&m_pp, '-');
-             m_colorizer.set_normal_text ();
+               m_sink.add_character ('-');
+             m_sink.colorize_text_ensure_normal ();
            }
          /* Print the replacement text.  REPLACE also covers
             removals, so only do this extra work (potentially starting
@@ -2943,27 +3410,21 @@ layout_printer::print_trailing_fixits (linenum_type row)
          if (c->m_byte_length > 0)
            {
              move_to_column (&column, start_column, true);
-             m_colorizer.set_fixit_insert ();
-             pp_string (&m_pp, c->m_text);
-             m_colorizer.set_normal_text ();
+             m_sink.colorize_text_for_fixit_insert ();
+             m_sink.add_text (c->m_text);
+             m_sink.colorize_text_ensure_normal ();
              column += c->m_display_cols;
            }
        }
     }
 
+  if (!corrections.m_corrections.is_empty ())
+    m_sink.pop_html_tag ("td");
+
   /* Add a trailing newline, if necessary.  */
   move_to_column (&column, 1 + m_layout.m_x_offset_display, false);
 }
 
-/* Disable any colorization and emit a newline.  */
-
-void
-layout_printer::print_newline ()
-{
-  m_colorizer.set_normal_text ();
-  pp_newline (&m_pp);
-}
-
 /* Return true if (ROW/COLUMN) is within a range of the layout.
    If it returns true, OUT_STATE is written to, with the
    range index, and whether we should draw the caret at
@@ -3060,27 +3521,28 @@ layout::get_x_bound_for_row (linenum_type row, int caret_column,
    and updating *COLUMN.  If ADD_LEFT_MARGIN, then print the (empty)
    left margin after any newline.  */
 
+template<typename Sink>
 void
-layout_printer::move_to_column (int *column,
-                               int dest_column,
-                               bool add_left_margin)
+layout_printer<Sink>::move_to_column (int *column,
+                                     int dest_column,
+                                     bool add_left_margin)
 {
   /* Start a new line if we need to.  */
   if (*column > dest_column)
     {
-      print_newline ();
+      end_line ();
       if (add_left_margin)
-       start_annotation_line ();
+       start_annotation_line (margin_kind::normal);
       *column = 1 + m_layout.m_x_offset_display;
     }
 
   while (*column < dest_column)
     {
       /* For debugging column issues, it can be helpful to replace this
-        pp_space call with
-          pp_character (&m_pp, '0' + (*column % 10));
+        add_space call with
+          m_sink.add_character ('0' + (*column % 10));
         to visualize the changing value of "*column".  */
-      pp_space (&m_pp);
+      m_sink.add_space ();
       (*column)++;
     }
 }
@@ -3088,49 +3550,55 @@ layout_printer::move_to_column (int *column,
 /* For debugging layout issues, render a ruler giving column numbers
    (after the 1-column indent).  */
 
+template<typename Sink>
 void
-layout_printer::show_ruler (int max_column)
+layout_printer<Sink>::show_ruler (int max_column)
 {
+  m_sink.push_html_tag_with_class("thead", "ruler", false);
+
   /* Hundreds.  */
   if (max_column > 99)
     {
-      start_annotation_line ();
+      start_annotation_line (margin_kind::ruler);
       for (int column = 1 + m_layout.m_x_offset_display;
           column <= max_column;
           ++column)
        if (column % 10 == 0)
-         pp_character (&m_pp, '0' + (column / 100) % 10);
+         m_sink.add_character ('0' + (column / 100) % 10);
        else
-         pp_space (&m_pp);
-      pp_newline (&m_pp);
+         m_sink.add_space ();
+      end_line ();
     }
 
   /* Tens.  */
-  start_annotation_line ();
+  start_annotation_line (margin_kind::ruler);
   for (int column = 1 + m_layout.m_x_offset_display;
        column <= max_column;
        ++column)
     if (column % 10 == 0)
-      pp_character (&m_pp, '0' + (column / 10) % 10);
+      m_sink.add_character ('0' + (column / 10) % 10);
     else
-      pp_space (&m_pp);
-  pp_newline (&m_pp);
+      m_sink.add_space ();
+  end_line ();
 
   /* Units.  */
-  start_annotation_line ();
+  start_annotation_line (margin_kind::ruler);
   for (int column = 1 + m_layout.m_x_offset_display;
        column <= max_column;
        ++column)
-    pp_character (&m_pp, '0' + (column % 10));
-  pp_newline (&m_pp);
+    m_sink.add_character ('0' + (column % 10));
+  end_line ();
+
+  m_sink.pop_html_tag("thead"); // thead
 }
 
 /* Print leading fix-its (for new lines inserted before the source line)
    then the source line, followed by an annotation line
    consisting of any caret/underlines, then any fixits.
    If the source line can't be read, print nothing.  */
+template<typename Sink>
 void
-layout_printer::print_line (linenum_type row)
+layout_printer<Sink>::print_line (linenum_type row)
 {
   char_span line
     = m_layout.m_file_cache.get_source_line (m_layout.m_exploc.file, row);
@@ -3154,8 +3622,9 @@ layout_printer::print_line (linenum_type row)
    showing the link entering at the top right and emerging
    at the bottom left.  */
 
+template<typename Sink>
 void
-layout_printer::print_any_right_to_left_edge_lines ()
+layout_printer<Sink>::print_any_right_to_left_edge_lines ()
 {
   if (m_link_rhs_column == -1)
     /* Can also happen if the out-edge had UNKNOWN_LOCATION.  */
@@ -3164,31 +3633,30 @@ layout_printer::print_any_right_to_left_edge_lines ()
   gcc_assert (get_options ().show_event_links_p);
 
   /* Print the line with "|".  */
-  start_annotation_line ();
+  start_annotation_line (margin_kind::normal);
+
   int column = 1 + m_layout.m_x_offset_display;
   move_to_column (&column, m_link_rhs_column, true);
-  m_colorizer.set_cfg_edge ();
+  m_sink.colorize_text_for_cfg_edge ();
   const cppchar_t down= get_theme ().get_cppchar
     (text_art::theme::cell_kind::CFG_DOWN);
-  pp_unicode_character (&m_pp, down);
-  m_colorizer.set_normal_text ();
-  pp_newline (&m_pp);
+  m_sink.add_character (down);
+  end_line ();
 
   /* Print the line with "┌──────────────────────────────────────────┘".  */
   m_link_lhs_state = link_lhs_state::rewinding_to_lhs;
-  start_annotation_line ();
-  m_colorizer.set_cfg_edge ();
+  start_annotation_line (margin_kind::normal);
+  m_sink.colorize_text_for_cfg_edge ();
   const cppchar_t left= get_theme ().get_cppchar
     (text_art::theme::cell_kind::CFG_LEFT);
   for (int column = 1 + m_layout.m_x_offset_display;
        column < m_link_rhs_column;
        ++column)
-    pp_unicode_character (&m_pp, left);
+    m_sink.add_character (left);
   const cppchar_t from_down_to_left = get_theme ().get_cppchar
     (text_art::theme::cell_kind::CFG_FROM_DOWN_TO_LEFT);
-  pp_unicode_character (&m_pp, from_down_to_left);
-  m_colorizer.set_normal_text ();
-  pp_newline (&m_pp);
+  m_sink.add_character (from_down_to_left);
+  end_line ();
 
   /* We now have a link line on the LHS,
      and no longer have one on the RHS.  */
@@ -3196,14 +3664,13 @@ layout_printer::print_any_right_to_left_edge_lines ()
   m_link_rhs_column = -1;
 }
 
-layout_printer::layout_printer (pretty_printer &pp,
-                               const layout &layout,
-                               const rich_location &richloc,
-                               diagnostic_t diagnostic_kind)
-: m_pp (pp),
+template<typename Sink>
+layout_printer<Sink>::layout_printer (Sink &sink,
+                                     const layout &layout,
+                                     bool is_diagnostic_path)
+: m_sink (sink),
   m_layout (layout),
-  m_colorizer (m_pp, richloc, diagnostic_kind),
-  m_is_diagnostic_path (diagnostic_kind == DK_DIAGNOSTIC_PATH),
+  m_is_diagnostic_path (is_diagnostic_path),
   m_link_lhs_state (link_lhs_state::none),
   m_link_rhs_column (-1)
 {
@@ -3293,11 +3760,47 @@ diagnostic_context::maybe_show_locus (const rich_location &richloc,
   source_policy.print (pp, richloc, diagnostic_kind, effects);
 }
 
+/* As above, but print in HTML form to XP.
+   If non-null, use LABEL_WRITER when writing labelled ranges.  */
+
+void
+diagnostic_context::maybe_show_locus_as_html (const rich_location &richloc,
+                                             const diagnostic_source_printing_options &opts,
+                                             diagnostic_t diagnostic_kind,
+                                             xml::printer &xp,
+                                             diagnostic_source_effect_info *effects,
+                                             html_label_writer *label_writer)
+{
+  const location_t loc = richloc.get_loc ();
+  /* Do nothing if source-printing has been disabled.  */
+  if (!opts.enabled)
+    return;
+
+  /* Don't attempt to print source for UNKNOWN_LOCATION and for builtins.  */
+  if (loc <= BUILTINS_LOCATION)
+    return;
+
+  /* Don't print the same source location twice in a row, unless we have
+     fix-it hints, or multiple locations, or a label.  */
+  if (loc == m_last_location
+      && richloc.get_num_fixit_hints () == 0
+      && richloc.get_num_locations () == 1
+      && richloc.get_range (0)->m_label == NULL)
+    return;
+
+  m_last_location = loc;
+
+  diagnostic_source_print_policy source_policy (*this, opts);
+  source_policy.print_as_html (xp, richloc, diagnostic_kind, effects,
+                              label_writer);
+}
+
 diagnostic_source_print_policy::
 diagnostic_source_print_policy (const diagnostic_context &dc)
 : m_options (dc.m_source_printing),
   m_location_policy (dc),
-  m_start_span_cb (dc.m_text_callbacks.m_start_span),
+  m_text_start_span_cb (dc.m_text_callbacks.m_text_start_span),
+  m_html_start_span_cb (dc.m_text_callbacks.m_html_start_span),
   m_file_cache (dc.get_file_cache ()),
   m_diagram_theme (dc.get_diagram_theme ()),
   m_escape_format (dc.get_escape_format ())
@@ -3309,7 +3812,8 @@ diagnostic_source_print_policy (const diagnostic_context &dc,
                                const diagnostic_source_printing_options &opts)
 : m_options (opts),
   m_location_policy (dc),
-  m_start_span_cb (dc.m_text_callbacks.m_start_span),
+  m_text_start_span_cb (dc.m_text_callbacks.m_text_start_span),
+  m_html_start_span_cb (dc.m_text_callbacks.m_html_start_span),
   m_file_cache (dc.get_file_cache ()),
   m_diagram_theme (dc.get_diagram_theme ()),
   m_escape_format (dc.get_escape_format ())
@@ -3329,15 +3833,36 @@ diagnostic_source_print_policy::print (pretty_printer &pp,
   const
 {
   layout layout (*this, richloc, effects);
-  layout_printer lp (pp, layout, richloc, diagnostic_kind);
+  colorizer col (pp, richloc, diagnostic_kind);
+  to_text sink (pp, col);
+  layout_printer<to_text> lp (sink, layout,
+                             diagnostic_kind == DK_DIAGNOSTIC_PATH);
   lp.print (*this);
 }
 
+/* As above, but print in HTML form to XP.
+   If non-null, use LABEL_WRITER when writing labelled ranges.  */
+
 void
-layout_printer::print (const diagnostic_source_print_policy &source_policy)
+diagnostic_source_print_policy::print_as_html (xml::printer &xp,
+                                              const rich_location &richloc,
+                                              diagnostic_t diagnostic_kind,
+                                              diagnostic_source_effect_info *effects,
+                                              html_label_writer *label_writer)
+  const
 {
-  diagnostic_prefixing_rule_t saved_rule = pp_prefixing_rule (&m_pp);
-  pp_prefixing_rule (&m_pp) = DIAGNOSTICS_SHOW_PREFIX_EVERY_LINE;
+  layout layout (*this, richloc, effects);
+  to_html sink (xp, label_writer);
+  layout_printer<to_html> lp (sink, layout,
+                             diagnostic_kind == DK_DIAGNOSTIC_PATH);
+  lp.print (*this);
+}
+
+template <typename Sink>
+void
+layout_printer<Sink>::print (const diagnostic_source_print_policy &source_policy)
+{
+  m_sink.push_html_tag_with_class ("table", "locus", false);
 
   if (get_options ().show_ruler_p)
     show_ruler (m_layout.m_x_offset_display + get_options ().max_width);
@@ -3362,27 +3887,65 @@ layout_printer::print (const diagnostic_source_print_policy &source_policy)
                = m_layout.get_expanded_location (line_span);
              const diagnostic_location_print_policy &
                loc_policy = source_policy.get_location_policy ();
-             source_policy.get_start_span_fn () (loc_policy, &m_pp, exploc);
+             m_sink.invoke_start_span_fn (source_policy, loc_policy, exploc);
            }
        }
+
+      m_sink.push_html_tag_with_class ("tbody", "line-span", false);
+
       /* Iterate over the lines within this span (using linenum_arith_t to
         avoid overflow with 0xffffffff causing an infinite loop).  */
       linenum_arith_t last_line = line_span->get_last_line ();
       for (linenum_arith_t row = line_span->get_first_line ();
           row <= last_line; row++)
        print_line (row);
+
+      m_sink.pop_html_tag ("tbody");
     }
 
   if (auto effect_info = m_layout.m_effect_info)
     effect_info->m_trailing_out_edge_column = m_link_rhs_column;
 
-  pp_prefixing_rule (&m_pp) = saved_rule;
+  m_sink.pop_html_tag ("table");
 }
 
 #if CHECKING_P
 
 namespace selftest {
 
+static std::unique_ptr<xml::node>
+make_element_for_locus (const rich_location &rich_loc,
+                       diagnostic_t kind,
+                       diagnostic_context &dc)
+{
+  dc.m_last_location = UNKNOWN_LOCATION;
+
+  xml::element wrapper ("wrapper", false);
+  xml::printer xp (wrapper);
+  dc.maybe_show_locus_as_html (rich_loc,
+                              dc.m_source_printing,
+                              kind,
+                              xp,
+                              nullptr,
+                              nullptr); // label_writer
+  if (wrapper.m_children.size () > 0)
+    return std::move (wrapper.m_children[0]);
+  else
+    return nullptr;
+}
+
+static label_text
+make_raw_html_for_locus (const rich_location &rich_loc,
+                        diagnostic_t kind,
+                        diagnostic_context &dc)
+{
+  auto node = make_element_for_locus (rich_loc, kind, dc);
+  pretty_printer pp;
+  if (node)
+    node->write_as_xml (&pp, 0, true);
+  return label_text::take (xstrdup (pp_formatted_text (&pp)));
+}
+
 /* Selftests for diagnostic_show_locus.  */
 
 diagnostic_show_locus_fixture::
@@ -3600,7 +4163,10 @@ test_layout_x_offset_display_utf8 (const line_table_case &case_)
                           linemap_position_for_column (line_table,
                                                        emoji_col));
     layout test_layout (policy, richloc, nullptr);
-    layout_printer lp (*dc.get_reference_printer (), test_layout, richloc, DK_ERROR);
+    colorizer col (*dc.get_reference_printer (),
+                  richloc, DK_ERROR);
+    to_text sink (*dc.get_reference_printer (), col);
+    layout_printer<to_text> lp (sink, test_layout, false);
     lp.print (policy);
     ASSERT_STREQ ("     |         1         \n"
                  "     |         1         \n"
@@ -3627,7 +4193,10 @@ test_layout_x_offset_display_utf8 (const line_table_case &case_)
                           linemap_position_for_column (line_table,
                                                        emoji_col + 2));
     layout test_layout (dc, richloc, nullptr);
-    layout_printer lp (*dc.get_reference_printer (), test_layout, richloc, DK_ERROR);
+    colorizer col (*dc.get_reference_printer (),
+                  richloc, DK_ERROR);
+    to_text sink (*dc.get_reference_printer (), col);
+    layout_printer<to_text> lp (sink, test_layout, false);
     lp.print (policy);
     ASSERT_STREQ ("     |        1         1 \n"
                  "     |        1         2 \n"
@@ -3708,7 +4277,10 @@ test_layout_x_offset_display_tab (const line_table_case &case_)
       dc.m_tabstop = tabstop;
       diagnostic_source_print_policy policy (dc);
       layout test_layout (policy, richloc, nullptr);
-      layout_printer lp (*dc.get_reference_printer (), test_layout, richloc, DK_ERROR);
+      colorizer col (*dc.get_reference_printer (),
+                    richloc, DK_ERROR);
+      to_text sink (*dc.get_reference_printer (), col);
+      layout_printer<to_text> lp (sink, test_layout, false);
       lp.print (policy);
       const char *out = pp_formatted_text (dc.get_reference_printer ());
       ASSERT_EQ (NULL, strchr (out, '\t'));
@@ -3733,7 +4305,10 @@ test_layout_x_offset_display_tab (const line_table_case &case_)
       dc.m_source_printing.show_line_numbers_p = true;
       diagnostic_source_print_policy policy (dc);
       layout test_layout (policy, richloc, nullptr);
-      layout_printer lp (*dc.get_reference_printer (), test_layout, richloc, DK_ERROR);
+      colorizer col (*dc.get_reference_printer (),
+                    richloc, DK_ERROR);
+      to_text sink (*dc.get_reference_printer (), col);
+      layout_printer<to_text> lp (sink, test_layout, false);
       lp.print (policy);
 
       /* We have arranged things so that two columns will be printed before
@@ -3814,6 +4389,32 @@ test_one_liner_caret_and_range ()
   ASSERT_STREQ (" foo = bar.field;\n"
                "       ~~~^~~~~~\n",
                dc.test_show_locus (richloc));
+
+  {
+    test_diagnostic_context dc;
+    auto out = make_raw_html_for_locus (richloc, DK_ERROR, dc);
+     ASSERT_STREQ
+       ("<table class=\"locus\">\n"
+       "  <tbody class=\"line-span\">\n"
+       "    <tr><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n"
+       "    <tr><td class=\"left-margin\"> </td><td class=\"annotation\">      ~~~^~~~~~</td></tr>\n"
+       "  </tbody>\n"
+       "</table>\n",
+       out.get ());
+  }
+  {
+    test_diagnostic_context dc;
+    dc.m_source_printing.show_line_numbers_p = true;
+    auto out = make_raw_html_for_locus (richloc, DK_ERROR, dc);
+     ASSERT_STREQ
+       ("<table class=\"locus\">\n"
+       "  <tbody class=\"line-span\">\n"
+       "    <tr><td class=\"linenum\">    1</td><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n"
+       "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">      ~~~^~~~~~</td></tr>\n"
+       "  </tbody>\n"
+       "</table>\n",
+       out.get ());
+  }
 }
 
 /* Multiple ranges and carets.  */
@@ -4231,6 +4832,24 @@ test_one_liner_labels ()
                  " |     label 1\n"
                  " label 0\n",
                  dc.test_show_locus (richloc));
+
+    {
+      test_diagnostic_context dc;
+      dc.m_source_printing.show_line_numbers_p = true;
+      auto out = make_raw_html_for_locus (richloc, DK_ERROR, dc);
+      ASSERT_STREQ
+       ("<table class=\"locus\">\n"
+        "  <tbody class=\"line-span\">\n"
+        "    <tr><td class=\"linenum\">    1</td><td class=\"left-margin\"> </td><td class=\"source\">foo = bar.field;</td></tr>\n"
+        "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">^~~   ~~~ ~~~~~</td></tr>\n"
+        "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">|     |   |</td></tr>\n"
+        "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">|     |   label 2</td></tr>\n"
+        "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">|     label 1</td></tr>\n"
+        "    <tr><td class=\"linenum\">     </td><td class=\"left-margin\"> </td><td class=\"annotation\">label 0</td></tr>\n"
+        "  </tbody>\n"
+        "</table>\n",
+        out.get ());
+    }
   }
 
   /* Example of boundary conditions: label 0 and 1 have just enough clearance,
index b43fc907ed131c9065302813375d71b8ebf538ac..20582b1fd5ee972f639624e6652f934ff003cd17 100644 (file)
@@ -250,7 +250,10 @@ diagnostic_context::initialize (int n_opts)
   m_internal_error = nullptr;
   m_adjust_diagnostic_info = nullptr;
   m_text_callbacks.m_begin_diagnostic = default_diagnostic_text_starter;
-  m_text_callbacks.m_start_span = default_diagnostic_start_span_fn;
+  m_text_callbacks.m_text_start_span
+    = default_diagnostic_start_span_fn<to_text>;
+  m_text_callbacks.m_html_start_span
+    = default_diagnostic_start_span_fn<to_html>;
   m_text_callbacks.m_end_diagnostic = default_diagnostic_text_finalizer;
   m_option_mgr = nullptr;
   m_urlifier_stack = new auto_vec<urlifier_stack_node> ();
@@ -1071,21 +1074,6 @@ logical_location_manager::function_p (key k) const
     }
 }
 
-void
-default_diagnostic_start_span_fn (const diagnostic_location_print_policy &loc_policy,
-                                 pretty_printer *pp,
-                                 expanded_location exploc)
-{
-  const diagnostic_column_policy &column_policy
-    = loc_policy.get_column_policy ();
-  label_text text
-    = column_policy.get_location_text (exploc,
-                                      loc_policy.show_column_p (),
-                                      pp_show_color (pp));
-  pp_string (pp, text.get ());
-  pp_newline (pp);
-}
-
 /* Interface to specify diagnostic kind overrides.  Returns the
    previous setting, or DK_UNSPECIFIED if the parameters are out of
    range.  If OPTION_ID is zero, the new setting is for all the
index 00d7812248a4ac028cfbe8c57270ae8dd80a077d..cdd6f26ba2a311c8a2bc517d6cffcf68fb700d9b 100644 (file)
@@ -31,6 +31,11 @@ namespace text_art
   class theme;
 } // namespace text_art
 
+namespace xml
+{
+  class printer;
+} // namespace xml
+
 /* An enum for controlling what units to use for the column number
    when diagnostics are output, used by the -fdiagnostics-column-unit option.
    Tabs will be expanded or not according to the value of -ftabstop.  The origin
@@ -177,10 +182,15 @@ class diagnostic_source_print_policy;
 typedef void (*diagnostic_text_starter_fn) (diagnostic_text_output_format &,
                                            const diagnostic_info *);
 
-typedef void
-(*diagnostic_start_span_fn) (const diagnostic_location_print_policy &,
-                            pretty_printer *,
-                            expanded_location);
+struct to_text;
+struct to_html;
+
+extern pretty_printer *get_printer (to_text &);
+
+template <typename Sink>
+using diagnostic_start_span_fn = void (*) (const diagnostic_location_print_policy &,
+                                          Sink &sink,
+                                          expanded_location);
 
 typedef void (*diagnostic_text_finalizer_fn) (diagnostic_text_output_format &,
                                              const diagnostic_info *,
@@ -389,11 +399,32 @@ public:
   const diagnostic_column_policy &
   get_column_policy () const { return m_column_policy; }
 
+  void
+  print_text_span_start (const diagnostic_context &dc,
+                        pretty_printer &pp,
+                        const expanded_location &exploc);
+
+  void
+  print_html_span_start (const diagnostic_context &dc,
+                        xml::printer &xp,
+                        const expanded_location &exploc);
+
 private:
   diagnostic_column_policy m_column_policy;
   bool m_show_column;
 };
 
+/* Abstract base class for optionally supplying extra tags when writing
+   out annotation labels in HTML output.  */
+
+class html_label_writer
+{
+public:
+  virtual ~html_label_writer () {}
+  virtual void begin_label () = 0;
+  virtual void end_label () = 0;
+};
+
 /* A bundle of state for printing source within a diagnostic,
    to isolate the interactions between diagnostic_context and the
    implementation of diagnostic_show_locus.  */
@@ -411,11 +442,21 @@ public:
         diagnostic_t diagnostic_kind,
         diagnostic_source_effect_info *effect_info) const;
 
+  void
+  print_as_html (xml::printer &xp,
+                const rich_location &richloc,
+                diagnostic_t diagnostic_kind,
+                diagnostic_source_effect_info *effect_info,
+                html_label_writer *label_writer) const;
+
   const diagnostic_source_printing_options &
   get_options () const { return m_options; }
 
-  diagnostic_start_span_fn
-  get_start_span_fn () const { return m_start_span_cb; }
+  diagnostic_start_span_fn<to_text>
+  get_text_start_span_fn () const { return m_text_start_span_cb; }
+
+  diagnostic_start_span_fn<to_html>
+  get_html_start_span_fn () const { return m_html_start_span_cb; }
 
   file_cache &
   get_file_cache () const { return m_file_cache; }
@@ -442,7 +483,8 @@ public:
 private:
   const diagnostic_source_printing_options &m_options;
   class diagnostic_location_print_policy m_location_policy;
-  diagnostic_start_span_fn m_start_span_cb;
+  diagnostic_start_span_fn<to_text> m_text_start_span_cb;
+  diagnostic_start_span_fn<to_html> m_html_start_span_cb;
   file_cache &m_file_cache;
 
   /* Other data copied from diagnostic_context.  */
@@ -508,7 +550,7 @@ public:
   /* Give access to m_text_callbacks.  */
   friend diagnostic_text_starter_fn &
   diagnostic_text_starter (diagnostic_context *context);
-  friend diagnostic_start_span_fn &
+  friend diagnostic_start_span_fn<to_text> &
   diagnostic_start_span (diagnostic_context *context);
   friend diagnostic_text_finalizer_fn &
   diagnostic_text_finalizer (diagnostic_context *context);
@@ -602,6 +644,12 @@ public:
                         diagnostic_t diagnostic_kind,
                         pretty_printer &pp,
                         diagnostic_source_effect_info *effect_info);
+  void maybe_show_locus_as_html (const rich_location &richloc,
+                                const diagnostic_source_printing_options &opts,
+                                diagnostic_t diagnostic_kind,
+                                xml::printer &xp,
+                                diagnostic_source_effect_info *effect_info,
+                                html_label_writer *label_writer);
 
   void emit_diagram (const diagnostic_diagram &diagram);
 
@@ -882,7 +930,8 @@ private:
     /* This function is called by diagnostic_show_locus in between
        disjoint spans of source code, so that the context can print
        something to indicate that a new span of source code has begun.  */
-    diagnostic_start_span_fn m_start_span;
+    diagnostic_start_span_fn<to_text> m_text_start_span;
+    diagnostic_start_span_fn<to_html> m_html_start_span;
 
     /* This function is called after the diagnostic message is printed.  */
     diagnostic_text_finalizer_fn m_end_diagnostic;
@@ -1040,10 +1089,10 @@ diagnostic_text_starter (diagnostic_context *context)
 /* Client supplied function called between disjoint spans of source code,
    so that the context can print
    something to indicate that a new span of source code has begun.  */
-inline diagnostic_start_span_fn &
+inline diagnostic_start_span_fn<to_text> &
 diagnostic_start_span (diagnostic_context *context)
 {
-  return context->m_text_callbacks.m_start_span;
+  return context->m_text_callbacks.m_text_start_span;
 }
 
 /* Client supplied function called after a diagnostic message is
@@ -1128,6 +1177,21 @@ diagnostic_show_locus (diagnostic_context *context,
   context->maybe_show_locus (*richloc, opts, diagnostic_kind, *pp, effect_info);
 }
 
+inline void
+diagnostic_show_locus_as_html (diagnostic_context *context,
+                              const diagnostic_source_printing_options &opts,
+                              rich_location *richloc,
+                              diagnostic_t diagnostic_kind,
+                              xml::printer &xp,
+                              diagnostic_source_effect_info *effect_info = nullptr,
+                              html_label_writer *label_writer = nullptr)
+{
+  gcc_assert (context);
+  gcc_assert (richloc);
+  context->maybe_show_locus_as_html (*richloc, opts, diagnostic_kind, xp,
+                                    effect_info, label_writer);
+}
+
 /* Because we read source files a second time after the frontend did it the
    first time, we need to know how the frontend handled things like character
    set conversion and UTF-8 BOM stripping, in order to make everything
@@ -1201,8 +1265,9 @@ extern void diagnostic_set_info_translated (diagnostic_info *, const char *,
 #endif
 void default_diagnostic_text_starter (diagnostic_text_output_format &,
                                      const diagnostic_info *);
+template <typename Sink>
 void default_diagnostic_start_span_fn (const diagnostic_location_print_policy &,
-                                      pretty_printer *,
+                                      Sink &sink,
                                       expanded_location);
 void default_diagnostic_text_finalizer (diagnostic_text_output_format &,
                                        const diagnostic_info *,
index e3bc833c59bd865677ae90f6d29d412bcf1fad5b..0150ad088798090184757b01cb6d6983a3719402 100644 (file)
@@ -6119,18 +6119,25 @@ in this release.
 
 @item experimental-html
 Emit diagnostics to a file in HTML format.  This scheme is experimental,
-and may go away in future GCC releases.  The details of the output are
-also subject to change.
+and may go away in future GCC releases.  The keys and details of the output
+are also subject to change.
 
 Supported keys are:
 
 @table @gcctabopt
 
+@item css=@r{[}yes@r{|}no@r{]}
+Add an embedded <style> to the generated HTML.  Defaults to yes.
+
 @item file=@var{FILENAME}
 Specify the filename to write the HTML output to, potentially with a
 leading absolute or relative path.  If not specified, it defaults to
 @file{@var{source}.html}.
 
+@item javascript=@r{[}yes@r{|}no@r{]}
+Add an embedded <script> to the generated HTML providing a barebones UI
+for viewing results.  Defaults to yes.
+
 @end table
 
 @end table
index f89d41d74633d591ed705ac56deca6a5c38fab2a..004a4b26c2f42abf9673293ad029c3d0d90bdd43 100644 (file)
@@ -618,9 +618,10 @@ gfc_diagnostic_text_starter (diagnostic_text_output_format &text_output,
 
 static void
 gfc_diagnostic_start_span (const diagnostic_location_print_policy &loc_policy,
-                          pretty_printer *pp,
+                          to_text &sink,
                           expanded_location exploc)
 {
+  pretty_printer *pp = get_printer (sink);
   const bool colorize = pp_show_color (pp);
   char *locus_prefix
     = gfc_diagnostic_build_locus_prefix (loc_policy, exploc, colorize);
index 34c390695b9560b4e2608672c1ca797aee3df3d9..3a1dc528ac568a719d768d05acf42a5368f06cd1 100644 (file)
@@ -545,21 +545,40 @@ html_scheme_handler::make_sink (const context &ctxt,
                                const char *unparsed_arg,
                                const scheme_name_and_params &parsed_arg) const
 {
+  bool css = true;
   label_text filename;
+  bool javascript = true;
   for (auto& iter : parsed_arg.m_kvs)
     {
       const std::string &key = iter.first;
       const std::string &value = iter.second;
+      if (key == "css")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                css))
+           return nullptr;
+         continue;
+       }
       if (key == "file")
        {
          filename = label_text::take (xstrdup (value.c_str ()));
          continue;
        }
+      if (key == "javascript")
+       {
+         if (!parse_bool_value (ctxt, unparsed_arg, key, value,
+                                javascript))
+           return nullptr;
+         continue;
+       }
 
       /* Key not found.  */
       auto_vec<const char *> known_keys;
+      known_keys.safe_push ("css");
       known_keys.safe_push ("file");
-      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (), known_keys);
+      known_keys.safe_push ("javascript");
+      ctxt.report_unknown_key (unparsed_arg, key, get_scheme_name (),
+                              known_keys);
       return nullptr;
     }
 
@@ -579,8 +598,13 @@ html_scheme_handler::make_sink (const context &ctxt,
   if (!output_file)
     return nullptr;
 
+  html_generation_options html_gen_opts;
+  html_gen_opts.m_css = css;
+  html_gen_opts.m_javascript = javascript;
+
   auto sink = make_html_sink (ctxt.m_dc,
                              *line_table,
+                             html_gen_opts,
                              std::move (output_file));
   return sink;
 }
index 1a10807243ec1baa7d34b6dfcf493ddd97396c6b..eeee2eb8fe317b90048ba555fcb1dd8aa8dd750d 100644 (file)
@@ -58,11 +58,11 @@ test_diagnostic_context::~test_diagnostic_context ()
 void
 test_diagnostic_context::
 start_span_cb (const diagnostic_location_print_policy &loc_policy,
-              pretty_printer *pp,
+              to_text &sink,
               expanded_location exploc)
 {
   exploc.file = "FILENAME";
-  default_diagnostic_start_span_fn (loc_policy, pp, exploc);
+  default_diagnostic_start_span_fn<to_text> (loc_policy, sink, exploc);
 }
 
 bool
index c8f67a0398503e17dd532780787c8ea2d87fa80f..4a4331016e2ba8c7fb4c281603da39a0e7b8ff93 100644 (file)
@@ -40,7 +40,7 @@ class test_diagnostic_context : public diagnostic_context
      real filename (to avoid printing the names of tempfiles).  */
   static void
   start_span_cb (const diagnostic_location_print_policy &,
-                pretty_printer *,
+                to_text &sink,
                 expanded_location exploc);
 
   /* Report a diagnostic to this context.  For a selftest, this
index c211f4f76e714ad42bde494ce9ee7aeb864eba59..fe5f081dce49088d8d279f9b398e04446bae6677 100644 (file)
@@ -1,5 +1,5 @@
 /* { dg-do compile } */
-/* { dg-options "-fdiagnostics-add-output=experimental-html" } */
+/* { dg-options "-fdiagnostics-add-output=experimental-html:javascript=no" } */
 
 /* Verify that basics of HTML output work.  */
 
index 8ac1f142388cb6e8724352f256381af2cc7a1d95..02ff84c14bb6c2ae453bef66c156284991028c36 100644 (file)
@@ -18,12 +18,6 @@ import pytest
 def html_tree():
     return html_tree_from_env()
 
-XHTML = 'http://www.w3.org/1999/xhtml'
-ns = {'xhtml': XHTML}
-
-def make_tag(local_name):
-    return f'{{{XHTML}}}' + local_name
-
 def test_basics(html_tree):
     root = html_tree.getroot ()
     assert root.tag == make_tag('html')
index 2499e8d288ab589c01c3914e04704c37c02365a3..df57b256d9881a302d636bd5c2d472a483c9601e 100644 (file)
@@ -1,6 +1,6 @@
 /* { dg-do compile } */
-/* { dg-options "-fdiagnostics-set-output=experimental-html" } */
-/* { dg-additional-options "-fdiagnostics-show-caret" } */
+/* { dg-options "-fdiagnostics-set-output=experimental-html:javascript=no" } */
+/* { dg-additional-options "-fdiagnostics-show-caret -fdiagnostics-show-line-numbers" } */
 
 extern char *gets (char *s);
 
index e475e95058b7f6a2d8aaf35d600b65a769bcf5bf..b4c75b230ee2112cd642fe1e92724460fec1af17 100644 (file)
@@ -8,12 +8,6 @@ import pytest
 def html_tree():
     return html_tree_from_env()
 
-XHTML = 'http://www.w3.org/1999/xhtml'
-ns = {'xhtml': XHTML}
-
-def make_tag(local_name):
-    return f'{{{XHTML}}}' + local_name
-
 def test_metadata(html_tree):
     root = html_tree.getroot ()
     assert root.tag == make_tag('html')
@@ -48,11 +42,21 @@ def test_metadata(html_tree):
     assert metadata[1][0].text == 'STR34-C'
     assert metadata[1][0].tail == ']'
 
-    src = diag.find('xhtml:pre', ns)
-    assert src.attrib['class'] == 'gcc-annotated-source'
-    assert src.text == (
-        '   gets (buf);\n'
-        '   ^~~~~~~~~~\n')
+    src = diag.find('xhtml:table', ns)
+    assert src.attrib['class'] == 'locus'
+
+    tbody = src.find('xhtml:tbody', ns)
+    assert tbody.attrib['class'] == 'line-span'
+
+    rows = tbody.findall('xhtml:tr', ns)
+
+    quoted_src_tr = rows[0]
+    assert_quoted_line(quoted_src_tr,
+                       '   10', '  gets (buf);')
+    
+    annotation_tr = rows[1]
+    assert_annotation_line(annotation_tr,
+                           '  ^~~~~~~~~~')
 
 # For reference, here's the generated HTML:
 """
@@ -60,8 +64,13 @@ def test_metadata(html_tree):
     <div class="gcc-diagnostic-list">
       <div class="gcc-diagnostic">
         <span class="gcc-message">never use &apos;<span class="gcc-quoted-text">gets</span>&apos;</span> 
-        <span class="gcc-metadata"><span class="gcc-metadata-item">[<a href="https://cwe.mitre.org/data/definitions/242.html">CWE-242</a>]</span><span class="gcc-metadata-item">[<a href="https://example.com/">STR34-C</a>]</span></span>
-        ...etc...
+        <span class="gcc-metadata"><span class="gcc-metadata-item">[<a href="https://cwe.mitre.org/data/definitions/242.html">CWE-242</a>]</span><span class="gcc-metadata-item">[<a href="https://example.com/">STR34-C</a>]</span></span><table class="locus">
+<tbody class="line-span">
+<tr><td class="linenum">   10</td> <td class="source">  gets (buf);</td></tr>
+<tr><td class="linenum"/><td class="annotation">  ^~~~~~~~~~</td></tr>
+</tbody>
+</table>
+
       </div>
     </div>
   </body>
index 26605f7607d9b5bc265633e53f3856cc00bb3391..dab9c38f1bfdc7ba99092a05caaffc21ea17eb47 100644 (file)
@@ -1,5 +1,5 @@
 /* { dg-do compile } */
-/* { dg-options "-fdiagnostics-show-caret -fdiagnostics-show-line-numbers -fdiagnostics-path-format=inline-events -fdiagnostics-add-output=experimental-html" } */
+/* { dg-options "-fdiagnostics-show-caret -fdiagnostics-show-line-numbers -fdiagnostics-path-format=inline-events -fdiagnostics-add-output=experimental-html:javascript=no" } */
 
 #include <stddef.h>
 #include <stdlib.h>
index c212e4906bb0e99fedc123146a255d49989caf10..59bee247103f432203cae71816b3e51b9340bc5e 100644 (file)
@@ -8,12 +8,6 @@ import pytest
 def html_tree():
     return html_tree_from_env()
 
-XHTML = 'http://www.w3.org/1999/xhtml'
-ns = {'xhtml': XHTML}
-
-def make_tag(local_name):
-    return f'{{{XHTML}}}' + local_name
-
 def test_paths(html_tree):
     root = html_tree.getroot ()
     assert root.tag == make_tag('html')
@@ -29,7 +23,19 @@ def test_paths(html_tree):
     assert diag is not None
     assert diag.attrib['class'] == 'gcc-diagnostic'
 
-    pre = diag.findall('xhtml:pre', ns)
-    assert pre[0].attrib['class'] == 'gcc-annotated-source'
-    assert pre[1].attrib['class'] == 'gcc-execution-path'
-    assert pre[1].text.startswith("  'make_a_list_of_random_ints_badly': events 1-3")
+    event_ranges = diag.find('xhtml:div', ns)
+    assert_class(event_ranges, 'event-ranges')
+
+    frame_margin = event_ranges.find('xhtml:table', ns)
+    assert_class(frame_margin, 'stack-frame-with-margin')
+
+    tr = frame_margin.find('xhtml:tr', ns)
+    assert tr is not None
+    tds = tr.findall('xhtml:td', ns)
+    assert len(tds) == 2
+
+    assert_class(tds[0], 'interprocmargin')
+
+    test_frame = tds[1]
+    assert_frame(test_frame, 'make_a_list_of_random_ints_badly')
+    assert_event_range_with_margin(test_frame[1])
index 847b6d423e40f032449e8ccbb91fe4deb09fb8e5..7eb0c506679fce56c240d9fe07b1ef9f0967d52c 100644 (file)
@@ -1,5 +1,5 @@
 /* { dg-do compile } */
-/* { dg-options "-fdiagnostics-path-format=inline-events -fdiagnostics-show-caret -fdiagnostics-show-line-numbers" } */
+/* { dg-options "-fdiagnostics-path-format=inline-events -fdiagnostics-show-caret -fdiagnostics-show-line-numbers -fdiagnostics-add-output=experimental-html:javascript=no" } */
 /* { dg-enable-nn-line-numbers "" } */
 
 #include <stdio.h>
@@ -82,3 +82,7 @@ void test (void)
                   |      |   (9) calling 'fprintf'
                   |
    { dg-end-multiline-output "" } */
+
+/* Use a Python script to verify various properties about the generated
+   HTML file:
+   { dg-final { run-html-pytest diagnostic-test-paths-4.c "diagnostic-test-paths-4.py" } } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-4.py b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-paths-4.py
new file mode 100644 (file)
index 0000000..e738729
--- /dev/null
@@ -0,0 +1,190 @@
+# Verify that interprocedural execution paths work in HTML output.
+
+from htmltest import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def html_tree():
+    return html_tree_from_env()
+
+def test_paths(html_tree):
+    diag = get_diag_by_index(html_tree, 0)
+    src = get_locus_within_diag (diag)
+
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+
+    rows = tbody.findall('xhtml:tr', ns)
+
+    quoted_src_tr = rows[0]
+    assert_quoted_line(quoted_src_tr,
+                       '   13', '  fprintf(stderr, "LOG: %s", msg); /* { dg-warning "call to \'fprintf\' from within signal handler" } */')
+    
+    annotation_tr = rows[1]
+    assert_annotation_line(annotation_tr,
+                           '  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
+
+    event_ranges = diag.find('xhtml:div', ns)
+    assert_class(event_ranges, 'event-ranges')
+
+    test_frame_margin = event_ranges.find('xhtml:table', ns)
+    assert_class(test_frame_margin, 'stack-frame-with-margin')
+
+    tr = test_frame_margin.find('xhtml:tr', ns)
+    assert tr is not None
+    tds = tr.findall('xhtml:td', ns)
+    assert len(tds) == 2
+
+    assert_class(tds[0], 'interprocmargin')
+
+    test_frame = tds[1]
+    assert_frame(test_frame, 'test')
+    assert_event_range_with_margin(test_frame[1])
+
+# For reference, generated HTML looks like this:
+"""
+<table class="stack-frame-with-margin"><tr>
+  <td class="interprocmargin" style="padding-left: 100px"/>
+  <td class="stack-frame">
+<div class="frame-funcname"><span>test</span></div><table class="event-range-with-margin"><tr>
+  <td class="event-range">
+    <div class="events-hdr"><span class="funcname">test</span>: <span class="event-ids">events 1-2</span></div>
+<table class="locus">
+<tbody class="line-span">
+<tr><td class="linenum">   27</td> <td class="source">{</td></tr>
+<tr><td class="linenum"/><td class="annotation">^</td></tr>
+<tr><td class="linenum"/><td class="annotation">|</td></tr>
+<tr><td class="linenum"/><td class="annotation">(1) entering 'test'</td></tr>
+<tr><td class="linenum">   28</td> <td class="source">  register_handler ();</td></tr>
+<tr><td class="linenum"/><td class="annotation">  ~~~~~~~~~~~~~~~~~~~</td></tr>
+<tr><td class="linenum"/><td class="annotation">  |</td></tr>
+<tr><td class="linenum"/><td class="annotation">  (2) calling 'register_handler'</td></tr>
+</tbody>
+</table>
+</td></tr></table>
+<div class="between-ranges-call">
+  <svg height="30" width="150">
+    <defs>
+      <marker id="arrowhead" markerWidth="10" markerHeight="7"
+              refX="0" refY="3.5" orient="auto" stroke="#0088ce" fill="#0088ce">
+      <polygon points="0 0, 10 3.5, 0 7"/>
+      </marker>
+    </defs>
+    <polyline points="20,0 20,10 120,10 120,20"
+              style="fill:none;stroke: #0088ce"
+              marker-end="url(#arrowhead)"/>
+  </svg>
+</div>
+
+<table class="stack-frame-with-margin"><tr>
+  <td class="interprocmargin" style="padding-left: 100px"/>
+  <td class="stack-frame">
+<div class="frame-funcname"><span>register_handler</span></div><table class="event-range-with-margin"><tr>
+  <td class="event-range">
+    <div class="events-hdr"><span class="funcname">register_handler</span>: <span class="event-ids">events 3-4</span></div>
+<table class="locus">
+<tbody class="line-span">
+<tr><td class="linenum">   22</td> <td class="source">{</td></tr>
+<tr><td class="linenum"/><td class="annotation">^</td></tr>
+<tr><td class="linenum"/><td class="annotation">|</td></tr>
+<tr><td class="linenum"/><td class="annotation">(3) entering 'register_handler'</td></tr>
+<tr><td class="linenum">   23</td> <td class="source">  signal(SIGINT, int_handler);</td></tr>
+<tr><td class="linenum"/><td class="annotation">  ~~~~~~~~~~~~~~~~~~~~~~~~~~~</td></tr>
+<tr><td class="linenum"/><td class="annotation">  |</td></tr>
+<tr><td class="linenum"/><td class="annotation">  (4) registering 'int_handler' as signal handler</td></tr>
+</tbody>
+</table>
+</td></tr></table>
+</td></tr></table>
+</td></tr></table>
+<div class="between-ranges-return">
+  <svg height="30" width="250">
+    <defs>
+      <marker id="arrowhead" markerWidth="10" markerHeight="7"
+              refX="0" refY="3.5" orient="auto" stroke="#0088ce" fill="#0088ce">
+      <polygon points="0 0, 10 3.5, 0 7"/>
+      </marker>
+    </defs>
+    <polyline points="220,0 220,10 20,10 20,20"
+              style="fill:none;stroke: #0088ce"
+              marker-end="url(#arrowhead)"/>
+  </svg>
+</div>
+
+<table class="event-range-with-margin"><tr>
+  <td class="event-range">
+    <div class="events-hdr"><span class="event-ids">event 5</span></div>
+ (5): later on, when the signal is delivered to the process
+</td></tr></table>
+<div class="between-ranges-call">
+  <svg height="30" width="150">
+    <defs>
+      <marker id="arrowhead" markerWidth="10" markerHeight="7"
+              refX="0" refY="3.5" orient="auto" stroke="#0088ce" fill="#0088ce">
+      <polygon points="0 0, 10 3.5, 0 7"/>
+      </marker>
+    </defs>
+    <polyline points="20,0 20,10 120,10 120,20"
+              style="fill:none;stroke: #0088ce"
+              marker-end="url(#arrowhead)"/>
+  </svg>
+</div>
+
+<table class="stack-frame-with-margin"><tr>
+  <td class="interprocmargin" style="padding-left: 100px"/>
+  <td class="stack-frame">
+<div class="frame-funcname"><span>int_handler</span></div><table class="event-range-with-margin"><tr>
+  <td class="event-range">
+    <div class="events-hdr"><span class="funcname">int_handler</span>: <span class="event-ids">events 6-7</span></div>
+<table class="locus">
+<tbody class="line-span">
+<tr><td class="linenum">   17</td> <td class="source">{</td></tr>
+<tr><td class="linenum"/><td class="annotation">^</td></tr>
+<tr><td class="linenum"/><td class="annotation">|</td></tr>
+<tr><td class="linenum"/><td class="annotation">(6) entering 'int_handler'</td></tr>
+<tr><td class="linenum">   18</td> <td class="source">  custom_logger("got signal");</td></tr>
+<tr><td class="linenum"/><td class="annotation">  ~~~~~~~~~~~~~~~~~~~~~~~~~~~</td></tr>
+<tr><td class="linenum"/><td class="annotation">  |</td></tr>
+<tr><td class="linenum"/><td class="annotation">  (7) calling 'custom_logger'</td></tr>
+</tbody>
+</table>
+</td></tr></table>
+<div class="between-ranges-call">
+  <svg height="30" width="150">
+    <defs>
+      <marker id="arrowhead" markerWidth="10" markerHeight="7"
+              refX="0" refY="3.5" orient="auto" stroke="#0088ce" fill="#0088ce">
+      <polygon points="0 0, 10 3.5, 0 7"/>
+      </marker>
+    </defs>
+    <polyline points="20,0 20,10 120,10 120,20"
+              style="fill:none;stroke: #0088ce"
+              marker-end="url(#arrowhead)"/>
+  </svg>
+</div>
+
+<table class="stack-frame-with-margin"><tr>
+  <td class="interprocmargin" style="padding-left: 100px"/>
+  <td class="stack-frame">
+<div class="frame-funcname"><span>custom_logger</span></div><table class="event-range-with-margin"><tr>
+  <td class="event-range">
+    <div class="events-hdr"><span class="funcname">custom_logger</span>: <span class="event-ids">events 8-9</span></div>
+<table class="locus">
+<tbody class="line-span">
+<tr><td class="linenum">   12</td> <td class="source">{</td></tr>
+<tr><td class="linenum"/><td class="annotation">^</td></tr>
+<tr><td class="linenum"/><td class="annotation">|</td></tr>
+<tr><td class="linenum"/><td class="annotation">(8) entering 'custom_logger'</td></tr>
+<tr><td class="linenum">   13</td> <td class="source">  fprintf(stderr, "LOG: %s", msg); /* { dg-warning "call to 'fprintf' from within signal handler" } */</td></tr>
+<tr><td class="linenum"/><td class="annotation">  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</td></tr>
+<tr><td class="linenum"/><td class="annotation">  |</td></tr>
+<tr><td class="linenum"/><td class="annotation">  (9) calling 'fprintf'</td></tr>
+</tbody>
+</table>
+</td></tr></table>
+</td></tr></table>
+</td></tr></table>
+
+</div>
+    """
index 1e8f73bdb5d92763b7c74864a6696767291b02b0..e81856a10e3698091a04ce525d8fb1637c96d7d1 100644 (file)
@@ -1,5 +1,5 @@
 /* { dg-do compile } */
-/* { dg-options "-O -fdiagnostics-show-caret -fdiagnostics-show-line-numbers" } */
+/* { dg-options "-O -fdiagnostics-show-caret -fdiagnostics-show-line-numbers -fdiagnostics-add-output=experimental-html:javascript=no" } */
 
 /* This is a collection of unittests for diagnostic_show_locus;
    see the overview in diagnostic_plugin_test_show_locus.c.
@@ -118,3 +118,7 @@ void test_fixit_insert_newline (void)
    { dg-end-multiline-output "" } */
 #endif
 }
+
+/* Use a Python script to verify various properties about the generated
+   HTML file:
+   { dg-final { run-html-pytest diagnostic-test-show-locus-bw-line-numbers.c "diagnostic-test-show-locus.py" } } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-show-locus.py b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-show-locus.py
new file mode 100644 (file)
index 0000000..d963b29
--- /dev/null
@@ -0,0 +1,111 @@
+# Verify that diagnostic-show-locus.cc works with HTML output.
+
+from htmltest import *
+
+import pytest
+
+@pytest.fixture(scope='function', autouse=True)
+def html_tree():
+    return html_tree_from_env()
+
+#def get_tr_within_thead(thead, idx)
+
+def get_ruler_text(thead, idx):
+    trs = thead.findall('xhtml:tr', ns)
+    tr = trs[idx]
+    tds = tr.findall('xhtml:td', ns)
+    assert len(tds) == 3
+    assert_class(tds[2], 'ruler')
+    return tds[2].text
+
+def test_very_wide_line(html_tree):
+    diag = get_diag_by_index(html_tree, 2)
+    src = get_locus_within_diag(diag)
+
+    # Check ruler
+    thead = src.find('xhtml:thead', ns)
+    assert_class(thead, 'ruler')
+    trs = thead.findall('xhtml:tr', ns)
+    assert len(trs) == 3
+
+    assert get_ruler_text(thead, 0) == '       0         0         0         0         0         1         1   '
+    assert get_ruler_text(thead, 1) == '       5         6         7         8         9         0         1   '
+    assert get_ruler_text(thead, 2) == '34567890123456789012345678901234567890123456789012345678901234567890123'
+
+    # Check quoted source
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+    trs = tbody.findall('xhtml:tr', ns)
+    assert len(trs) == 5
+    assert_quoted_line(trs[0], '   43', '                                      float f = foo * bar; /* { dg-warning "95: test" } */')
+    assert_annotation_line(trs[1],      '                                                ~~~~^~~~~')
+    assert_annotation_line(trs[2],      '                                                    |')
+    assert_annotation_line(trs[3],      '                                                    label 0')
+    assert_annotation_line(trs[4],      '                                                bar * foo')
+
+def test_fixit_insert(html_tree):
+    diag = get_diag_by_index(html_tree, 3)
+    msg = get_message_within_diag(diag)
+    assert msg.text == 'example of insertion hints'
+
+    src = get_locus_within_diag(diag)
+
+    # Check quoted source
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+    trs = tbody.findall('xhtml:tr', ns)
+    assert len(trs) == 3
+    assert_quoted_line(trs[0], '   63', '   int a[2][2] = { 0, 1 , 2, 3 }; /* { dg-warning "insertion hints" } */')
+    assert_annotation_line(trs[1],      '                   ^~~~')
+    assert_annotation_line(trs[2],      '                   {   }')
+
+def test_fixit_remove(html_tree):
+    diag = get_diag_by_index(html_tree, 4)
+    msg = get_message_within_diag(diag)
+    assert msg.text == 'example of a removal hint'
+
+    src = get_locus_within_diag(diag)
+
+    # Check quoted source
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+    trs = tbody.findall('xhtml:tr', ns)
+    assert len(trs) == 3
+    assert_quoted_line(trs[0], '   77', '  int a;; /* { dg-warning "example of a removal hint" } */')
+    assert_annotation_line(trs[1],      '        ^')
+    assert_annotation_line(trs[2],      '        -')
+
+def test_fixit_replace(html_tree):
+    diag = get_diag_by_index(html_tree, 5)
+    msg = get_message_within_diag(diag)
+    assert msg.text == 'example of a replacement hint'
+
+    src = get_locus_within_diag(diag)
+
+    # Check quoted source
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+    trs = tbody.findall('xhtml:tr', ns)
+    assert len(trs) == 3
+    assert_quoted_line(trs[0], '   91', '  gtk_widget_showall (dlg); /* { dg-warning "example of a replacement hint" } */')
+    assert_annotation_line(trs[1],      '  ^~~~~~~~~~~~~~~~~~')
+    assert_annotation_line(trs[2],      '  gtk_widget_show_all')
+
+def test_fixit_insert_newline(html_tree):
+    diag = get_diag_by_index(html_tree, 6)
+    msg = get_message_within_diag(diag)
+    assert msg.text == 'example of newline insertion hint'
+
+    src = get_locus_within_diag(diag)
+
+    # Check quoted source
+    tbody = src.find('xhtml:tbody', ns)
+    assert_class(tbody, 'line-span')
+    trs = tbody.findall('xhtml:tr', ns)
+    assert len(trs) == 4
+    assert_quoted_line(trs[0], '  109', '      x = a;')
+    assert_annotation_line(trs[1],      '      break;',
+                           expected_line_num='  +++',
+                           expected_left_margin='+')
+    assert_quoted_line(trs[2], '  110', "    case 'b':  /* { dg-warning \"newline insertion\" } */")
+    assert_annotation_line(trs[3],      '    ^~~~~~~~')
index 5ec341847d996b9dd924a7bba7e427cfccda83cf..4ade2328dd518943c12acd65915cb68ba7221f47 100644 (file)
@@ -176,9 +176,10 @@ test_diagnostic_text_starter (diagnostic_text_output_format &text_output,
 
 void
 test_diagnostic_start_span_fn (const diagnostic_location_print_policy &,
-                              pretty_printer *pp,
+                              to_text &sink,
                               expanded_location)
 {
+  pretty_printer *pp = get_printer (sink);
   pp_string (pp, "START_SPAN_FN: ");
   pp_newline (pp);
 }
index b249ea691f19e205b6a3ae321755c35cc60647da..8e42a8c2a17e4273d0a57c7623d6c7568d4a45bd 100644 (file)
@@ -7,3 +7,93 @@ def html_tree_from_env():
     html_filename += '.html'
     print('html_filename: %r' % html_filename)
     return ET.parse(html_filename)
+
+XHTML = 'http://www.w3.org/1999/xhtml'
+ns = {'xhtml': XHTML}
+
+def make_tag(local_name):
+    return f'{{{XHTML}}}' + local_name
+
+def assert_tag(element, expected):
+    assert element.tag == make_tag(expected)
+
+def assert_class(element, expected):
+    assert element.attrib['class'] == expected
+
+def assert_quoted_line(tr, expected_line_num, expected_src):
+    """Verify that tr is a line of quoted source."""
+    tds = tr.findall('xhtml:td', ns)
+    assert len(tds) == 3
+    assert_class(tds[0], 'linenum')
+    assert tds[0].text == expected_line_num
+    assert_class(tds[1], 'left-margin')
+    assert tds[1].text == ' '
+    assert_class(tds[2], 'source')
+    assert tds[2].text == expected_src
+
+def assert_annotation_line(tr, expected_src,
+                           expected_line_num='     ',
+                           expected_left_margin=' '):
+    """Verify that tr is an annotation line."""
+    tds = tr.findall('xhtml:td', ns)
+    assert len(tds) == 3
+    assert_class(tds[0], 'linenum')
+    assert tds[0].text == expected_line_num
+    assert_class(tds[1], 'left-margin')
+    assert tds[1].text == expected_left_margin
+    assert_class(tds[2], 'annotation')
+    assert tds[2].text == expected_src
+
+def assert_frame(frame, expected_fnname):
+    """
+    Assert that frame is of class 'stack-frame'
+    and has a child showing the expected fnname.
+    """
+    assert_class(frame, 'stack-frame')
+    funcname = frame[0]
+    assert_class(funcname, 'frame-funcname')
+    span = funcname[0]
+    assert_tag(span, 'span')
+    assert span.text == expected_fnname
+
+def assert_event_range_with_margin(element):
+    """
+    Verify that "element" is an event-range-with-margin
+    """
+    assert_tag(element, 'table')
+    assert_class(element, 'event-range-with-margin')
+    tr = element.find('xhtml:tr', ns)
+    assert tr is not None
+    td = tr.find('xhtml:td', ns)
+    assert_class(td, 'event-range')
+
+    events_hdr = td.find('xhtml:div', ns)
+    assert_class(events_hdr, 'events-hdr')
+
+    #...etc
+
+def get_diag_by_index(html_tree, index):
+    root = html_tree.getroot ()
+    assert root.tag == make_tag('html')
+
+    body = root.find('xhtml:body', ns)
+    assert body is not None
+
+    diag_list = body.find('xhtml:div', ns)
+    assert diag_list is not None
+    assert_class(diag_list, 'gcc-diagnostic-list')
+
+    diags = diag_list.findall('xhtml:div', ns)
+    diag = diags[index]
+    assert_class(diag, 'gcc-diagnostic')
+    return diag
+
+def get_message_within_diag(diag_element):
+    msg = diag_element.find('xhtml:span', ns)
+    assert_class(msg, 'gcc-message')
+    return msg
+
+def get_locus_within_diag(diag_element):
+    src = diag_element.find('xhtml:table', ns)
+    assert_class(src, 'locus')
+    return src
diff --git a/gcc/xml-printer.h b/gcc/xml-printer.h
new file mode 100644 (file)
index 0000000..b90390c
--- /dev/null
@@ -0,0 +1,84 @@
+/* Classes for creating XML trees by appending.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_XML_PRINTER_H
+#define GCC_XML_PRINTER_H
+
+namespace xml {
+
+class node;
+  class element;
+
+/* A class for creating XML trees by appending to an insertion
+   point, with a stack of open tags.  */
+
+class printer
+{
+public:
+  printer (element &insertion_point);
+
+  void push_tag (std::string name,
+                bool preserve_whitespace = false);
+  void push_tag_with_class (std::string name,
+                           std::string class_,
+                           bool preserve_whitespace = false);
+  void pop_tag ();
+
+  void set_attr (const char *name, std::string value);
+
+  void add_text (std::string text);
+
+  void add_raw (std::string text);
+
+  void push_element (std::unique_ptr<element> new_element);
+
+  void append (std::unique_ptr<node> new_node);
+
+  element *get_insertion_point () const;
+
+private:
+  // borrowed ptrs:
+  std::vector<element *> m_open_tags;
+};
+
+// RAII for push/pop element on xml::printer
+
+class auto_print_element
+{
+public:
+  auto_print_element (printer &printer,
+                     std::string name,
+                     bool preserve_whitespace = false)
+  : m_printer (printer)
+  {
+    m_printer.push_tag (name, preserve_whitespace);
+  }
+  ~auto_print_element ()
+  {
+    m_printer.pop_tag ();
+  }
+
+private:
+  printer &m_printer;
+};
+
+} // namespace xml
+
+#endif /* GCC_XML_PRINTER_H.  */
diff --git a/gcc/xml.h b/gcc/xml.h
new file mode 100644 (file)
index 0000000..523a44d
--- /dev/null
+++ b/gcc/xml.h
@@ -0,0 +1,113 @@
+/* Classes for representing XML trees.
+   Copyright (C) 2024-2025 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_XML_H
+#define GCC_XML_H
+
+namespace xml {
+
+// Forward decls; indentation reflects inheritance
+struct node;
+  struct text;
+  struct node_with_children;
+    struct document;
+    struct element;
+
+struct node
+{
+  virtual ~node () {}
+  virtual void write_as_xml (pretty_printer *pp,
+                            int depth, bool indent) const = 0;
+  virtual text *dyn_cast_text ()
+  {
+    return 0;
+  }
+  void dump (FILE *out) const;
+  void DEBUG_FUNCTION dump () const { dump (stderr); }
+};
+
+struct text : public node
+{
+  text (std::string str)
+  : m_str (std::move (str))
+  {}
+
+  void write_as_xml (pretty_printer *pp,
+                    int depth, bool indent) const final override;
+
+  text *dyn_cast_text () final override
+  {
+    return this;
+  }
+
+  std::string m_str;
+};
+
+struct node_with_children : public node
+{
+  void add_child (std::unique_ptr<node> node);
+  void add_text (std::string str);
+
+  std::vector<std::unique_ptr<node>> m_children;
+};
+
+struct document : public node_with_children
+{
+  void write_as_xml (pretty_printer *pp,
+                    int depth, bool indent) const final override;
+};
+
+struct element : public node_with_children
+{
+  element (std::string kind, bool preserve_whitespace)
+    : m_kind (std::move (kind)),
+    m_preserve_whitespace (preserve_whitespace)
+  {}
+
+  void write_as_xml (pretty_printer *pp,
+                    int depth, bool indent) const final override;
+
+  void set_attr (const char *name, std::string value);
+
+  std::string m_kind;
+  bool m_preserve_whitespace;
+  std::map<std::string, std::string> m_attributes;
+  std::vector<std::string> m_key_insertion_order;
+};
+
+/* A fragment of raw XML source, to be spliced in directly.
+   Use sparingly.  */
+
+struct raw : public node
+{
+  raw (std::string xml_src)
+  : m_xml_src (xml_src)
+  {
+  }
+
+  void write_as_xml (pretty_printer *pp,
+                    int depth, bool indent) const final override;
+
+  std::string m_xml_src;
+};
+
+} // namespace xml
+
+#endif /* GCC_XML_H.  */