Add a lightweight parser for the analyzer spec (remove sdk deps on package:html).

Change-Id: Ia8776c8aea845a7dde089d5ac9f3dab82e984f4f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/249721
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Devon Carew <devoncarew@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/doc/api.html b/pkg/analysis_server/doc/api.html
index 8571ef7..b63c3f2 100644
--- a/pkg/analysis_server/doc/api.html
+++ b/pkg/analysis_server/doc/api.html
@@ -1,7 +1,8 @@
-<!DOCTYPE html><html><head>
+<!DOCTYPE html><html>
+<head>
   <meta charset="UTF-8">
   <title>Analysis Server API Specification</title>
-<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;700&amp;family=Roboto:ital,wght@0,300;0,400;0,700;1,400&amp;display=swap" type="text/css"><style>body {
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;700&family=Roboto:ital,wght@0,300;0,400;0,700;1,400&display=swap" type="text/css"><style>body {
   font-family: 'Roboto', sans-serif;
   max-width: 800px;
   margin: 0 auto;
@@ -6361,6 +6362,5 @@
 </p>
 <h2 class="domain"><a name="index">Index</a></h2>
 <h3>Domains</h3><h4>server (<a href="#domain_server">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_server.getVersion">getVersion</a></li><li><a href="#request_server.shutdown">shutdown</a></li><li><a href="#request_server.setSubscriptions">setSubscriptions</a></li><li><a href="#request_server.cancelRequest">cancelRequest</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_server.connected">connected</a></li><li><a href="#notification_server.error">error</a></li><li><a href="#notification_server.log">log</a></li><li><a href="#notification_server.status">status</a></li></ul></div></div><h4>analysis (<a href="#domain_analysis">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_analysis.getErrors">getErrors</a></li><li><a href="#request_analysis.getHover">getHover</a></li><li><a href="#request_analysis.getLibraryDependencies">getLibraryDependencies</a></li><li><a href="#request_analysis.getNavigation">getNavigation</a></li><li><a href="#request_analysis.getReachableSources">getReachableSources</a></li><li><a href="#request_analysis.reanalyze">reanalyze</a></li><li><a href="#request_analysis.setAnalysisRoots">setAnalysisRoots</a></li><li><a href="#request_analysis.setGeneralSubscriptions">setGeneralSubscriptions</a></li><li><a href="#request_analysis.setPriorityFiles">setPriorityFiles</a></li><li><a href="#request_analysis.setSubscriptions">setSubscriptions</a></li><li><a href="#request_analysis.updateContent">updateContent</a></li><li><a href="#request_analysis.updateOptions">updateOptions</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_analysis.analyzedFiles">analyzedFiles</a></li><li><a href="#notification_analysis.closingLabels">closingLabels</a></li><li><a href="#notification_analysis.errors">errors</a></li><li><a href="#notification_analysis.flushResults">flushResults</a></li><li><a href="#notification_analysis.folding">folding</a></li><li><a href="#notification_analysis.highlights">highlights</a></li><li><a href="#notification_analysis.implemented">implemented</a></li><li><a href="#notification_analysis.invalidate">invalidate</a></li><li><a href="#notification_analysis.navigation">navigation</a></li><li><a href="#notification_analysis.occurrences">occurrences</a></li><li><a href="#notification_analysis.outline">outline</a></li><li><a href="#notification_analysis.overrides">overrides</a></li></ul></div></div><h4>completion (<a href="#domain_completion">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_completion.getSuggestions">getSuggestions</a></li><li><a href="#request_completion.getSuggestions2">getSuggestions2</a></li><li><a href="#request_completion.setSubscriptions">setSubscriptions</a></li><li><a href="#request_completion.registerLibraryPaths">registerLibraryPaths</a></li><li><a href="#request_completion.getSuggestionDetails">getSuggestionDetails</a></li><li><a href="#request_completion.getSuggestionDetails2">getSuggestionDetails2</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_completion.results">results</a></li><li><a href="#notification_completion.availableSuggestions">availableSuggestions</a></li><li><a href="#notification_completion.existingImports">existingImports</a></li></ul></div></div><h4>search (<a href="#domain_search">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_search.findElementReferences">findElementReferences</a></li><li><a href="#request_search.findMemberDeclarations">findMemberDeclarations</a></li><li><a href="#request_search.findMemberReferences">findMemberReferences</a></li><li><a href="#request_search.findTopLevelDeclarations">findTopLevelDeclarations</a></li><li><a href="#request_search.getTypeHierarchy">getTypeHierarchy</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_search.results">results</a></li></ul></div></div><h4>edit (<a href="#domain_edit">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_edit.format">format</a></li><li><a href="#request_edit.getAssists">getAssists</a></li><li><a href="#request_edit.getAvailableRefactorings">getAvailableRefactorings</a></li><li><a href="#request_edit.getFixes">getFixes</a></li><li><a href="#request_edit.getPostfixCompletion">getPostfixCompletion</a></li><li><a href="#request_edit.getRefactoring">getRefactoring</a></li><li><a href="#request_edit.sortMembers">sortMembers</a></li><li><a href="#request_edit.organizeDirectives">organizeDirectives</a></li></ul></div><h4>execution (<a href="#domain_execution">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_execution.createContext">createContext</a></li><li><a href="#request_execution.deleteContext">deleteContext</a></li><li><a href="#request_execution.getSuggestions">getSuggestions</a></li><li><a href="#request_execution.mapUri">mapUri</a></li><li><a href="#request_execution.setSubscriptions">setSubscriptions</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_execution.launchData">launchData</a></li></ul></div></div><h4>diagnostic (<a href="#domain_diagnostic">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_diagnostic.getDiagnostics">getDiagnostics</a></li><li><a href="#request_diagnostic.getServerPort">getServerPort</a></li></ul></div><h4>flutter (<a href="#domain_flutter">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_flutter.setSubscriptions">setSubscriptions</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_flutter.outline">outline</a></li></ul></div></div><h3>Types (<a href="#types">↑</a>)</h3><div class="subindex"><ul><li><a href="#type_AddContentOverlay">AddContentOverlay</a></li><li><a href="#type_AnalysisError">AnalysisError</a></li><li><a href="#type_AnalysisErrorFixes">AnalysisErrorFixes</a></li><li><a href="#type_AnalysisErrorSeverity">AnalysisErrorSeverity</a></li><li><a href="#type_AnalysisErrorType">AnalysisErrorType</a></li><li><a href="#type_AnalysisOptions">AnalysisOptions</a></li><li><a href="#type_AnalysisService">AnalysisService</a></li><li><a href="#type_AnalysisStatus">AnalysisStatus</a></li><li><a href="#type_AvailableSuggestion">AvailableSuggestion</a></li><li><a href="#type_AvailableSuggestionRelevanceTag">AvailableSuggestionRelevanceTag</a></li><li><a href="#type_AvailableSuggestionSet">AvailableSuggestionSet</a></li><li><a href="#type_BulkFix">BulkFix</a></li><li><a href="#type_BulkFixDetail">BulkFixDetail</a></li><li><a href="#type_ChangeContentOverlay">ChangeContentOverlay</a></li><li><a href="#type_ClosingLabel">ClosingLabel</a></li><li><a href="#type_CompletionCaseMatchingMode">CompletionCaseMatchingMode</a></li><li><a href="#type_CompletionId">CompletionId</a></li><li><a href="#type_CompletionMode">CompletionMode</a></li><li><a href="#type_CompletionService">CompletionService</a></li><li><a href="#type_CompletionSuggestion">CompletionSuggestion</a></li><li><a href="#type_CompletionSuggestionKind">CompletionSuggestionKind</a></li><li><a href="#type_ContextData">ContextData</a></li><li><a href="#type_DiagnosticMessage">DiagnosticMessage</a></li><li><a href="#type_Element">Element</a></li><li><a href="#type_ElementDeclaration">ElementDeclaration</a></li><li><a href="#type_ElementKind">ElementKind</a></li><li><a href="#type_ExecutableFile">ExecutableFile</a></li><li><a href="#type_ExecutableKind">ExecutableKind</a></li><li><a href="#type_ExecutionContextId">ExecutionContextId</a></li><li><a href="#type_ExecutionService">ExecutionService</a></li><li><a href="#type_ExistingImport">ExistingImport</a></li><li><a href="#type_ExistingImports">ExistingImports</a></li><li><a href="#type_FileKind">FileKind</a></li><li><a href="#type_FilePath">FilePath</a></li><li><a href="#type_FlutterOutline">FlutterOutline</a></li><li><a href="#type_FlutterOutlineAttribute">FlutterOutlineAttribute</a></li><li><a href="#type_FlutterOutlineKind">FlutterOutlineKind</a></li><li><a href="#type_FlutterService">FlutterService</a></li><li><a href="#type_FlutterWidgetProperty">FlutterWidgetProperty</a></li><li><a href="#type_FlutterWidgetPropertyEditor">FlutterWidgetPropertyEditor</a></li><li><a href="#type_FlutterWidgetPropertyEditorKind">FlutterWidgetPropertyEditorKind</a></li><li><a href="#type_FlutterWidgetPropertyValue">FlutterWidgetPropertyValue</a></li><li><a href="#type_FlutterWidgetPropertyValueEnumItem">FlutterWidgetPropertyValueEnumItem</a></li><li><a href="#type_FoldingKind">FoldingKind</a></li><li><a href="#type_FoldingRegion">FoldingRegion</a></li><li><a href="#type_GeneralAnalysisService">GeneralAnalysisService</a></li><li><a href="#type_HighlightRegion">HighlightRegion</a></li><li><a href="#type_HighlightRegionType">HighlightRegionType</a></li><li><a href="#type_HoverInformation">HoverInformation</a></li><li><a href="#type_ImplementedClass">ImplementedClass</a></li><li><a href="#type_ImplementedMember">ImplementedMember</a></li><li><a href="#type_ImportedElementSet">ImportedElementSet</a></li><li><a href="#type_ImportedElements">ImportedElements</a></li><li><a href="#type_IncludedSuggestionRelevanceTag">IncludedSuggestionRelevanceTag</a></li><li><a href="#type_IncludedSuggestionSet">IncludedSuggestionSet</a></li><li><a href="#type_KytheEntry">KytheEntry</a></li><li><a href="#type_KytheVName">KytheVName</a></li><li><a href="#type_LibraryPathSet">LibraryPathSet</a></li><li><a href="#type_LinkedEditGroup">LinkedEditGroup</a></li><li><a href="#type_LinkedEditSuggestion">LinkedEditSuggestion</a></li><li><a href="#type_LinkedEditSuggestionKind">LinkedEditSuggestionKind</a></li><li><a href="#type_Location">Location</a></li><li><a href="#type_NavigationRegion">NavigationRegion</a></li><li><a href="#type_NavigationTarget">NavigationTarget</a></li><li><a href="#type_Occurrences">Occurrences</a></li><li><a href="#type_Outline">Outline</a></li><li><a href="#type_OverriddenMember">OverriddenMember</a></li><li><a href="#type_Override">Override</a></li><li><a href="#type_Position">Position</a></li><li><a href="#type_PostfixTemplateDescriptor">PostfixTemplateDescriptor</a></li><li><a href="#type_PubStatus">PubStatus</a></li><li><a href="#type_RefactoringFeedback">RefactoringFeedback</a></li><li><a href="#type_RefactoringKind">RefactoringKind</a></li><li><a href="#type_RefactoringMethodParameter">RefactoringMethodParameter</a></li><li><a href="#type_RefactoringMethodParameterKind">RefactoringMethodParameterKind</a></li><li><a href="#type_RefactoringOptions">RefactoringOptions</a></li><li><a href="#type_RefactoringProblem">RefactoringProblem</a></li><li><a href="#type_RefactoringProblemSeverity">RefactoringProblemSeverity</a></li><li><a href="#type_RemoveContentOverlay">RemoveContentOverlay</a></li><li><a href="#type_RequestError">RequestError</a></li><li><a href="#type_RequestErrorCode">RequestErrorCode</a></li><li><a href="#type_RuntimeCompletionExpression">RuntimeCompletionExpression</a></li><li><a href="#type_RuntimeCompletionExpressionType">RuntimeCompletionExpressionType</a></li><li><a href="#type_RuntimeCompletionExpressionTypeKind">RuntimeCompletionExpressionTypeKind</a></li><li><a href="#type_RuntimeCompletionVariable">RuntimeCompletionVariable</a></li><li><a href="#type_SearchId">SearchId</a></li><li><a href="#type_SearchResult">SearchResult</a></li><li><a href="#type_SearchResultKind">SearchResultKind</a></li><li><a href="#type_ServerService">ServerService</a></li><li><a href="#type_SourceChange">SourceChange</a></li><li><a href="#type_SourceEdit">SourceEdit</a></li><li><a href="#type_SourceFileEdit">SourceFileEdit</a></li><li><a href="#type_TypeHierarchyItem">TypeHierarchyItem</a></li></ul></div><h3>Refactorings (<a href="#refactorings">↑</a>)</h3><div class="subindex"><ul><li><a href="#refactoring_CONVERT_GETTER_TO_METHOD">CONVERT_GETTER_TO_METHOD</a></li><li><a href="#refactoring_CONVERT_METHOD_TO_GETTER">CONVERT_METHOD_TO_GETTER</a></li><li><a href="#refactoring_EXTRACT_LOCAL_VARIABLE">EXTRACT_LOCAL_VARIABLE</a></li><li><a href="#refactoring_EXTRACT_METHOD">EXTRACT_METHOD</a></li><li><a href="#refactoring_EXTRACT_WIDGET">EXTRACT_WIDGET</a></li><li><a href="#refactoring_INLINE_LOCAL_VARIABLE">INLINE_LOCAL_VARIABLE</a></li><li><a href="#refactoring_INLINE_METHOD">INLINE_METHOD</a></li><li><a href="#refactoring_MOVE_FILE">MOVE_FILE</a></li><li><a href="#refactoring_RENAME">RENAME</a></li></ul></div>
-
-
-</body></html>
\ No newline at end of file
+</body>
+</html>
diff --git a/pkg/analysis_server/pubspec.yaml b/pkg/analysis_server/pubspec.yaml
index 7df20d4..555ede3 100644
--- a/pkg/analysis_server/pubspec.yaml
+++ b/pkg/analysis_server/pubspec.yaml
@@ -29,7 +29,6 @@
 dev_dependencies:
   analyzer_utilities: any
   cli_util: any
-  html: any
   lints: any
   logging: any
   matcher: any
diff --git a/pkg/analysis_server/tool/spec/api.dart b/pkg/analysis_server/tool/spec/api.dart
index 45fdf69..b0f4555 100644
--- a/pkg/analysis_server/tool/spec/api.dart
+++ b/pkg/analysis_server/tool/spec/api.dart
@@ -6,7 +6,7 @@
 /// for visiting those data structures.
 import 'dart:collection';
 
-import 'package:html/dom.dart' as dom;
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 
 /// Toplevel container for the API.
 class Api extends ApiNode {
@@ -303,8 +303,7 @@
 
 /// Base class for all possible types.
 abstract class TypeDecl extends ApiNode {
-  TypeDecl(super.html,
-      {super.experimental, super.deprecated});
+  TypeDecl(super.html, {super.experimental, super.deprecated});
 
   T accept<T>(ApiVisitor<T> visitor);
 }
@@ -413,6 +412,9 @@
       bool experimental = false,
       bool deprecated = false})
       : super(html, experimental: experimental, deprecated: deprecated);
