From 1b473f61dcb1d48ee29b3cbf0c7b474db932099e Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Thu, 15 Oct 2020 10:23:10 -0400
Subject: [PATCH 05/49] Add -fdiagnostics-path-format=html

The patch generalizes diagnostic-show-locus.c so that it can print
HTML, as well as the existing text output.
It uses this to implement a new -fdiagnostics-path-format=html
option, in which diagnostic paths are written out as HTML files
of the form "DUMP_BASE_NAME.path-[0-9]+.html"

For example:

LANG=C ./xgcc -B. -fanalyzer -c test.c -fdiagnostics-path-format=html
test.c: In function 'wrapped_free':
test.c:11:3: warning: double-'free' of 'ptr' [CWE-415] [-Wanalyzer-double-free]
   11 |   free (ptr);
      |   ^~~~~~~~~~
test.c:11:3: note: path with 23 events written to 'test.c.path-1.html'

which moves the verbose path information from stderr to the HTML file.

I've uploaded the generated HTML for that example to:
  https://dmalcolm.fedorapeople.org/gcc/2020-10-22/html-examples/test.c.path-1.html
and other examples can be seen in that directory.

There is a little JavaScript to allow for using the "j" and "k" keys
to go backwards and forwards through the events in the path.

Currently it merely emits the path information; it doesn't capture the
associated diagnostic; perhaps this should be a diagnostic format option
instead?

Also, there are various places that could be improved.  For example,
LTO paths where the filename changes are currently printed rather
plainly:
  https://dmalcolm.fedorapeople.org/gcc/2020-10-22/html-examples/a.wpa.path-1.html
Help with HTML, JS and CSS would be appreciated.

Tested lightly on Firefox, Chromium, and lynx.

gcc/ChangeLog:
	* common.opt (diagnostic_path_format): Add DPF_HTML.
	* diagnostic-show-locus.c (colorizer::m_context): Replace with...
	(colorizer::m_pp): ...this new field.
	(layout::m_context): Make const.
	(layout::m_is_html): New field.
	(layout::m_html_writer): New field.
	(colorizer::colorizer): Update for use of pp rather than context.
	(colorizer::begin_state): Likewise.
	(colorizer::finish_state): Likewise.
	(colorizer::get_color_by_name): Likewise.
	(layout::layout): Make "context" param const.  Add "pp", "is_html"
	and "writer" params, with defaults.  Use them to initialize new
	fields.  Update for change to m_colorizer.
	(layout::print_source_line): Add HTML support.
	(layout::start_annotation_line): Likewise.
	(layout::print_annotation_line): Likewise.
	(line_label::line_label): Make context const.
	(layout::print_any_labels): Add HTML support.
	(get_affected_range): Make context const.
	(get_printed_columns): Likewise.
	(line_corrections::line_corrections): Likewise.
	(line_corrections::m_context): Likewise.
	(layout::print_line): Don't print fix-it hints for HTML support.
	(diagnostic_show_locus_as_html): New.
	(class selftest::html_printer): New.
	(selftest::assert_html_eq): New.
	(ASSERT_HTML_EQ): New.
	(selftest::test_one_liner_simple_caret): Verify the HTML output.
	(selftest::test_diagnostic_show_locus_fixit_lines): Likewise.
	(selftest::test_html): New.
	(selftest::diagnostic_show_locus_c_tests): Call it.
	* diagnostic.c (diagnostic_show_any_path): Pass the location to
	the print_path callback.
	* diagnostic.h (enum diagnostic_path_format): Add DPF_HTML.
	(diagnostic_context::num_html_paths): New field.
	(diagnostic_context::print_path): Add location_t param.
	(class html_writer): New.
	(diagnostic_show_locus_as_html): New decl.
	* doc/invoke.texi (Diagnostic Message Formatting Options): Add
	"html" to -fdiagnostics-path-format=.
	(-fdiagnostics-path-format=): Add html.
	* pretty-print.c (pp_write_text_as_html_to_stream): New.
	* pretty-print.h (pp_write_text_as_html_to_stream): New decl.
	* tree-diagnostic-path.cc: Define GCC_DIAG_STYLE where possible
	and necessary.  Include "options.h" for dump_base_name.
	(path_label::path_label): Add "colorize" param and use it to
	initialize m_colorize.
	(path_label::get_text): Use m_colorizer rather than accessing
	global_dc's printer.
	(path_label::m_colorize): New field.
	(event_range::event_range): Add "colorize" param and use it to
	initialize m_path_label.
	(event_range::print_as_html): New.
	(event_range::get_filename): New.
	(path_summary::path_summary): Add "colorize" param and use it when
	creating event_ranges.
	(class html_path_writer): New.
	(path_summary::print_as_html): New.
	(HTML_STYLE): New.
	(HTML_SCRIPT): New.
	(write_html_for_path): New.
	(default_tree_diagnostic_path_printer): Add "loc" param.
	Update DPF_INLINE_EVENTS for new "colorize" param of
	path_summary's ctor.  Add DPF_HTML.
	(selftest::test_empty_path): Update for new param of path_summary
	ctor.
	(selftest::test_intraprocedural_path): Likewise.
	(selftest::test_interprocedural_path_1): Likewise.
	(selftest::test_interprocedural_path_2): Likewise.
	(selftest::test_recursion): Likewise.
	* tree-diagnostic.h (default_tree_diagnostic_path_printer): Add
	location_t param.

gcc/testsuite/ChangeLog:
	* gcc.dg/plugin/diagnostic-path-format-html.c: New test.
	* gcc.dg/plugin/plugin.exp (plugin_test_list): Add it.
---
 gcc/common.opt                                |   3 +
 gcc/diagnostic-show-locus.c                   | 344 ++++++++++++++--
 gcc/diagnostic.c                              |   2 +-
 gcc/diagnostic.h                              |  23 +-
 gcc/doc/invoke.texi                           |  11 +-
 gcc/pretty-print.c                            |  43 ++
 gcc/pretty-print.h                            |   1 +
 .../plugin/diagnostic-path-format-html.c      |  45 +++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |   3 +-
 gcc/tree-diagnostic-path.cc                   | 368 +++++++++++++++++-
 gcc/tree-diagnostic.h                         |   3 +-
 11 files changed, 781 insertions(+), 65 deletions(-)
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-path-format-html.c

diff --git a/gcc/common.opt b/gcc/common.opt
index 7e789d1c47f..c520d5d86e9 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -1398,6 +1398,9 @@ Enum(diagnostic_path_format) String(separate-events) Value(DPF_SEPARATE_EVENTS)
 EnumValue
 Enum(diagnostic_path_format) String(inline-events) Value(DPF_INLINE_EVENTS)
 
+EnumValue
+Enum(diagnostic_path_format) String(html) Value(DPF_HTML)
+
 fdiagnostics-show-path-depths
 Common Var(flag_diagnostics_show_path_depths) Init(0)
 Show stack depths of events in paths.
