[analyzer] Separate the HTML parser utility from the DOM

With the parser code in the same library as the DOM classes, the
`pkg/analyzer/tool/messages/generate.dart` program, which generates all
of the diagnostic classes and diagnostics, _depends_ on a library
with a `parse` method which depends on these diagnostic classes (in
order to report errors while parsing HTML). This means that if
there is any existing error (like an unknown identifier) in the
existing error codes, it is impossible to generate the error codes.

Since we don't need the `parse` method to generate diagnostics, we
split up the library and remove the indirect dependency.

Separating the parser code out removes the loop.

Change-Id: Ifb9e9fd979e341ae64a1ed49aaf3758d3dcf1dee
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/287220
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/tool/spec/from_html.dart b/pkg/analysis_server/tool/spec/from_html.dart
index 5dee0bf..856d74b 100644
--- a/pkg/analysis_server/tool/spec/from_html.dart
+++ b/pkg/analysis_server/tool/spec/from_html.dart
@@ -7,6 +7,7 @@
 
 import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/html_generator.dart';
+import 'package:analyzer_utilities/html_parser.dart' as parser;
 import 'package:path/path.dart';
 
 import 'api.dart';
@@ -313,7 +314,7 @@
   Api readApi() {
     var file = File(filePath);
     var htmlContents = file.readAsStringSync();
-    var document = dom.parse(htmlContents, file.uri);
+    var document = parser.parse(htmlContents, file.uri);
     var htmlElement = document.children
         .singleWhere((element) => element.name.toLowerCase() == 'html');
     return apiFromHtml(htmlElement);
diff --git a/pkg/analyzer_plugin/tool/spec/from_html.dart b/pkg/analyzer_plugin/tool/spec/from_html.dart
index 11a80fe..6e9bb32 100644
--- a/pkg/analyzer_plugin/tool/spec/from_html.dart
+++ b/pkg/analyzer_plugin/tool/spec/from_html.dart
@@ -7,6 +7,7 @@
 
 import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/html_generator.dart';
+import 'package:analyzer_utilities/html_parser.dart' as parser;
 import 'package:path/path.dart';
 
 import 'api.dart';
@@ -292,7 +293,7 @@
   Api readApi() {
     var file = File(filePath);
     var htmlContents = file.readAsStringSync();
-    var document = dom.parse(htmlContents, file.uri);
+    var document = parser.parse(htmlContents, file.uri);
     var htmlElement = document.children
         .singleWhere((element) => element.name.toLowerCase() == 'html');
     return apiFromHtml(htmlElement);
diff --git a/pkg/analyzer_utilities/lib/html_dom.dart b/pkg/analyzer_utilities/lib/html_dom.dart
index f08d986..87cd410 100644
--- a/pkg/analyzer_utilities/lib/html_dom.dart
+++ b/pkg/analyzer_utilities/lib/html_dom.dart
@@ -2,13 +2,10 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-/// A lightweight html parser and DOM model.
+/// A lightweight DOM model.
 
 import 'dart:convert';
 
-// ignore: implementation_imports
-import 'package:analyzer/src/manifest/manifest_validator.dart';
-
 const _htmlEscape = HtmlEscape(HtmlEscapeMode.element);
 
 abstract class Node {
@@ -109,71 +106,3 @@
     return buf.toString();
   }
 }
-
-/// Given HTML text, return a parsed HTML tree.
-Document parse(String htmlContents, Uri uri) {
-  final RegExp commentRegex = RegExp(r'<!--[^>]+-->');
-
-  Element createElement(XmlElement xmlElement) {
-    // element
-    var element = Element.tag(xmlElement.name);
-
-    // attributes
-    for (var key in xmlElement.attributes.keys) {
-      element.attributes[key] = xmlElement.attributes[key]!.value;
-    }
-
-    // From the immediate children, determine where the text between the tags is
-    // report any such non empty text as Text nodes.
-    var text = xmlElement.sourceSpan?.text ?? '';
-
-    if (!text.endsWith('/>')) {
-      var indices = <int>[];
-      var offset = xmlElement.sourceSpan!.start.offset;
-
-      indices.add(text.indexOf('>') + 1);
-      for (var child in xmlElement.children) {
-        var childSpan = child.sourceSpan!;
-        indices.add(childSpan.start.offset - offset);
-        indices.add(childSpan.end.offset - offset);
-      }
-      indices.add(text.lastIndexOf('<'));
-
-      var textNodes = <Text>[];
-      for (var index = 0; index < indices.length; index += 2) {
-        var start = indices[index];
-        var end = indices[index + 1];
-        // Remove html comments (<!--  -->) from text.
-        textNodes.add(
-          Text(text.substring(start, end).replaceAll(commentRegex, '')),
-        );
-      }
-
-      element.append(textNodes.removeAt(0));
-
-      for (var child in xmlElement.children) {
-        element.append(createElement(child));
-        element.append(textNodes.removeAt(0));
-      }
-
-      element.nodes.removeWhere((node) => node is Text && node.text.isEmpty);
-    }
-
-    return element;
-  }
-
-  var parser = ManifestParser.general(htmlContents, uri: uri);
-  var result = parser.parseXmlTag();
-
-  while (result.parseResult != ParseTagResult.eof.parseResult) {
-    if (result.element != null) {
-      var document = Document();
-      document.append(createElement(result.element!));
-      return document;
-    }
-
-    result = parser.parseXmlTag();
-  }
-
-  throw 'parse error - element not found';
-}
diff --git a/pkg/analyzer_utilities/lib/html_parser.dart b/pkg/analyzer_utilities/lib/html_parser.dart
new file mode 100644
index 0000000..7847c3c
--- /dev/null
+++ b/pkg/analyzer_utilities/lib/html_parser.dart
@@ -0,0 +1,78 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A lightweight HTML parser.
+
+// ignore: implementation_imports
+import 'package:analyzer/src/manifest/manifest_validator.dart';
+
+import 'html_dom.dart';
+
+/// Given HTML text, return a parsed HTML tree.
+Document parse(String htmlContents, Uri uri) {
+  final RegExp commentRegex = RegExp(r'<!--[^>]+-->');
+
+  Element createElement(XmlElement xmlElement) {
+    // element
+    var element = Element.tag(xmlElement.name);
+
+    // attributes
+    for (var key in xmlElement.attributes.keys) {
+      element.attributes[key] = xmlElement.attributes[key]!.value;
+    }
+
+    // From the immediate children, determine where the text between the tags is
+    // report any such non empty text as Text nodes.
+    var text = xmlElement.sourceSpan?.text ?? '';
+
+    if (!text.endsWith('/>')) {
+      var indices = <int>[];
+      var offset = xmlElement.sourceSpan!.start.offset;
+
+      indices.add(text.indexOf('>') + 1);
+      for (var child in xmlElement.children) {
+        var childSpan = child.sourceSpan!;
+        indices.add(childSpan.start.offset - offset);
+        indices.add(childSpan.end.offset - offset);
+      }
+      indices.add(text.lastIndexOf('<'));
+
+      var textNodes = <Text>[];
+      for (var index = 0; index < indices.length; index += 2) {
+        var start = indices[index];
+        var end = indices[index + 1];
+        // Remove html comments (<!--  -->) from text.
+        textNodes.add(
+          Text(text.substring(start, end).replaceAll(commentRegex, '')),
+        );
+      }
+
+      element.append(textNodes.removeAt(0));
+
+      for (var child in xmlElement.children) {
+        element.append(createElement(child));
+        element.append(textNodes.removeAt(0));
+      }
+
+      element.nodes.removeWhere((node) => node is Text && node.text.isEmpty);
+    }
+
+    return element;
+  }
+
+  var parser = ManifestParser.general(htmlContents, uri: uri);
+  var result = parser.parseXmlTag();
+
+  while (result.parseResult != ParseTagResult.eof.parseResult) {
+    if (result.element != null) {
+      var document = Document();
+      document.append(createElement(result.element!));
+      return document;
+    }
+
+    result = parser.parseXmlTag();
+  }
+
+  throw 'parse error - element not found';
+}