+
+  @override
+  String toString() => name;
 }
 
 /// A reference to a type which is either defined elsewhere in the API or which
diff --git a/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart b/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
index d8bf61f..1122cee 100644
--- a/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
+++ b/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
@@ -2,8 +2,9 @@
 // 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.
 
+import 'package:analyzer_utilities/html_dom.dart';
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart';
 
 import 'api.dart';
 import 'codegen_dart.dart';
@@ -24,7 +25,7 @@
 
 List<String> _generateDartDoc(Element html) => html.children
     .where((Element elem) => elem.localName == 'p')
-    .map<String>((Element elem) => elem.text.trim())
+    .map<String>((Element elem) => innerText(elem).trim())
     .toList();
 
 String _generateNotificationMethodName(String domainName, String event) =>
diff --git a/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart b/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart
index 9566809..7ee51f7 100644
--- a/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart
+++ b/pkg/analysis_server/tool/spec/codegen_dart_protocol.dart
@@ -4,8 +4,8 @@
 
 import 'dart:convert';
 
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 import 'package:path/path.dart' as path;
 
 import 'api.dart';
diff --git a/pkg/analysis_server/tool/spec/codegen_java.dart b/pkg/analysis_server/tool/spec/codegen_java.dart
index 1143012..5bfd161 100644
--- a/pkg/analysis_server/tool/spec/codegen_java.dart
+++ b/pkg/analysis_server/tool/spec/codegen_java.dart
@@ -3,8 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Tools for Java code generation.
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 
 import 'api.dart';
 import 'from_html.dart';