diff --git a/gcc/diagnostic-show-locus.c b/gcc/diagnostic-show-locus.c
index da3c5b6a92d..a37d7ae3dc0 100644
--- a/gcc/diagnostic-show-locus.c
+++ b/gcc/diagnostic-show-locus.c
@@ -83,7 +83,7 @@ struct point_state
 class colorizer
 {
  public:
-  colorizer (diagnostic_context *context,
+  colorizer (pretty_printer *pp,
 	     diagnostic_t diagnostic_kind);
   ~colorizer ();
 
@@ -113,7 +113,7 @@ class colorizer
   static const int STATE_FIXIT_INSERT  = -2;
   static const int STATE_FIXIT_DELETE  = -3;
 
-  diagnostic_context *m_context;
+  pretty_printer *m_pp;
   diagnostic_t m_diagnostic_kind;
   int m_current_state;
   const char *m_range1;
@@ -324,9 +324,12 @@ test_line_span ()
 class layout
 {
  public:
-  layout (diagnostic_context *context,
+  layout (const diagnostic_context *context,
 	  rich_location *richloc,
-	  diagnostic_t diagnostic_kind);
+	  diagnostic_t diagnostic_kind,
+	  pretty_printer *pp = NULL,
+	  bool is_html = false,
+	  html_writer *writer = NULL);
 
   bool maybe_add_location_range (const location_range *loc_range,
 				 unsigned original_idx,
@@ -384,8 +387,10 @@ class layout
   move_to_column (int *column, int dest_column, bool add_left_margin);
 
  private:
-  diagnostic_context *m_context;
+  const diagnostic_context *m_context;
   pretty_printer *m_pp;
+  bool m_is_html;
+  html_writer *m_html_writer;
   location_t m_primary_loc;
   exploc_with_display_col m_exploc;
   colorizer m_colorizer;
@@ -405,9 +410,9 @@ class layout
 /* The constructor for "colorizer".  Lookup and store color codes for the
    different kinds of things we might need to print.  */
 
-colorizer::colorizer (diagnostic_context *context,
-		      diagnostic_t diagnostic_kind) :
-  m_context (context),
+colorizer::colorizer (pretty_printer *pp,
+		      diagnostic_t diagnostic_kind)
+: m_pp (pp),
   m_diagnostic_kind (diagnostic_kind),
   m_current_state (STATE_NORMAL_TEXT)
 {
@@ -415,7 +420,7 @@ colorizer::colorizer (diagnostic_context *context,
   m_range2 = get_color_by_name ("range2");
   m_fixit_insert = get_color_by_name ("fixit-insert");
   m_fixit_delete = get_color_by_name ("fixit-delete");
-  m_stop_color = colorize_stop (pp_show_color (context->printer));
+  m_stop_color = colorize_stop (pp_show_color (m_pp));
 }
 
 /* The destructor for "colorize".  If colorization is on, print a code to
@@ -451,35 +456,35 @@ colorizer::begin_state (int state)
       break;
 
     case STATE_FIXIT_INSERT:
-      pp_string (m_context->printer, m_fixit_insert);
+      pp_string (m_pp, m_fixit_insert);
       break;
 
     case STATE_FIXIT_DELETE:
-      pp_string (m_context->printer, m_fixit_delete);
+      pp_string (m_pp, m_fixit_delete);
       break;
 
     case 0:
       /* Make range 0 be the same color as the "kind" text
 	 (error vs warning vs note).  */
       pp_string
-	(m_context->printer,
-	 colorize_start (pp_show_color (m_context->printer),
+	(m_pp,
+	 colorize_start (pp_show_color (m_pp),
 			 diagnostic_get_color_for_kind (m_diagnostic_kind)));
       break;
 
     case 1:
-      pp_string (m_context->printer, m_range1);
+      pp_string (m_pp, m_range1);
       break;
 
     case 2:
-      pp_string (m_context->printer, m_range2);
+      pp_string (m_pp, m_range2);
       break;
 
     default:
       /* For ranges beyond 2, alternate between color 1 and color 2.  */
       {
 	gcc_assert (state > 2);
-	pp_string (m_context->printer,
+	pp_string (m_pp,
 		   state % 2 ? m_range1 : m_range2);
       }
       break;
@@ -492,7 +497,7 @@ void
 colorizer::finish_state (int state)
 {
   if (state != STATE_NORMAL_TEXT)
-    pp_string (m_context->printer, m_stop_color);
+    pp_string (m_pp, m_stop_color);
 }
 
 /* Get the color code for NAME (or the empty string if
@@ -501,7 +506,7 @@ colorizer::finish_state (int state)
 const char *
 colorizer::get_color_by_name (const char *name)
 {
-  return colorize_start (pp_show_color (m_context->printer), name);
+  return colorize_start (pp_show_color (m_pp), name);
 }
 
 /* Implementation of class layout_range.  */
@@ -961,14 +966,19 @@ fixit_cmp (const void *p_a, const void *p_b)
    Determine m_x_offset_display, to ensure that the primary caret
    will fit within the max_width provided by the diagnostic_context.  */
 
-layout::layout (diagnostic_context * context,
+layout::layout (const diagnostic_context *context,
 		rich_location *richloc,
-		diagnostic_t diagnostic_kind)
+		diagnostic_t diagnostic_kind,
+		pretty_printer *pp,
+		bool is_html,
+		html_writer *writer)
 : m_context (context),
-  m_pp (context->printer),
+  m_pp (pp ? pp : context->printer),
+  m_is_html (is_html),
+  m_html_writer (writer),
   m_primary_loc (richloc->get_range (0)->m_loc),
   m_exploc (richloc->get_expanded_location (0), context->tabstop),
-  m_colorizer (context, diagnostic_kind),
+  m_colorizer (m_pp, diagnostic_kind),
   m_colorize_source_p (context->colorize_source_p),
   m_show_labels_p (context->show_labels_p),
   m_show_line_numbers_p (context->show_line_numbers_p),
@@ -1461,13 +1471,21 @@ layout::print_source_line (linenum_type row, const char *line, int line_bytes)
 {
   m_colorizer.set_normal_text ();
 
+  if (m_is_html)
+      pp_string (m_pp, "<tr>");
+
   pp_emit_prefix (m_pp);
   if (m_show_line_numbers_p)
     {
+      if (m_is_html)
+	pp_string (m_pp, "<td class=\"linenum\">");
       int width = num_digits (row);
       for (int i = 0; i < m_linenum_width - width; i++)
 	pp_space (m_pp);
-      pp_printf (m_pp, "%i | ", row);
+      if (m_is_html)
+	pp_printf (m_pp, "%i</td>", row);
+      else
+	pp_printf (m_pp, "%i | ", row);
     }
   else
     pp_space (m_pp);
@@ -1481,6 +1499,12 @@ layout::print_source_line (linenum_type row, const char *line, int line_bytes)
      tab expansion, and for implementing m_x_offset_display.  */
   cpp_display_width_computation dw (line, line_bytes, m_context->tabstop);
 
+  if (m_is_html)
+    {
+      pp_string (m_pp, "<td class=\"source\">");
+      pp_write_text_to_stream (m_pp);
+    }
+
   /* 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
@@ -1558,6 +1582,12 @@ layout::print_source_line (linenum_type row, const char *line, int line_bytes)
       /* Output the character.  */
       while (c != dw.next_byte ()) pp_character (m_pp, *c++);
     }
+  if (m_is_html)
+    {
+      pp_write_text_as_html_to_stream (m_pp);
+      pp_string (m_pp, "</td>");
+      pp_string (m_pp, "</tr>");
+    }
   print_newline ();
   return lbounds;
 }
@@ -1587,16 +1617,23 @@ void
 layout::start_annotation_line (char margin_char) const
 {
   pp_emit_prefix (m_pp);
+  if (m_is_html)
+    pp_string (m_pp, "<tr>");
   if (m_show_line_numbers_p)
     {
-      /* Print the margin.  If MARGIN_CHAR != ' ', then print up to 3
-	 of it, right-aligned, padded with spaces.  */
-      int i;
-      for (i = 0; i < m_linenum_width - 3; i++)
-	pp_space (m_pp);
-      for (; i < m_linenum_width; i++)
-	pp_character (m_pp, margin_char);
-      pp_string (m_pp, " |");
+      if (m_is_html)
+	pp_string (m_pp, "<td class=\"linenum\"/>");
+      else
+	{
+	  /* Print the margin.  If MARGIN_CHAR != ' ', then print up to 3
+	     of it, right-aligned, padded with spaces.  */
+	  int i;
+	  for (i = 0; i < m_linenum_width - 3; i++)
+	    pp_space (m_pp);
+	  for (; i < m_linenum_width; i++)
+	    pp_character (m_pp, margin_char);
+	  pp_string (m_pp, " |");
+	}
     }
 }
 
@@ -1610,7 +1647,13 @@ layout::print_annotation_line (linenum_type row, const line_bounds lbounds)
 				     lbounds.m_last_non_ws_disp_col);
 
   start_annotation_line ();
-  pp_space (m_pp);
+  if (m_is_html)
+    {
+      pp_string (m_pp, "<td class=\"annotation\">");
+      pp_write_text_to_stream (m_pp);
+    }
+  else
+    pp_space (m_pp);
 
   for (int column = 1 + m_x_offset_display; column < x_bound; column++)
     {
@@ -1645,6 +1688,11 @@ layout::print_annotation_line (linenum_type row, const line_bounds lbounds)
 	  pp_character (m_pp, ' ');
 	}
     }
+  if (m_is_html)
+    {
+      pp_write_text_as_html_to_stream (m_pp);
+      pp_string (m_pp, "</td></tr>");
+    }
   print_newline ();
 }
 
@@ -1655,7 +1703,7 @@ layout::print_annotation_line (linenum_type row, const line_bounds lbounds)
 class line_label
 {
 public:
-  line_label (diagnostic_context *context, int state_idx, int column,
+  line_label (const diagnostic_context *context, int state_idx, int column,
 	      label_text text)
   : m_state_idx (state_idx), m_column (column),
     m_text (text), m_label_line (0), m_has_vbar (true)
@@ -1795,7 +1843,10 @@ layout::print_any_labels (linenum_type row)
     for (int label_line = 0; label_line <= max_label_line; label_line++)
       {
 	start_annotation_line ();
-	pp_space (m_pp);
+	if (m_is_html)
+	  pp_string (m_pp, "<td class=\"annotation\">");
+	else
+	  pp_space (m_pp);
 	int column = 1 + m_x_offset_display;
 	line_label *label;
 	FOR_EACH_VEC_ELT (labels, i, label)
@@ -1812,7 +1863,19 @@ layout::print_any_labels (linenum_type row)
 		   diagnostic_path.  */
 		if (!m_diagnostic_path_p)
 		  m_colorizer.set_range (label->m_state_idx);
+		if (m_is_html)
+		  {
+		    pp_write_text_to_stream (m_pp);
+		    if (m_html_writer)
+		      m_html_writer->begin_label (m_pp);
+		  }
 		pp_string (m_pp, label->m_text.m_buffer);
+		if (m_is_html)
+		  {
+		    pp_write_text_as_html_to_stream (m_pp);
+		    if (m_html_writer)
+		      m_html_writer->end_label (m_pp);
+		  }
 		m_colorizer.set_normal_text ();
 		column += label->m_display_width;
 	      }
@@ -1826,6 +1889,8 @@ layout::print_any_labels (linenum_type row)
 		column++;
 	      }
 	  }
+	if (m_is_html)
+	  pp_string (m_pp, "</td></tr>");
 	print_newline ();
       }
     }
@@ -2002,7 +2067,7 @@ public:
 
 /* Get the range of bytes or display columns that HINT would affect.  */
 static column_range
-get_affected_range (diagnostic_context *context,
+get_affected_range (const diagnostic_context *context,
 		    const fixit_hint *hint, enum column_unit col_unit)
 {
   expanded_location exploc_start = expand_location (hint->get_start_loc ());
@@ -2032,7 +2097,7 @@ get_affected_range (diagnostic_context *context,
 /* Get the range of display columns that would be printed for HINT.  */
 
 static column_range
-get_printed_columns (diagnostic_context *context, const fixit_hint *hint)
+get_printed_columns (const diagnostic_context *context, const fixit_hint *hint)
 {
   expanded_location exploc = expand_location (hint->get_start_loc ());
   int start_column = location_compute_display_column (exploc, context->tabstop);
@@ -2154,7 +2219,7 @@ correction::ensure_terminated ()
 class line_corrections
 {
 public:
-  line_corrections (diagnostic_context *context, const char *filename,
+  line_corrections (const diagnostic_context *context, const char *filename,
 		    linenum_type row)
     : m_context (context), m_filename (filename), m_row (row)
   {}
@@ -2162,7 +2227,7 @@ public:
 
   void add_hint (const fixit_hint *hint);
 
-  diagnostic_context *m_context;
+  const diagnostic_context *m_context;
   const char *m_filename;
   linenum_type m_row;
   auto_vec <correction *> m_corrections;
@@ -2543,7 +2608,8 @@ layout::print_line (linenum_type row)
     print_annotation_line (row, lbounds);
   if (m_show_labels_p)
     print_any_labels (row);
-  print_trailing_fixits (row);
+  if (!m_is_html)
+    print_trailing_fixits (row);
 }
 
 } /* End of anonymous namespace.  */
@@ -2629,6 +2695,61 @@ diagnostic_show_locus (diagnostic_context * context,
     }
 }
 
+/* As diagnostic_show_locus, but print to PP in HTML form, writing to the
+   underlying stream as necessary whenever escaping the text.
+   PP may be a different printer to DC's printer.
+   WRITER can be NULL, or can be non-NULL to customize how the HTML
+   is printed.
+   Return true if anything was printed.  */
+
+bool
+diagnostic_show_locus_as_html (diagnostic_context *dc,
+			       pretty_printer *pp,
+			       html_writer *writer,
+			       rich_location *richloc,
+			       diagnostic_t diagnostic_kind)
+{
+  location_t loc = richloc->get_loc ();
+
+  /* Don't attempt to print source for UNKNOWN_LOCATION and for builtins.  */
+  if (loc <= BUILTINS_LOCATION)
+    return false;
+
+  pp_string (pp, "<table class=\"locus\">\n");
+
+  layout layout (dc, richloc, diagnostic_kind, pp, true, writer);
+  for (int line_span_idx = 0; line_span_idx < layout.get_num_line_spans ();
+       line_span_idx++)
+    {
+      const line_span *line_span = layout.get_line_span (line_span_idx);
+
+      if (line_span_idx > 0)
+	{
+	  pp_string (pp,
+		     "<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");
+	}
+
+      pp_string (pp, "<tbody class=\"line-span\">\n");
+
+      /* 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++)
+	layout.print_line (row);
+
+      pp_string (pp, "</tbody>\n");
+    }
+
+  pp_string (pp, "</table>\n");
+
+  return true;
+}
+
 #if CHECKING_P
 
 namespace selftest {
@@ -2913,6 +3034,61 @@ test_layout_x_offset_display_tab (const line_table_case &case_)
     }
 }
 
+/* A test fixture for capturing escaped HTML output.
+   This has to be done from FILE * stream, rather than from a pretty_printer,
+   since escaping happens when writing from the latter to the former.  */
+
+class html_printer
+{
+ public:
+  html_printer ()
+    : m_pp (), m_outf_name (".html")
+  {
+    m_pp.buffer->stream = fopen (m_outf_name.get_filename (), "w");
+    ASSERT_NE (m_pp.buffer->stream, NULL);
+  }
+
+  pretty_printer *get_pp () { return &m_pp; }
+
+  char *get_html_output ()
+  {
+    gcc_assert (m_pp.buffer->stream);
+    pp_flush (&m_pp);
+    fclose (m_pp.buffer->stream);
+    m_pp.buffer->stream = NULL;
+    return read_file (SELFTEST_LOCATION, m_outf_name.get_filename ());
+  }
+
+ private:
+  pretty_printer m_pp;
+  named_temp_file m_outf_name;
+};
+
+/* Implementation of ASSERT_HTML_EQ.  */
+
+static void
+assert_html_eq (const location &loc,
+		diagnostic_context *dc,
+		rich_location *rich_loc,
+		diagnostic_t diagnostic_kind,
+		const char *expected_html)
+{
+
+  html_printer to_html;
+  diagnostic_show_locus_as_html (dc, to_html.get_pp (), NULL,
+				 rich_loc, diagnostic_kind);
+  char *actual_html = to_html.get_html_output ();
+  ASSERT_STREQ_AT (loc, actual_html, expected_html);
+  free (actual_html);
+}
+
+/* Assert that when printing RICHLOC as HTML to DC's printer that the
+   result written to the stream after escaping is EXPECTED_HTML.  */
+
+#define ASSERT_HTML_EQ(DC, RICHLOC, DK, EXPECTED_HTML) \
+  SELFTEST_BEGIN_STMT						    \
+  assert_html_eq (SELFTEST_LOCATION, (DC), (RICHLOC), (DK), (EXPECTED_HTML)); \
+  SELFTEST_END_STMT
 
 /* Verify that diagnostic_show_locus works sanely on UNKNOWN_LOCATION.  */
 
@@ -2947,6 +3123,14 @@ test_one_liner_simple_caret ()
   ASSERT_STREQ (" foo = bar.field;\n"
 		"          ^\n",
 		pp_formatted_text (dc.printer));
+  ASSERT_HTML_EQ
+    (&dc, &richloc, DK_ERROR,
+     "<table class=\"locus\">\n"
+     "<tbody class=\"line-span\">\n"
+     "<tr> <td class=\"source\">foo = bar.field;</td></tr>\n"
+     "<tr><td class=\"annotation\">         ^</td></tr>\n"
+     "</tbody>\n"
+     "</table>\n");
 }
 
 /* Caret and range.  */
@@ -4224,6 +4408,22 @@ test_diagnostic_show_locus_fixit_lines (const line_table_case &case_)
 		  "      |                         ^\n"
 		  "      |                         =\n",
 		  pp_formatted_text (dc.printer));
+    /* Verify that HTML output correctly emits a line-numbering gap.
+       Fix-it hints aren't yet emitted in HTML output.  */
+    ASSERT_HTML_EQ
+      (&dc, &richloc, DK_ERROR,
+       "<table class=\"locus\">\n"
+       "<tbody class=\"line-span\">\n"
+       "<tr><td class=\"linenum\">    3</td><td class=\"source\">                       y</td></tr>\n"
+       "</tbody>\n"
+       "<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"
+       "<tbody class=\"line-span\">\n"
+       "<tr><td class=\"linenum\">    6</td><td class=\"source\">                        : 0.0};</td></tr>\n"
+       "<tr><td class=\"linenum\"/><td class=\"annotation\">                        ^</td></tr>\n"
+       "</tbody>\n"
+       "</table>\n");
   }
 }
 
@@ -5187,6 +5387,71 @@ test_tab_expansion (const line_table_case &case_)
   }
 }
 
+/* Test of HTML output, with an example of labeling the ranges within
+   a rich_location.  */
+
+static void
+test_html (const line_table_case &case_)
+{
+  /* Create a tempfile and write some text to it.
+     ....................0000000001111111.
+     ....................1234567890123456.  */
+  const char *content = "foo = bar.field; /* < & > \".  */\n";
+  /* This is similar to the text in test_diagnostic_show_locus_one_liner,
+     but with characters needing HTML escaping added.  */
+  temp_source_file tmp (SELFTEST_LOCATION, ".c", content);
+  line_table_test ltt (case_);
+
+  linemap_add (line_table, LC_ENTER, false, tmp.get_filename (), 1);
+
+  location_t last_col = linemap_position_for_column (line_table, 16);
+
+  /* Don't attempt to run the tests if column data might be unavailable.  */
+  if (last_col > LINE_MAP_MAX_LOCATION_WITH_COLS)
+    return;
+
+  location_t foo
+    = make_location (linemap_position_for_column (line_table, 1),
+		     linemap_position_for_column (line_table, 1),
+		     linemap_position_for_column (line_table, 3));
+  location_t bar
+    = make_location (linemap_position_for_column (line_table, 7),
+		     linemap_position_for_column (line_table, 7),
+		     linemap_position_for_column (line_table, 9));
+  location_t field
+    = make_location (linemap_position_for_column (line_table, 11),
+		     linemap_position_for_column (line_table, 11),
+		     linemap_position_for_column (line_table, 15));
+
+  if (field > LINE_MAP_MAX_LOCATION_WITH_COLS)
+      return;
+
+  /* Example where the labels need extra lines.  */
+  {
+    text_range_label label0 ("label 0");
+    text_range_label label1 ("label 1");
+    text_range_label label2 ("label 2");
+    gcc_rich_location richloc (foo, &label0);
+    richloc.add_range (bar, SHOW_RANGE_WITHOUT_CARET, &label1);
+    richloc.add_range (field, SHOW_RANGE_WITHOUT_CARET, &label2);
+
+    test_diagnostic_context dc;
+    ASSERT_HTML_EQ
+      (&dc, &richloc, DK_ERROR,
+       "<table class=\"locus\">\n"
+       "<tbody class=\"line-span\">\n"
+       "<tr> <td class=\"source\">foo = bar.field;"
+       " /* &lt; &amp; &gt; &quot;.  */</td></tr>\n"
+       "<tr><td class=\"annotation\">^~~   ~~~ ~~~~~</td></tr>\n"
+       "<tr><td class=\"annotation\">|     |   |</td></tr>\n"
+       "<tr><td class=\"annotation\">|     |   label 2</td></tr>\n"
+       "<tr><td class=\"annotation\">|     label 1</td></tr>\n"
+       "<tr><td class=\"annotation\">label 0</td></tr>\n"
+       "</tbody>\n"
+       "</table>\n");
+  }
+}
+
 /* Verify that line numbers are correctly printed for the case of
    a multiline range in which the width of the line numbers changes
    (e.g. from "9" to "10").  */
@@ -5263,6 +5528,7 @@ diagnostic_show_locus_c_tests ()
   for_each_line_table_case (test_fixit_replace_containing_newline);
   for_each_line_table_case (test_fixit_deletion_affecting_newline);
   for_each_line_table_case (test_tab_expansion);
+  for_each_line_table_case (test_html);
 
   test_line_numbers_multiline_range ();
 }
diff --git a/gcc/diagnostic.c b/gcc/diagnostic.c
index 1b6c9845892..316b6c615b4 100644
--- a/gcc/diagnostic.c
+++ b/gcc/diagnostic.c
@@ -734,7 +734,7 @@ diagnostic_show_any_path (diagnostic_context *context,
     return;
 
   if (context->print_path)
-    context->print_path (context, path);
+    context->print_path (context, path, diagnostic->richloc->get_loc ());
 }
 
 /* Return true if the events in this path involve more than one
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index 4051601abfd..c303a83ba22 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -63,7 +63,10 @@ enum diagnostic_path_format
      are close enough, and printing such runs of events with multiple
      calls to diagnostic_show_locus, showing the individual events in
      each run via labels in the source.  */
-  DPF_INLINE_EVENTS
+  DPF_INLINE_EVENTS,
+
+  /* Print diagnostic_paths by emitting an HTML file for each path.  */
+  DPF_HTML
 };
 
 /* A diagnostic is described by the MESSAGE to send, the FILE and LINE of
@@ -169,6 +172,9 @@ struct diagnostic_context
   /* How should diagnostic_path objects be printed.  */
   enum diagnostic_path_format path_format;
 
+  /* How many HTML paths have been emitted by DPF_HTML.  */
+  int num_html_paths;
+
   /* True if we should print stack depths when printing diagnostic paths.  */
   bool show_path_depths;
 