@@ -51,8 +51,7 @@
   /// Visitor used to produce doc comments.
   final ToHtmlVisitor toHtmlVisitor;
 
-  CodegenJavaVisitor(super.api)
-      : toHtmlVisitor = ToHtmlVisitor(api);
+  CodegenJavaVisitor(super.api) : toHtmlVisitor = ToHtmlVisitor(api);
 
   /// Create a constructor, using [callback] to create its contents.
   void constructor(String name, void Function() callback) {
diff --git a/pkg/analysis_server/tool/spec/codegen_java_types.dart b/pkg/analysis_server/tool/spec/codegen_java_types.dart
index 6431566..d8eea5c 100644
--- a/pkg/analysis_server/tool/spec/codegen_java_types.dart
+++ b/pkg/analysis_server/tool/spec/codegen_java_types.dart
@@ -3,8 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 /// Code generation for the file "AnalysisServer.java".
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 
 import 'api.dart';
 import 'codegen_java.dart';
diff --git a/pkg/analysis_server/tool/spec/from_html.dart b/pkg/analysis_server/tool/spec/from_html.dart
index d3a1ce7..0b6d2ef 100644
--- a/pkg/analysis_server/tool/spec/from_html.dart
+++ b/pkg/analysis_server/tool/spec/from_html.dart
@@ -5,9 +5,8 @@
 /// Code for reading an HTML API description.
 import 'dart:io';
 
-import 'package:analyzer_utilities/html.dart';
-import 'package:html/dom.dart' as dom;
-import 'package:html/parser.dart' as parser;
+import 'package:analyzer_utilities/html_dom.dart' as dom;
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:path/path.dart';
 
 import 'api.dart';
@@ -21,8 +20,6 @@
 
 typedef ElementProcessor = void Function(dom.Element element);
 
-typedef TextProcessor = void Function(dom.Text text);
-
 class ApiReader {
   static const List<String> specialElements = [
     'domain',
@@ -109,10 +106,6 @@
       {List<String> optionalAttributes = const []}) {
     var attributesFound = <String>{};
     element.attributes.forEach((name, value) {
-      if (name is! String) {
-        throw Exception(
-            '$context: Only string attribute names expected: $name');
-      }
       if (!requiredAttributes.contains(name) &&
           !optionalAttributes.contains(name)) {
         throw Exception(
@@ -319,8 +312,9 @@
 
   /// Read the API description from file with the given [filePath].
   Api readApi() {
-    var htmlContents = File(filePath).readAsStringSync();
-    var document = parser.parse(htmlContents);
+    var file = File(filePath);
+    var htmlContents = file.readAsStringSync();
+    var document = dom.parse(htmlContents, file.uri);
     var htmlElement = document.children
         .singleWhere((element) => element.localName!.toLowerCase() == 'html');
     return apiFromHtml(htmlElement);
diff --git a/pkg/analysis_server/tool/spec/to_html.dart b/pkg/analysis_server/tool/spec/to_html.dart
index 7d4a72eb..d5f865d 100644
--- a/pkg/analysis_server/tool/spec/to_html.dart
+++ b/pkg/analysis_server/tool/spec/to_html.dart
@@ -7,9 +7,9 @@
 /// in generated code.
 import 'dart:convert';
 
-import 'package:analyzer_utilities/html.dart';
+import 'package:analyzer_utilities/html_dom.dart' as dom;
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 
 import 'api.dart';
 import 'from_html.dart';
@@ -127,7 +127,6 @@
     GeneratedFile('doc/api.html', (String pkgPath) async {
   var visitor = ToHtmlVisitor(readApi(pkgPath));
   var document = dom.Document();
-  document.append(dom.DocumentType('html', null, null));
   for (var node in visitor.collectHtml(visitor.visitApi)) {
     document.append(node);
   }
@@ -169,7 +168,7 @@
   void dl(void Function() callback) => element('dl', {}, callback);
   void dt(String cls, void Function() callback) =>
       element('dt', {'class': cls}, callback);
-  void element(String name, Map<Object, String> attributes,
+  void element(String name, Map<String, String> attributes,
       [void Function() callback]);
   void gray(void Function() callback) =>
       element('span', {'style': 'color:#999999'}, callback);
@@ -188,7 +187,7 @@
   void i(void Function() callback) => element('i', {}, callback);
   void li(void Function() callback) => element('li', {}, callback);
   void link(String id, void Function() callback,
-      [Map<Object, String>? attributes]) {
+      [Map<String, String>? attributes]) {
     attributes ??= {};
     attributes['href'] = '#$id';
     element('a', attributes, callback);
@@ -212,8 +211,7 @@
   /// Mappings from HTML elements to API nodes.
   ApiMappings apiMappings;
 
-  ToHtmlVisitor(super.api)
-      : apiMappings = ApiMappings(api) {
+  ToHtmlVisitor(super.api) : apiMappings = ApiMappings(api) {
     apiMappings.visitApi();
   }
 
@@ -470,7 +468,7 @@
             }
         }
       } else if (node is dom.Text) {
-        var text = node.text;
+        var text = node.textRemoveTags;
         write(text);
       }
     }
diff --git a/pkg/analyzer/lib/src/manifest/manifest_validator.dart b/pkg/analyzer/lib/src/manifest/manifest_validator.dart
index ff88bff..1ceb595 100644
--- a/pkg/analyzer/lib/src/manifest/manifest_validator.dart
+++ b/pkg/analyzer/lib/src/manifest/manifest_validator.dart
@@ -34,13 +34,13 @@
 /// (https://www.w3.org/TR/xml/#sec-references).
 class ManifestParser {
   /// Elements which are relevant to manifest validation.
-  static const List<String> _relevantElements = [
+  static const Set<String> _manifestValidationElements = {
     ACTIVITY_TAG,
     APPLICATION_TAG,
     MANIFEST_TAG,
     USES_FEATURE_TAG,
     USES_PERMISSION_TAG
-  ];
+  };
 
   /// The text of the Android Manifest file.
   final String content;
@@ -49,12 +49,23 @@
   /// purposes.
   final SourceFile sourceFile;
 
+  /// The set of element types to return.
+  final Set<String>? restrictElements;
+
   /// The current offset in the source file.
   int _pos;
 
   ManifestParser(this.content, Uri uri)
       : sourceFile = SourceFile.fromString(content, url: uri),
-        _pos = 0;
+        _pos = 0,
+        restrictElements = _manifestValidationElements;
+
+  ManifestParser.general(
+    this.content, {
+    required Uri uri,
+  })  : sourceFile = SourceFile.fromString(content, url: uri),
+        _pos = 0,
+        restrictElements = null;
 
   /// Whether the current character is a tag-closing character (">").
   bool get _isClosing =>
@@ -115,7 +126,8 @@
 
   /// Returns whether [name] represents an element that is relevant to manifest
   /// validation.
-  bool _isRelevantElement(String name) => _relevantElements.contains(name);
+  bool _isRelevantElement(String name) =>
+      restrictElements?.contains(name) ?? true;
 
   /// Parses any whitespace, returning `null` when non-whitespace is parsed.
   ParseResult? _parseAnyWhitespace() {
@@ -134,7 +146,7 @@
 
   /// Parses an attribute.
   ParseAttributeResult _parseAttribute(bool isRelevant) {
-    var attributes = <String, _XmlAttribute>{};
+    var attributes = <String, XmlAttribute>{};
     bool isEmptyElement;
 
     while (true) {
@@ -221,7 +233,7 @@
         var attributeValue = content.substring(attributeValuePos, _pos);
         var sourceSpan = sourceFile.span(attributeNamePos, _pos);
         attributes[attributeName] =
-            _XmlAttribute(attributeName, attributeValue, sourceSpan);
+            XmlAttribute(attributeName, attributeValue, sourceSpan);
       }
       _pos++;
     }
@@ -323,7 +335,7 @@
       if (_isClosing) {
         // End tags cannot have attributes.
         return ParseTagResult(
-            ParseResult.endTag, _XmlElement(name, {}, [], null));
+            ParseResult.endTag, XmlElement(name, {}, [], null));
       } else {
         return ParseTagResult.error;
       }
@@ -331,7 +343,7 @@
 
     var isRelevant = _isRelevantElement(name);
 
-    Map<String, _XmlAttribute> attributes;
+    Map<String, XmlAttribute> attributes;
     bool isEmptyElement;
     if (tagClosingState == _TagClosingState.notClosed) {
       // Have not parsed the tag close yet; parse attributes.
@@ -352,7 +364,7 @@
       isEmptyElement = true;
     }
 
-    var children = <_XmlElement>[];
+    var children = <XmlElement>[];
     if (!isEmptyElement) {
       ParseTagResult child;
       _pos++;
@@ -372,9 +384,9 @@
 
     // Finished parsing start tag.
     if (isRelevant) {
-      var sourceSpan = sourceFile.span(startPos, _pos);
+      var sourceSpan = sourceFile.span(startPos, _pos + 1);
       return ParseTagResult(ParseResult.relevantElement,
-          _XmlElement(name, attributes, children, sourceSpan));
+          XmlElement(name, attributes, children, sourceSpan));
     } else {
       // Discard all parsed children. This requires the notion that all relevant
       // tags are direct children of other relevant tags.
@@ -439,17 +451,17 @@
     }
   }
 
-  bool _hasFeatureCamera(Iterable<_XmlElement> features) => features
+  bool _hasFeatureCamera(Iterable<XmlElement> features) => features
       .any((f) => f.attributes[ANDROID_NAME]?.value == HARDWARE_FEATURE_CAMERA);
 
-  bool _hasFeatureCameraAutoFocus(Iterable<_XmlElement> features) =>
+  bool _hasFeatureCameraAutoFocus(Iterable<XmlElement> features) =>
       features.any((f) =>
           f.attributes[ANDROID_NAME]?.value ==
           HARDWARE_FEATURE_CAMERA_AUTOFOCUS);
 
   /// Report an error for the given node.
-  void _reportErrorForNode(ErrorReporter reporter, _XmlElement node,
-      String? key, ErrorCode errorCode,
+  void _reportErrorForNode(
+      ErrorReporter reporter, XmlElement node, String? key, ErrorCode errorCode,
       [List<Object>? arguments]) {
     var span =
         key == null ? node.sourceSpan! : node.attributes[key]!.sourceSpan;
@@ -458,7 +470,7 @@
   }
 
   /// Validate the 'activity' tags.
-  void _validateActivity(_XmlElement activity, ErrorReporter reporter) {
+  void _validateActivity(XmlElement activity, ErrorReporter reporter) {
     var attributes = activity.attributes;
     if (attributes.containsKey(ATTRIBUTE_SCREEN_ORIENTATION)) {
       if (UNSUPPORTED_ORIENTATIONS
@@ -477,7 +489,7 @@
 
   /// Validate the `uses-feature` tags.
   void _validateFeatures(
-      Iterable<_XmlElement> features, ErrorReporter reporter) {
+      Iterable<XmlElement> features, ErrorReporter reporter) {
     var unsupported = features.where((element) => UNSUPPORTED_HARDWARE_FEATURES
         .contains(element.attributes[ANDROID_NAME]?.value));
     for (var element in unsupported) {
@@ -510,8 +522,8 @@
   }
 
   /// Validate the `uses-permission` tags.
-  void _validatePermissions(Iterable<_XmlElement> permissions,
-      Iterable<_XmlElement> features, ErrorReporter reporter) {
+  void _validatePermissions(Iterable<XmlElement> permissions,
+      Iterable<XmlElement> features, ErrorReporter reporter) {
     for (var permission in permissions) {
       if (permission.attributes[ANDROID_NAME]?.value ==
           ANDROID_PERMISSION_CAMERA) {
@@ -536,8 +548,8 @@
   }
 
   /// Validate the presence/absence of the touchscreen feature tag.
-  void _validateTouchScreenFeature(Iterable<_XmlElement> features,
-      _XmlElement manifest, ErrorReporter reporter) {
+  void _validateTouchScreenFeature(Iterable<XmlElement> features,
+      XmlElement manifest, ErrorReporter reporter) {
     var feature = features.firstWhereOrNull((element) =>
         element.attributes[ANDROID_NAME]?.value ==
         HARDWARE_FEATURE_TOUCHSCREEN);
@@ -571,7 +583,7 @@
 
   final ParseResult parseResult;
 
-  final Map<String, _XmlAttribute>? attributes;
+  final Map<String, XmlAttribute>? attributes;
 
   ParseAttributeResult(this.parseResult, this.attributes);
 }
@@ -596,18 +608,34 @@
   relevantElement,
 }
 
-@visibleForTesting
 class ParseTagResult {
   static ParseTagResult eof = ParseTagResult(ParseResult.eof, null);
   static ParseTagResult error = ParseTagResult(ParseResult.error, null);
 
   final ParseResult parseResult;
 
-  final _XmlElement? element;
+  final XmlElement? element;
 
   ParseTagResult(this.parseResult, this.element);
 }
 
+class XmlAttribute {
+  final String name;
+  final String value;
+  final SourceSpan sourceSpan;
+
+  XmlAttribute(this.name, this.value, this.sourceSpan);
+}
+
+class XmlElement {
+  final String name;
+  final Map<String, XmlAttribute> attributes;
+  final List<XmlElement> children;
+  final SourceSpan? sourceSpan;
+
+  XmlElement(this.name, this.attributes, this.children, this.sourceSpan);
+}
+
 enum _TagClosingState {
   // Represents that the tag's close has not been parsed.
   notClosed,
@@ -617,20 +645,3 @@
   // an empty element, as per https://www.w3.org/TR/xml/#sec-starttags.
   closedEmptyElement,
 }
-
-class _XmlAttribute {
-  final String name;
-  final String value;
-  final SourceSpan sourceSpan;
-
-  _XmlAttribute(this.name, this.value, this.sourceSpan);
-}
-
-class _XmlElement {
-  final String name;
-  final Map<String, _XmlAttribute> attributes;
-  final List<_XmlElement> children;
-  final SourceSpan? sourceSpan;
-
-  _XmlElement(this.name, this.attributes, this.children, this.sourceSpan);
-}
diff --git a/pkg/analyzer/test/src/manifest/manifest_validator_test.dart b/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
index a3f0cc1..c9782c9 100644
--- a/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
+++ b/pkg/analyzer/test/src/manifest/manifest_validator_test.dart
@@ -375,7 +375,7 @@
     expect(result.element!.name, MANIFEST_TAG);
     var sourceSpan = result.element!.sourceSpan!;
     expect(sourceSpan.start.offset, equals(0));
-    expect(sourceSpan.end.offset, equals(10));
+    expect(sourceSpan.end.offset, equals(11));
   }
 
   void test_relevantTag_emptyElement_whitespace_nameIsParsed() {
@@ -384,7 +384,7 @@
     expect(result.element!.name, MANIFEST_TAG);
     var sourceSpan = result.element!.sourceSpan!;
     expect(sourceSpan.start.offset, equals(0));
-    expect(sourceSpan.end.offset, equals(11));
+    expect(sourceSpan.end.offset, equals(12));
   }
 
   void test_relevantTag_withAttributes_emptyElement_nameIsParsed() {
@@ -393,7 +393,7 @@
     expect(result.element!.name, MANIFEST_TAG);
     var sourceSpan = result.element!.sourceSpan!;
     expect(sourceSpan.start.offset, equals(0));
-    expect(sourceSpan.end.offset, equals(21));
+    expect(sourceSpan.end.offset, equals(22));
   }
 
   void test_relevantTag_withAttributes_nameIsParsed() {
@@ -403,7 +403,7 @@
     expect(result.element!.name, MANIFEST_TAG);
     var sourceSpan = result.element!.sourceSpan!;
     expect(sourceSpan.start.offset, equals(0));
-    expect(sourceSpan.end.offset, equals(30));
+    expect(sourceSpan.end.offset, equals(31));
   }
 
   void test_tagBeginningWithWhitespace_isError() {
diff --git a/pkg/analyzer_plugin/doc/api.html b/pkg/analyzer_plugin/doc/api.html
index 859796d..3427f3f 100644
--- a/pkg/analyzer_plugin/doc/api.html
+++ b/pkg/analyzer_plugin/doc/api.html
@@ -1,4 +1,5 @@
-<!DOCTYPE html><html><head>
+<!DOCTYPE html><html>
+<head>
   <meta charset="UTF-8">
   <title>Analysis Server Plugin API Specification</title>
 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Roboto:500,400italic,300,400" type="text/css"><style>body {
@@ -2427,6 +2428,5 @@
       </dd></dl></dd></dl>
 <h2 class="domain"><a name="index">Index</a></h2>
 <h3>Domains</h3><h4>plugin (<a href="#domain_plugin">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_plugin.versionCheck">versionCheck</a></li><li><a href="#request_plugin.shutdown">shutdown</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_plugin.error">error</a></li></ul></div></div><h4>analysis (<a href="#domain_analysis">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_analysis.getNavigation">getNavigation</a></li><li><a href="#request_analysis.handleWatchEvents">handleWatchEvents</a></li><li><a href="#request_analysis.setContextRoots">setContextRoots</a></li><li><a href="#request_analysis.setPriorityFiles">setPriorityFiles</a></li><li><a href="#request_analysis.setSubscriptions">setSubscriptions</a></li><li><a href="#request_analysis.updateContent">updateContent</a></li></ul><h5>Notifications</h5><div class="subindex"><ul><li><a href="#notification_analysis.errors">errors</a></li><li><a href="#notification_analysis.folding">folding</a></li><li><a href="#notification_analysis.highlights">highlights</a></li><li><a href="#notification_analysis.navigation">navigation</a></li><li><a href="#notification_analysis.occurrences">occurrences</a></li><li><a href="#notification_analysis.outline">outline</a></li></ul></div></div><h4>completion (<a href="#domain_completion">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_completion.getSuggestions">getSuggestions</a></li></ul></div><h4>edit (<a href="#domain_edit">↑</a>)</h4><div class="subindex"><h5>Requests</h5><ul><li><a href="#request_edit.getAssists">getAssists</a></li><li><a href="#request_edit.getFixes">getFixes</a></li></ul></div><h3>Types (<a href="#types">↑</a>)</h3><div class="subindex"><ul><li><a href="#type_AddContentOverlay">AddContentOverlay</a></li><li><a href="#type_AnalysisError">AnalysisError</a></li><li><a href="#type_AnalysisErrorFixes">AnalysisErrorFixes</a></li><li><a href="#type_AnalysisErrorSeverity">AnalysisErrorSeverity</a></li><li><a href="#type_AnalysisErrorType">AnalysisErrorType</a></li><li><a href="#type_AnalysisService">AnalysisService</a></li><li><a href="#type_ChangeContentOverlay">ChangeContentOverlay</a></li><li><a href="#type_CompletionSuggestion">CompletionSuggestion</a></li><li><a href="#type_CompletionSuggestionKind">CompletionSuggestionKind</a></li><li><a href="#type_ContextRoot">ContextRoot</a></li><li><a href="#type_DiagnosticMessage">DiagnosticMessage</a></li><li><a href="#type_Element">Element</a></li><li><a href="#type_ElementKind">ElementKind</a></li><li><a href="#type_FilePath">FilePath</a></li><li><a href="#type_FoldingKind">FoldingKind</a></li><li><a href="#type_FoldingRegion">FoldingRegion</a></li><li><a href="#type_HighlightRegion">HighlightRegion</a></li><li><a href="#type_HighlightRegionType">HighlightRegionType</a></li><li><a href="#type_KytheEntry">KytheEntry</a></li><li><a href="#type_KytheVName">KytheVName</a></li><li><a href="#type_LinkedEditGroup">LinkedEditGroup</a></li><li><a href="#type_LinkedEditSuggestion">LinkedEditSuggestion</a></li><li><a href="#type_LinkedEditSuggestionKind">LinkedEditSuggestionKind</a></li><li><a href="#type_Location">Location</a></li><li><a href="#type_NavigationRegion">NavigationRegion</a></li><li><a href="#type_NavigationTarget">NavigationTarget</a></li><li><a href="#type_Occurrences">Occurrences</a></li><li><a href="#type_Outline">Outline</a></li><li><a href="#type_Position">Position</a></li><li><a href="#type_PrioritizedSourceChange">PrioritizedSourceChange</a></li><li><a href="#type_RefactoringKind">RefactoringKind</a></li><li><a href="#type_RefactoringMethodParameter">RefactoringMethodParameter</a></li><li><a href="#type_RefactoringMethodParameterKind">RefactoringMethodParameterKind</a></li><li><a href="#type_RefactoringProblem">RefactoringProblem</a></li><li><a href="#type_RefactoringProblemSeverity">RefactoringProblemSeverity</a></li><li><a href="#type_RemoveContentOverlay">RemoveContentOverlay</a></li><li><a href="#type_RequestError">RequestError</a></li><li><a href="#type_RequestErrorCode">RequestErrorCode</a></li><li><a href="#type_SourceChange">SourceChange</a></li><li><a href="#type_SourceEdit">SourceEdit</a></li><li><a href="#type_SourceFileEdit">SourceFileEdit</a></li><li><a href="#type_WatchEvent">WatchEvent</a></li><li><a href="#type_WatchEventType">WatchEventType</a></li></ul></div><h3>Refactorings (<a href="#refactorings">↑</a>)</h3><div class="subindex"><ul><li><a href="#refactoring_CONVERT_GETTER_TO_METHOD">CONVERT_GETTER_TO_METHOD</a></li><li><a href="#refactoring_CONVERT_METHOD_TO_GETTER">CONVERT_METHOD_TO_GETTER</a></li><li><a href="#refactoring_EXTRACT_LOCAL_VARIABLE">EXTRACT_LOCAL_VARIABLE</a></li><li><a href="#refactoring_EXTRACT_METHOD">EXTRACT_METHOD</a></li><li><a href="#refactoring_INLINE_LOCAL_VARIABLE">INLINE_LOCAL_VARIABLE</a></li><li><a href="#refactoring_INLINE_METHOD">INLINE_METHOD</a></li><li><a href="#refactoring_MOVE_FILE">MOVE_FILE</a></li><li><a href="#refactoring_RENAME">RENAME</a></li></ul></div>
-
-
-</body></html>
\ No newline at end of file
+</body>
+</html>
diff --git a/pkg/analyzer_plugin/pubspec.yaml b/pkg/analyzer_plugin/pubspec.yaml
index c3322ed..11bb975 100644
--- a/pkg/analyzer_plugin/pubspec.yaml
+++ b/pkg/analyzer_plugin/pubspec.yaml
@@ -19,7 +19,6 @@
 # See also https://dart.dev/tools/pub/dependencies.
 dev_dependencies:
   analyzer_utilities: any
-  html: any
   meta: any
   path: any
   test_reflective_loader: any
diff --git a/pkg/analyzer_plugin/tool/spec/api.dart b/pkg/analyzer_plugin/tool/spec/api.dart
index 04ef8d8..78a086b 100644
--- a/pkg/analyzer_plugin/tool/spec/api.dart
+++ b/pkg/analyzer_plugin/tool/spec/api.dart
@@ -6,7 +6,7 @@
 /// for visiting those data structures.
 import 'dart:collection';
 
-import 'package:html/dom.dart' as dom;
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 
 /// Toplevel container for the API.
 class Api extends ApiNode {
diff --git a/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart b/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
index e99476e..b968c7b 100644
--- a/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
+++ b/pkg/analyzer_plugin/tool/spec/codegen_dart_protocol.dart
@@ -4,8 +4,8 @@
 
 import 'dart:convert';
 
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 import 'package:path/path.dart' as path;
 
 import 'api.dart';
diff --git a/pkg/analyzer_plugin/tool/spec/from_html.dart b/pkg/analyzer_plugin/tool/spec/from_html.dart
index 53dc875..d5351a2 100644
--- a/pkg/analyzer_plugin/tool/spec/from_html.dart
+++ b/pkg/analyzer_plugin/tool/spec/from_html.dart
@@ -5,9 +5,8 @@
 /// Code for reading an HTML API description.
 import 'dart:io';
 
-import 'package:analyzer_utilities/html.dart';
-import 'package:html/dom.dart' as dom;
-import 'package:html/parser.dart' as parser;
+import 'package:analyzer_utilities/html_dom.dart' as dom;
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:path/path.dart';
 
 import 'api.dart';
@@ -114,7 +113,7 @@
         throw Exception(
             '$context: Unexpected attribute in ${element.localName}: $name');
       }
-      attributesFound.add(name as String);
+      attributesFound.add(name);
     });
     for (var expectedAttribute in requiredAttributes) {
       if (!attributesFound.contains(expectedAttribute)) {
@@ -292,8 +291,9 @@
 
   /// Read the API description from file with the given [filePath].
   Api readApi() {
-    var htmlContents = File(filePath).readAsStringSync();
-    var document = parser.parse(htmlContents);
+    var file = File(filePath);
+    var htmlContents = file.readAsStringSync();
+    var document = dom.parse(htmlContents, file.uri);
     var htmlElement = document.children
         .singleWhere((element) => element.localName?.toLowerCase() == 'html');
     return apiFromHtml(htmlElement);
diff --git a/pkg/analyzer_plugin/tool/spec/to_html.dart b/pkg/analyzer_plugin/tool/spec/to_html.dart
index 852a05f..84b59f8 100644
--- a/pkg/analyzer_plugin/tool/spec/to_html.dart
+++ b/pkg/analyzer_plugin/tool/spec/to_html.dart
@@ -7,9 +7,9 @@
 /// in generated code.
 import 'dart:convert';
 
-import 'package:analyzer_utilities/html.dart';
+import 'package:analyzer_utilities/html_dom.dart' as dom;
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 
 import 'api.dart';
 import 'from_html.dart';
@@ -127,7 +127,6 @@
     GeneratedFile('doc/api.html', (String pkgPath) async {
   var visitor = ToHtmlVisitor(readApi(pkgPath));
   var document = dom.Document();
-  document.append(dom.DocumentType('html', null, null));
   for (var node in visitor.collectHtml(visitor.visitApi)) {
     document.append(node);
   }
@@ -169,7 +168,7 @@
   void dl(void Function() callback) => element('dl', {}, callback);
   void dt(String cls, void Function() callback) =>
       element('dt', {'class': cls}, callback);
-  void element(String name, Map<Object, String> attributes,
+  void element(String name, Map<String, String> attributes,
       [void Function()? callback]);
   void gray(void Function() callback) =>
       element('span', {'style': 'color:#999999'}, callback);
@@ -191,7 +190,7 @@
   void i(void Function() callback) => element('i', {}, callback);
   void li(void Function() callback) => element('li', {}, callback);
   void link(String id, void Function() callback,
-      [Map<Object, String>? attributes]) {
+      [Map<String, String>? attributes]) {
     attributes ??= {};
     attributes['href'] = '#$id';
     element('a', attributes, callback);
diff --git a/pkg/analyzer_utilities/lib/html_dom.dart b/pkg/analyzer_utilities/lib/html_dom.dart
new file mode 100644
index 0000000..64fa9b6
--- /dev/null
+++ b/pkg/analyzer_utilities/lib/html_dom.dart
@@ -0,0 +1,182 @@
+// Copyright (c) 2022, 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 and DOM model.
+
+import 'dart:convert';
+
+// ignore: implementation_imports
+import 'package:analyzer/src/manifest/manifest_validator.dart';
+
+const _htmlEscape = HtmlEscape(HtmlEscapeMode.element);
+
+abstract class Node {
+  Element? parent;
+
+  final List<Node> nodes = [];
+}
+
+class Element extends Node {
+  String name;
+
+  Map<String, String> attributes = {};
+
+  List<Element> get children => nodes.whereType<Element>().toList();
+
+  Element.tag(this.name);
+
+  // This is for compatibility with the package:html DOM API.
+  String? get localName => name;
+
+  void append(Node child) {
+    child.parent = this;
+    nodes.add(child);
+  }
+
+  List<String> get classes {
+    final classes = attributes['class'];
+    if (classes != null) {
+      return classes.split(' ').toList();
+    } else {
+      return const [];
+    }
+  }
+}
+
+class Text extends Node {
+  static final RegExp tagRegex = RegExp(r'<[^>]+>');
+
+  final String text;
+
+  Text(this.text);
+
+  @override
+  List<Node> get nodes => const [];
+
+  String get textRemoveTags {
+    return text.replaceAll(tagRegex, '');
+  }
+}
+
+class Document extends Element {
+  static const Set<String> selfClosing = {
+    'br',
+    'link',
+    'meta',
+  };
+
+  Document() : super.tag('');
+
+  /// Return the full HTML text for the document.
+  String get outerHtml {
+    var buf = StringBuffer();
+
+    void emitNode(Node node) {
+      if (node is Element) {
+        buf.write('<${node.name}');
+        for (var attr in node.attributes.keys) {
+          buf.write(' $attr="${node.attributes[attr]}"');
+        }
+        if (node.nodes.length == 1 &&
+            node.nodes.first is Text &&
+            !(node.nodes.first as Text).text.contains('\n')) {
+          buf.write('>');
+          var text = node.nodes.first as Text;
+          buf.write(_htmlEscape.convert(text.text));
+          buf.write('</${node.name}>');
+        } else {
+          buf.write('>');
+          for (var child in node.nodes) {
+            emitNode(child);
+          }
+          if (!selfClosing.contains(node.name)) {
+            buf.write('</${node.name}>');
+          }
+        }
+      } else if (node is Text) {
+        buf.write(_htmlEscape.convert(node.text));
+      } else {
+        throw 'unknown node type: $node';
+      }
+    }
+
+    buf.write('<!DOCTYPE html>');
+
+    for (var child in nodes) {
+      emitNode(child);
+    }
+
+    buf.writeln();
+
+    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.dart b/pkg/analyzer_utilities/lib/html_generator.dart
similarity index 88%
rename from pkg/analyzer_utilities/lib/html.dart
rename to pkg/analyzer_utilities/lib/html_generator.dart
index c5dc4bf..c7aed6b 100644
--- a/pkg/analyzer_utilities/lib/html.dart
+++ b/pkg/analyzer_utilities/lib/html_generator.dart
@@ -4,11 +4,8 @@
 
 /// Tools for manipulating HTML during code generation of analyzer and analysis
 /// server.
-import 'package:html/dom.dart' as dom;
 
-/// Make a deep copy of the given HTML nodes.
-List<dom.Node> cloneHtmlNodes(List<dom.Node> nodes) =>
-    nodes.map((dom.Node node) => node.clone(true)).toList();
+import 'html_dom.dart' as dom;
 
 /// Return true if the given iterable contains only whitespace text nodes.
 bool containsOnlyWhitespace(Iterable<dom.Node> nodes) {
@@ -26,7 +23,7 @@
   void recurse(dom.Element parent) {
     for (var child in parent.nodes) {
       if (child is dom.Text) {
-        buffer.write(child.text);
+        buffer.write(child.textRemoveTags);
       } else if (child is dom.Element) {
         recurse(child);
       }
@@ -51,7 +48,7 @@
 
 /// Create an HTML element with the given name, attributes, and child nodes.
 dom.Element makeElement(
-    String name, Map<Object, String> attributes, List<dom.Node> children) {
+    String name, Map<String, String> attributes, List<dom.Node> children) {
   var result = dom.Element.tag(name);
   result.attributes.addAll(attributes);
   for (var child in children) {
@@ -94,7 +91,7 @@
 
   /// Execute [callback], wrapping its output in an element with the given
   /// [name] and [attributes].
-  void element(String name, Map<Object, String> attributes,
+  void element(String name, Map<String, String> attributes,
       [void Function()? callback]) {
     add(makeElement(name, attributes, collectHtml(callback)));
   }
diff --git a/pkg/analyzer_utilities/lib/text_formatter.dart b/pkg/analyzer_utilities/lib/text_formatter.dart
index 79c92d2..ce20d49 100644
--- a/pkg/analyzer_utilities/lib/text_formatter.dart
+++ b/pkg/analyzer_utilities/lib/text_formatter.dart
@@ -4,8 +4,8 @@
 
 /// Code for converting HTML into text, for use during code generation of
 /// analyzer and analysis server.
+import 'package:analyzer_utilities/html_dom.dart' as dom;
 import 'package:analyzer_utilities/tools.dart';
-import 'package:html/dom.dart' as dom;
 
 final RegExp whitespace = RegExp(r'\s');
 
@@ -83,6 +83,7 @@
           lineBreak(false);
           if (node.classes.contains('hangingIndent')) {
             resolveVerticalSpace();
+            // TODO(devoncarew): Remove a space here.
             indentSpecial('', '        ', () {
               addAll(node.nodes);
               lineBreak(false);
diff --git a/pkg/analyzer_utilities/lib/tools.dart b/pkg/analyzer_utilities/lib/tools.dart
index c1a5ca7..79f022c 100644
--- a/pkg/analyzer_utilities/lib/tools.dart
+++ b/pkg/analyzer_utilities/lib/tools.dart
@@ -6,9 +6,9 @@
 import 'dart:async';
 import 'dart:io';
 
-import 'package:analyzer_utilities/html.dart';
+import 'package:analyzer_utilities/html_dom.dart' as dom;
+import 'package:analyzer_utilities/html_generator.dart';
 import 'package:analyzer_utilities/text_formatter.dart';
-import 'package:html/dom.dart' as dom;
 import 'package:path/path.dart';
 import 'package:test/test.dart';
 
@@ -473,7 +473,7 @@
 
   /// Execute [callback], wrapping its output in an element with the given
   /// [name] and [attributes].
-  void element(String name, Map<Object, String> attributes,
+  void element(String name, Map<String, String> attributes,
       [void Function()? callback]) {
     add(makeElement(name, attributes, collectHtml(callback)));
   }
diff --git a/pkg/analyzer_utilities/pubspec.yaml b/pkg/analyzer_utilities/pubspec.yaml
index a7c634c..f803290 100644
--- a/pkg/analyzer_utilities/pubspec.yaml
+++ b/pkg/analyzer_utilities/pubspec.yaml
@@ -8,7 +8,6 @@
 # Use 'any' constraints here; we get our versions from the DEPS file.
 dependencies:
   analyzer: any
-  html: any
   meta: any
   path: any
   test: any