@@ -246,7 +252,9 @@ struct diagnostic_context
      particular option.  */
   char *(*get_option_url) (diagnostic_context *, int);
 
-  void (*print_path) (diagnostic_context *, const diagnostic_path *);
+  void (*print_path) (diagnostic_context *,
+		      const diagnostic_path *,
+		      location_t);
   json::value *(*make_json_for_path) (diagnostic_context *, const diagnostic_path *);
 
   /* Auxiliary data for client.  */
@@ -401,6 +409,17 @@ extern void diagnostic_report_current_module (diagnostic_context *, location_t);
 extern void diagnostic_show_locus (diagnostic_context *,
 				   rich_location *richloc,
 				   diagnostic_t diagnostic_kind);
+class html_writer
+{
+public:
+  virtual void begin_label (pretty_printer *pp) = 0;
+  virtual void end_label (pretty_printer *pp) = 0;
+};
+extern bool diagnostic_show_locus_as_html (diagnostic_context *dc,
+					   pretty_printer *pp,
+					   html_writer *writer,
+					   rich_location *richloc,
+					   diagnostic_t diagnostic_kind);
 extern void diagnostic_show_any_path (diagnostic_context *, diagnostic_info *);
 
 /* Force diagnostics controlled by OPTIDX to be kind KIND.  */
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 0b87822349f..eb8c323e50e 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -292,7 +292,7 @@ Objective-C and Objective-C++ Dialects}.
 -fdiagnostics-minimum-margin-width=@var{width} @gol
 -fdiagnostics-parseable-fixits  -fdiagnostics-generate-patch @gol
 -fdiagnostics-show-template-tree  -fno-elide-type @gol
--fdiagnostics-path-format=@r{[}none@r{|}separate-events@r{|}inline-events@r{]} @gol
+-fdiagnostics-path-format=@r{[}none@r{|}separate-events@r{|}inline-events@r{|}html@r{]} @gol
 -fdiagnostics-show-path-depths @gol
 -fno-show-column @gol
 -fdiagnostics-column-unit=@r{[}display@r{|}byte@r{]} @gol
@@ -4687,8 +4687,8 @@ This flag also affects the output of the
 Specify how to print paths of control-flow events for diagnostics that
 have such a path associated with them.
 
-@var{KIND} is @samp{none}, @samp{separate-events}, or @samp{inline-events},
-the default.
+@var{KIND} is @samp{none}, @samp{separate-events}, @samp{inline-events},
+or @samp{html}.  The default is @samp{inline-events}.
 
 @samp{none} means to not print diagnostic paths.
 
@@ -4779,6 +4779,11 @@ For example:
 (etc)
 @end smallexample
 
+@samp{html} means to write a separate HTML file for each diagnostic path,
+printing a ``note'' diagnostic giving the name of this file.  The generated
+HTML is intended for human consumption and the precise output format is
+liable to change.
+
 @item -fdiagnostics-show-path-depths
 @opindex fdiagnostics-show-path-depths
 This option provides additional information when printing control-flow paths
diff --git a/gcc/pretty-print.c b/gcc/pretty-print.c
index 407f7300dfb..335dae8bef0 100644
--- a/gcc/pretty-print.c
+++ b/gcc/pretty-print.c
@@ -956,6 +956,49 @@ pp_write_text_as_html_like_dot_to_stream (pretty_printer *pp)
   pp_clear_output_area (pp);
 }
 
+/* As pp_write_text_to_stream, but for HTML strings.
+
+   Flush the formatted text of pretty-printer PP onto the attached stream,
+   escaping these characters
+     ' " & < >
+   using named character references.  */
+
+void
+pp_write_text_as_html_to_stream (pretty_printer *pp)
+{
+  const char *text = pp_formatted_text (pp);
+  const char *p = text;
+  FILE *fp = pp_buffer (pp)->stream;
+
+  for (;*p; p++)
+    {
+      switch (*p)
+	{
+	case '\'':
+	  fputs ("&apos;", fp);
+	  break;
+	case '"':
+	  fputs ("&quot;", fp);
+	  break;
+	case '&':
+	  fputs ("&amp;", fp);
+	  break;
+	case '<':
+	  fputs ("&lt;", fp);
+	  break;
+	case '>':
+	  fputs ("&gt;",fp);
+	  break;
+
+	default:
+	  fputc (*p, fp);
+	  break;
+	}
+    }
+
+  pp_clear_output_area (pp);
+}
+
 /* Wrap a text delimited by START and END into PRETTY-PRINTER.  */
 static void
 pp_wrap_text (pretty_printer *pp, const char *start, const char *end)
diff --git a/gcc/pretty-print.h b/gcc/pretty-print.h
index 22892f12ab7..bf011f34c4b 100644
--- a/gcc/pretty-print.h
+++ b/gcc/pretty-print.h
@@ -398,6 +398,7 @@ extern void pp_string (pretty_printer *, const char *);
 extern void pp_write_text_to_stream (pretty_printer *);
 extern void pp_write_text_as_dot_label_to_stream (pretty_printer *, bool);
 extern void pp_write_text_as_html_like_dot_to_stream (pretty_printer *pp);
+extern void pp_write_text_as_html_to_stream (pretty_printer *pp);
 
 extern void pp_maybe_space (pretty_printer *);
 
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-path-format-html.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-path-format-html.c
new file mode 100644
index 00000000000..bdce3c0c843
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-path-format-html.c
@@ -0,0 +1,45 @@
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-path-format=html" } */
+
+#include <stdlib.h>
+
+void *wrapped_malloc (size_t size)
+{
+  return malloc (size);
+}
+
+void wrapped_free (void *ptr)
+{
+  free (ptr); /* { dg-warning "double-free of 'ptr' \\\[CWE-415\\]" } */
+}
+
+typedef struct boxed_int
+{
+  int i;
+} boxed_int;
+
+boxed_int *
+make_boxed_int (int i)
+{
+  boxed_int *result = (boxed_int *)wrapped_malloc (sizeof (boxed_int));
+  result->i = i;
+  return result;
+}
+
+void
+free_boxed_int (boxed_int *bi)
+{
+  wrapped_free (bi);
+}
+
+void test (int i)
+{
+  boxed_int *obj = make_boxed_int (i);
+  /* etc */
+
+  free_boxed_int (obj);
+
+  free_boxed_int (obj);
+}
+
+/* { dg-final { scan-file diagnostic-path-format-html.c.path-1.html "calling &apos;make_boxed_int&apos;" } } */
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index 5dd102ae05c..cc7faf2cd0f 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -105,7 +105,8 @@ set plugin_test_list [list \
 	  diagnostic-path-format-separate-events.c \
 	  diagnostic-path-format-inline-events-1.c \
 	  diagnostic-path-format-inline-events-2.c \
-	  diagnostic-path-format-inline-events-3.c } \
+	  diagnostic-path-format-inline-events-3.c \
+	  diagnostic-path-format-html.c } \
     { location_overflow_plugin.c \
 	  location-overflow-test-1.c \
 	  location-overflow-test-2.c \
diff --git a/gcc/tree-diagnostic-path.cc b/gcc/tree-diagnostic-path.cc
index 82b3c2d6b6a..151ffc5e7a9 100644
--- a/gcc/tree-diagnostic-path.cc
+++ b/gcc/tree-diagnostic-path.cc
@@ -18,10 +18,18 @@ 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/>.  */
 
+/* This source file uses pp_printf to build up HTML fragments, hence
+   use "_raw" to suppress -Wformat-diag checking (when building with a
+   sufficiently recent GCC version).  It also uses %qE, hence "tdiag".  */
+#if __GNUC__ >= 10
+#define GCC_DIAG_STYLE __gcc_tdiag_raw__
+#endif
+
 #include "config.h"
 #include "system.h"
 #include "coretypes.h"
 #include "tree.h"
+#include "options.h"
 #include "diagnostic.h"
 #include "tree-pretty-print.h"
 #include "gimple-pretty-print.h"
@@ -47,8 +55,9 @@ namespace {
 class path_label : public range_label
 {
  public:
-  path_label (const diagnostic_path *path, unsigned start_idx)
-  : m_path (path), m_start_idx (start_idx)
+  path_label (const diagnostic_path *path, unsigned start_idx,
+	      bool colorize)
+    : m_path (path), m_start_idx (start_idx), m_colorize (colorize)
   {}
 
   label_text get_text (unsigned range_idx) const FINAL OVERRIDE
@@ -59,11 +68,10 @@ class path_label : public range_label
     /* Get the description of the event, perhaps with colorization:
        normally, we don't colorize within a range_label, but this
        is special-cased for diagnostic paths.  */
-    bool colorize = pp_show_color (global_dc->printer);
-    label_text event_text (event.get_desc (colorize));
+    label_text event_text (event.get_desc (m_colorize));
     gcc_assert (event_text.m_buffer);
     pretty_printer pp;
-    pp_show_color (&pp) = pp_show_color (global_dc->printer);
+    pp_show_color (&pp) = m_colorize;
     diagnostic_event_id_t event_id (event_idx);
     pp_printf (&pp, "%@ %s", &event_id, event_text.m_buffer);
     event_text.maybe_free ();
@@ -74,6 +82,7 @@ class path_label : public range_label
  private:
   const diagnostic_path *m_path;
   unsigned m_start_idx;
+  bool m_colorize;
 };
 
 /* Return true if E1 and E2 can be consolidated into the same run of events
@@ -122,13 +131,14 @@ class path_summary
   struct event_range
   {
     event_range (const diagnostic_path *path, unsigned start_idx,
-		 const diagnostic_event &initial_event)
+		 const diagnostic_event &initial_event,
+		 bool colorize)
     : m_path (path),
       m_initial_event (initial_event),
       m_fndecl (initial_event.get_fndecl ()),
       m_stack_depth (initial_event.get_stack_depth ()),
       m_start_idx (start_idx), m_end_idx (start_idx),
-      m_path_label (path, start_idx),
+      m_path_label (path, start_idx, colorize),
       m_richloc (initial_event.get_location (), &m_path_label)
     {}
 
@@ -198,6 +208,39 @@ class path_summary
 	}
     }
 
+    void print_as_html (diagnostic_context *dc, pretty_printer *pp,
+			html_writer *writer)
+    {
+      if (!diagnostic_show_locus_as_html (dc, pp, writer, &m_richloc,
+					  DK_DIAGNOSTIC_PATH))
+	{
+	  /* If nothing was printed, then print any events directly.  */
+	  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);
+	      pp_printf (pp, "<div class=\"no-locus-event\">%@: ", &event_id);
+	      pp_write_text_to_stream (pp);
+	      if (writer)
+		writer->begin_label (pp);
+	      label_text event_text (iter_event.get_desc (true));
+	      pp_string (pp, event_text.m_buffer);
+	      event_text.maybe_free ();
+	      pp_write_text_as_html_to_stream (pp);
+	      if (writer)
+		writer->end_label (pp);
+	      pp_string (pp, "</div>");
+	      pp_newline (pp);
+	      pp_write_text_to_stream (pp);
+	    }
+	}
+    }
+
+    const char *get_filename () const
+    {
+      return LOCATION_FILE (m_initial_event.get_location ());
+    }
+
     const diagnostic_path *m_path;
     const diagnostic_event &m_initial_event;
     tree m_fndecl;
@@ -209,9 +252,12 @@ class path_summary
   };
 
  public:
-  path_summary (const diagnostic_path &path, bool check_rich_locations);
+  path_summary (const diagnostic_path &path, bool check_rich_locations,
+		bool colorize);
 
   void print (diagnostic_context *dc, bool show_depths) const;
+  void print_as_html (diagnostic_context *dc, pretty_printer *pp,
+		      bool show_depths) const;
 
   unsigned get_num_ranges () const { return m_ranges.length (); }
 
@@ -222,7 +268,8 @@ class path_summary
 /* path_summary's ctor.  */
 
 path_summary::path_summary (const diagnostic_path &path,
-			    bool check_rich_locations)
+			    bool check_rich_locations,
+			    bool colorize)
 {
   const unsigned num_events = path.num_events ();
 
@@ -234,7 +281,7 @@ path_summary::path_summary (const diagnostic_path &path,
 	if (cur_event_range->maybe_add_event (event, idx, check_rich_locations))
 	  continue;
 
-      cur_event_range = new event_range (&path, idx, event);
+      cur_event_range = new event_range (&path, idx, event, colorize);
       m_ranges.safe_push (cur_event_range);
     }
 }
@@ -441,13 +488,263 @@ path_summary::print (diagnostic_context *dc, bool show_depths) const
     }
 }
 
+/* Custom subclass of html_writer.
+   Wrap labels within a <span> element, supplying them with event IDs.  */
+
+class html_path_writer : public html_writer
+{
+public:
+  html_path_writer () : m_events_seen (0)
+  {
+  }
+
+  void begin_label (pretty_printer *pp) FINAL OVERRIDE
+  {
+    pp_printf (pp,
+	       "<span class=\"event\" id=\"event-%i\">",
+	       ++m_events_seen);
+    pp_write_text_to_stream (pp);
+  }
+
+  void end_label (pretty_printer *pp) FINAL OVERRIDE
+  {
+    pp_write_text_as_html_to_stream (pp);
+    pp_string (pp, "</span>");
+    pp_write_text_to_stream (pp);
+  }
+
+private:
+  int m_events_seen;
+};
+
+/* Print the path summary in HTML form to PP.  */
+
+void
+path_summary::print_as_html (diagnostic_context *dc,
+			     pretty_printer *pp,
+			     bool show_depths) const
+{
+  html_path_writer writer;
+
+  pp_string (pp, "<div class=\"event-ranges\">\n\n");
+
+  unsigned i;
+  event_range *range;
+  int min_depth = INT_MAX;
+  FOR_EACH_VEC_ELT (m_ranges, i, range)
+    if (range->m_stack_depth < min_depth)
+      min_depth = range->m_stack_depth;
+
+  /* Print the ranges.  */
+  FOR_EACH_VEC_ELT (m_ranges, i, range)
+    {
+      const int pixels_per_depth = 100;
+
+      if (i > 0)
+	{
+	  event_range *last_range = m_ranges[i - 1];
+
+	  const int this_depth = range->m_stack_depth;
+	  const int last_depth = last_range->m_stack_depth;
+	  if (this_depth != last_depth)
+	    {
+	      const int base_x = 20;
+	      const int excess = 30;
+	      const int last_x
+		= base_x + (last_depth - min_depth) * pixels_per_depth;
+	      const int this_x
+		= base_x + (this_depth - min_depth) * pixels_per_depth;
+	      pp_printf (pp, "<div class=\"%s\">\n",
+			 last_depth < this_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\">\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:black\"\n"
+		 "              marker-end=\"url(#arrowhead)\"/>\n"
+		 "  </svg>\n"
+		 "</div>\n\n");
+	    }
+
+	  const char *last_file = last_range->get_filename ();
+	  const char *this_file = range->get_filename ();
+	  if (last_file && this_file && strcmp (last_file, this_file))
+	    {
+	      pp_printf (pp, "change of file to %qs\n", this_file);
+	    }
+	}
+
+      pp_string (pp, "<table class=\"event-range-with-margin\"><tr>\n");
+
+      /* Indent to show stack depth.  */
+      pp_printf (pp,
+		 ("  <td class=\"interprocmargin\""
+		  " style=\"padding-left: %ipx\"/>\n"),
+		 (range->m_stack_depth - min_depth) * pixels_per_depth);
+      pp_string (pp, "  <td class=\"event-range\">\n");
+      pp_string (pp, "    <div class=\"events-hdr\">");
+      if (range->m_fndecl)
+	{
+	  pp_string (pp, "<span class=\"funcname\">");
+	  pp_write_text_to_stream (pp);
+	  print_fndecl (pp, range->m_fndecl, true);
+	  pp_write_text_as_html_to_stream (pp);
+	  pp_string (pp, "</span>: ");
+	}
+      pp_string (pp, "<span class=\"event-ids\">");
+      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);
+      pp_string (pp, "</span>");
+      if (show_depths)
+	pp_printf (pp, " <span class=\"depth\">(depth %i)</span>",
+		   range->m_stack_depth);
+      pp_string (pp, "</div>");
+      pp_newline (pp);
+
+      /* Print a run of events.  */
+      range->print_as_html (dc, pp, &writer);
+
+      pp_string (pp, "</td></tr></table>\n");
+    }
+
+  pp_string (pp, "\n</div>\n\n");
+}
+
+/* Style information for writing out HTML paths.
+   Colors taken from https://www.patternfly.org/v3/styles/color-palette/ */
+
+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"
+     "    .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"
+     "  </style>\n");
+
+/* JavaScript for writing out HTML paths.  */
+
+const char * const HTML_SCRIPT
+  = ("<script>\n"
+     "  var current_event = 1;\n"
+     "\n"
+     "  function get_event_span (event_id)\n"
+     "  {\n"
+     "      const element_id = 'event-' + event_id;\n"
+     "      return document.getElementById(element_id);\n"
+     "  }\n"
+     "  function unhighlight_current_event ()\n"
+     "  {\n"
+     "      get_event_span (current_event).classList.remove ('selected');\n"
+     "  }\n"
+     "  function highlight_current_event ()\n"
+     "  {\n"
+     "      const el = get_event_span (current_event);\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_event ()\n"
+     "  {\n"
+     "      unhighlight_current_event ();\n"
+     "      if (current_event > 1)\n"
+     "          current_event -= 1;\n"
+     "      else\n"
+     "          current_event = max_event;\n"
+     "      highlight_current_event ();\n"
+     "  }\n"
+     "  function select_next_event ()\n"
+     "  {\n"
+     "      unhighlight_current_event ();\n"
+     "      if (current_event < max_event)\n"
+     "          current_event += 1;\n"
+     "      else\n"
+     "          current_event = 1;\n"
+     "      highlight_current_event ();\n"
+     "  }\n"
+     "  document.addEventListener('keydown', function (ev) {\n"
+     "      if (ev.key == 'j')\n"
+     "          select_prev_event ();\n"
+     "      else if (ev.key == 'k')\n"
+     "          select_next_event ();\n"
+     "  });\n"
+     "  highlight_current_event ();\n"
+     "</script>\n");
+
+/* Write PATH to PP in HTML form.  */
+
+static void
+write_html_for_path (diagnostic_context *context,
+		     const diagnostic_path *path,
+		     pretty_printer *pp)
+{
+  pp_string (pp, "<html>\n");
+  pp_string (pp, "<head>\n");
+  pp_string (pp, HTML_STYLE);
+  pp_string (pp, "</head>\n");
+  pp_string (pp, "<body>\n");
+  pp_string (pp, "<h1>Bug path</h1>\n\n");
+
+  /* Consolidate related events.  */
+  path_summary summary (*path, true, false);
+  summary.print_as_html (context, pp, context->show_path_depths);
+
+  pp_printf (pp, "<script>max_event=%i;</script>\n",
+	     path->num_events ());
+  pp_string (pp, HTML_SCRIPT);
+  pp_string (pp, "</body>\n</html>\n");
+}
+
 } /* end of anonymous namespace for path-printing code.  */
 
-/* Print PATH to CONTEXT, according to CONTEXT's path_format.  */
+/* Print PATH to CONTEXT, according to CONTEXT's path_format,
+   for a diagnostic at LOC.  */
 
 void
 default_tree_diagnostic_path_printer (diagnostic_context *context,
-				      const diagnostic_path *path)
+				      const diagnostic_path *path,
+				      location_t loc)
 {
   gcc_assert (path);
 
@@ -478,13 +775,48 @@ default_tree_diagnostic_path_printer (diagnostic_context *context,
     case DPF_INLINE_EVENTS:
       {
 	/* Consolidate related events.  */
-	path_summary summary (*path, true);
+	path_summary summary (*path, true, pp_show_color (context->printer));
 	char *saved_prefix = pp_take_prefix (context->printer);
 	pp_set_prefix (context->printer, NULL);
 	summary.print (context, context->show_path_depths);
 	pp_flush (context->printer);
 	pp_set_prefix (context->printer, saved_prefix);
       }
+      break;
+
+    case DPF_HTML:
+      {
+	const int path_id = ++context->num_html_paths;
+
+	/* Generate a filename of the form
+	   "DUMP_BASE_NAME.path-[0-9]+.html".  */
+	char id_buf[100];
+	snprintf (id_buf, sizeof (id_buf), ".path-%d", path_id);
+	char *html_filename = concat (dump_base_name, id_buf, ".html", NULL);
+
+	FILE *html_outf = fopen (html_filename, "w");
+	if (!html_outf)
+	  {
+	    fnotice (stderr, "Could not open output file '%s'\n",
+		     html_filename);
+	    free (html_filename);
+	    return;
+	  }
+
+	pretty_printer pp;
+	pp_buffer (&pp)->stream = html_outf;
+	write_html_for_path (context, path, &pp);
+	pp_flush (&pp);
+
+	fclose (html_outf);
+	unsigned num_events = path->num_events ();
+	inform_n (loc, num_events,
+		  "path with %i event written to %qs",
+		  "path with %i events written to %qs",
+		  num_events, html_filename);
+	free (html_filename);
+      }
+      break;
     }
 }
 
@@ -565,7 +897,7 @@ test_empty_path (pretty_printer *event_pp)
   test_diagnostic_path path (event_pp);
   ASSERT_FALSE (path.interprocedural_p ());
 
-  path_summary summary (path, false);
+  path_summary summary (path, false, false);
   ASSERT_EQ (summary.get_num_ranges (), 0);
 
   test_diagnostic_context dc;
@@ -589,7 +921,7 @@ test_intraprocedural_path (pretty_printer *event_pp)
 
   ASSERT_FALSE (path.interprocedural_p ());
 
-  path_summary summary (path, false);
+  path_summary summary (path, false, false);
   ASSERT_EQ (summary.get_num_ranges (), 1);
 
   test_diagnostic_context dc;
@@ -638,7 +970,7 @@ test_interprocedural_path_1 (pretty_printer *event_pp)
 
   ASSERT_TRUE (path.interprocedural_p ());
 
-  path_summary summary (path, false);
+  path_summary summary (path, false, false);
   ASSERT_EQ (summary.get_num_ranges (), 9);
 
   test_diagnostic_context dc;
@@ -720,7 +1052,7 @@ test_interprocedural_path_2 (pretty_printer *event_pp)
 
   ASSERT_TRUE (path.interprocedural_p ());
 
-  path_summary summary (path, false);
+  path_summary summary (path, false, false);
   ASSERT_EQ (summary.get_num_ranges (), 5);
 
   test_diagnostic_context dc;
@@ -772,7 +1104,7 @@ test_recursion (pretty_printer *event_pp)
 
   ASSERT_TRUE (path.interprocedural_p ());
 
-  path_summary summary (path, false);
+  path_summary summary (path, false, false);
   ASSERT_EQ (summary.get_num_ranges (), 4);
 
   test_diagnostic_context dc;
diff --git a/gcc/tree-diagnostic.h b/gcc/tree-diagnostic.h
index 40dc9fa0e83..e4207120b68 100644
--- a/gcc/tree-diagnostic.h
+++ b/gcc/tree-diagnostic.h
@@ -58,7 +58,8 @@ bool default_tree_printer (pretty_printer *, text_info *, const char *,
 			   int, bool, bool, bool, bool *, const char **);
 
 extern void default_tree_diagnostic_path_printer (diagnostic_context *,
-						  const diagnostic_path *);
+						  const diagnostic_path *,
+						  location_t);
 extern json::value *default_tree_make_json_for_path (diagnostic_context *,
 						     const diagnostic_path *);
 
-- 
2.26.2

