Remove <base> tag, use hrefs prepended with base (#2098)

* Remove <base> tag, use hrefs prepended with base

* Add tests for relative hrefs in doc comments

* Add a CLI flag (hidden) to use the legacy behavior

* Detailed comment regarding usage of the placeholder

* Fix placeholder string in header and footer partials
diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart
index 223ed30..409f876 100644
--- a/lib/src/dartdoc_options.dart
+++ b/lib/src/dartdoc_options.dart
@@ -1422,6 +1422,9 @@
       excludePackages.any((pattern) => name == pattern);
 
   String get templatesDir => optionSet['templatesDir'].valueAt(context);
+
+  // TODO(jdkoren): temporary while we confirm href base behavior doesn't break important clients
+  bool get useBaseHref => optionSet['useBaseHref'].valueAt(context);
 }
 
 /// Instantiate dartdoc's configuration file and options parser with the
@@ -1635,6 +1638,14 @@
             'property, top_level_constant, top_level_property, typedef. Partial templates are '
             'supported; they must begin with an underscore, and references to them must omit the '
             'leading underscore (e.g. use {{>foo}} to reference the partial template _foo.html).'),
+    DartdocOptionArgOnly<bool>('useBaseHref', false,
+        help:
+            'Use <base href> in generated files (legacy behavior). This option '
+            'is temporary and support will be removed in the future. Use only '
+            'if the default behavior breaks links between your documentation '
+            'pages, and please file an issue on Github.',
+        negatable: false,
+        hide: true),
     // TODO(jcollins-g): refactor so there is a single static "create" for
     // each DartdocOptionContext that traverses the inheritance tree itself.
   ]
diff --git a/lib/src/html/html_generator.dart b/lib/src/html/html_generator.dart
index ca9b0c6..8cac333 100644
--- a/lib/src/html/html_generator.dart
+++ b/lib/src/html/html_generator.dart
@@ -142,13 +142,17 @@
   @override
   final String toolVersion;
 
+  @override
+  final bool useBaseHref;
+
   HtmlGeneratorOptions(
       {this.url,
       this.relCanonicalPrefix,
       this.faviconPath,
       String toolVersion,
       this.prettyIndexJson = false,
-      this.templatesDir})
+      this.templatesDir,
+      this.useBaseHref = false})
       : this.toolVersion = toolVersion ?? 'unknown';
 }
 
@@ -166,7 +170,8 @@
       toolVersion: dartdocVersion,
       faviconPath: config.favicon,
       prettyIndexJson: config.prettyIndexJson,
-      templatesDir: config.templatesDir);
+      templatesDir: config.templatesDir,
+      useBaseHref: config.useBaseHref);
 
   return [
     await HtmlGenerator.create(
diff --git a/lib/src/html/html_generator_instance.dart b/lib/src/html/html_generator_instance.dart
index 0ac5d7f..b82bbc4 100644
--- a/lib/src/html/html_generator_instance.dart
+++ b/lib/src/html/html_generator_instance.dart
@@ -76,6 +76,9 @@
     });
 
     String json = encoder.convert(indexItems);
+    if (!_options.useBaseHref) {
+      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
+    }
     _writer(path.join('categories.json'), '${json}\n');
   }
 
@@ -118,6 +121,9 @@
     });
 
     String json = encoder.convert(indexItems);
+    if (!_options.useBaseHref) {
+      json = json.replaceAll(HTMLBASE_PLACEHOLDER, '');
+    }
     _writer(path.join('index.json'), '${json}\n');
   }
 
@@ -412,6 +418,10 @@
     // Replaces '/' separators with proper separators for the platform.
     String outFile = path.joinAll(filename.split('/'));
     String content = template.renderString(data);
+
+    if (!_options.useBaseHref) {
+      content = content.replaceAll(HTMLBASE_PLACEHOLDER, data.htmlBase);
+    }
     _writer(outFile, content,
         element: data.self is Warnable ? data.self : null);
     if (data.self is Indexable) _indexedElements.add(data.self as Indexable);
diff --git a/lib/src/html/template_data.dart b/lib/src/html/template_data.dart
index 2127c29..0bcef5e 100644
--- a/lib/src/html/template_data.dart
+++ b/lib/src/html/template_data.dart
@@ -8,6 +8,7 @@
 abstract class HtmlOptions {
   String get relCanonicalPrefix;
   String get toolVersion;
+  bool get useBaseHref;
 }
 
 abstract class TemplateData<T extends Documentable> {
@@ -38,6 +39,7 @@
   T get self;
   String get version => htmlOptions.toolVersion;
   String get relCanonicalPrefix => htmlOptions.relCanonicalPrefix;
+  bool get useBaseHref => htmlOptions.useBaseHref;
 
   String _layoutTitle(String name, String kind, bool isDeprecated) =>
       _renderHelper.composeLayoutTitle(name, kind, isDeprecated);
@@ -67,9 +69,9 @@
   bool get hasHomepage => package.hasHomepage;
   String get homepage => package.homepage;
 
-  /// `null` for packages because they are at the root – not needed
+  /// empty for packages because they are at the root – not needed
   @override
-  String get htmlBase => null;
+  String get htmlBase => '';
 }
 
 class CategoryTemplateData extends TemplateData<Category> {
@@ -83,7 +85,7 @@
   String get title => '${category.name} ${category.kind} - Dart API';
 
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 
   @override
   String get layoutTitle => _layoutTitle(category.name, category.kind, false);
@@ -109,7 +111,7 @@
   @override
   String get title => '${library.name} library - Dart API';
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
   @override
   String get metaDescription =>
       '${library.name} library API docs, for the Dart programming language.';
@@ -164,7 +166,7 @@
   @override
   List get navLinks => [packageGraph.defaultPackage, library];
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 
   Class get objectType {
     if (_objectType != null) {
@@ -207,7 +209,7 @@
   @override
   List get navLinks => [packageGraph.defaultPackage, library];
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 }
 
 class ConstructorTemplateData extends TemplateData<Constructor> {
@@ -235,7 +237,7 @@
   List get navLinksWithGenerics => [clazz];
   @override
   @override
-  String get htmlBase => '../..';
+  String get htmlBase => '../../';
   @override
   String get title => '${constructor.name} constructor - ${clazz.name} class - '
       '${library.name} library - Dart API';
@@ -279,7 +281,7 @@
   @override
   List get navLinks => [packageGraph.defaultPackage, library];
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 }
 
 class MethodTemplateData extends TemplateData<Method> {
@@ -317,7 +319,7 @@
   @override
   List get navLinksWithGenerics => [container];
   @override
-  String get htmlBase => '../..';
+  String get htmlBase => '../../';
 }
 
 class PropertyTemplateData extends TemplateData<Field> {
@@ -356,7 +358,7 @@
   @override
   List get navLinksWithGenerics => [container];
   @override
-  String get htmlBase => '../..';
+  String get htmlBase => '../../';
 
   String get type => 'property';
 }
@@ -400,7 +402,7 @@
   @override
   List get navLinks => [packageGraph.defaultPackage, library];
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 }
 
 class TopLevelPropertyTemplateData extends TemplateData<TopLevelVariable> {
@@ -431,7 +433,7 @@
   @override
   List get navLinks => [packageGraph.defaultPackage, library];
   @override
-  String get htmlBase => '..';
+  String get htmlBase => '../';
 
   String get _type => 'property';
 }
diff --git a/lib/src/model/package.dart b/lib/src/model/package.dart
index 7771d3e..b712e92 100644
--- a/lib/src/model/package.dart
+++ b/lib/src/model/package.dart
@@ -14,6 +14,17 @@
 
 final RegExp substituteNameVersion = RegExp(r'%([bnv])%');
 
+// All hrefs are emitted as relative paths from the output root. We are unable
+// to compute them from the page we are generating, and many properties computed
+// using hrefs are memoized anyway. To build complete relative hrefs, we emit
+// the href with this placeholder, and then replace it with the current page's
+// base href afterwards.
+// See https://github.com/dart-lang/dartdoc/issues/2090 for further context.
+// TODO: Find an approach that doesn't require doing this.
+// Unlikely to be mistaken for an identifier, html tag, or something else that
+// might reasonably exist normally.
+final String HTMLBASE_PLACEHOLDER = '\%\%__HTMLBASE_dartdoc_internal__\%\%';
+
 /// A [LibraryContainer] that contains [Library] objects related to a particular
 /// package.
 class Package extends LibraryContainer
@@ -206,7 +217,7 @@
         });
         if (!_baseHref.endsWith('/')) _baseHref = '${_baseHref}/';
       } else {
-        _baseHref = '';
+        _baseHref = config.useBaseHref ? '' : HTMLBASE_PLACEHOLDER;
       }
     }
     return _baseHref;
diff --git a/lib/templates/_footer.html b/lib/templates/_footer.html
index c20bd74..bac8322 100644
--- a/lib/templates/_footer.html
+++ b/lib/templates/_footer.html
@@ -11,11 +11,12 @@
   <!-- footer-text placeholder -->
 </footer>
 
+{{! TODO(jdkoren): unwrap ^useBaseHref sections when the option is removed.}}
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
-<script src="static-assets/typeahead.bundle.min.js"></script>
-<script src="static-assets/highlight.pack.js"></script>
-<script src="static-assets/URI.js"></script>
-<script src="static-assets/script.js"></script>
+<script src="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/typeahead.bundle.min.js"></script>
+<script src="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/highlight.pack.js"></script>
+<script src="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/URI.js"></script>
+<script src="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/script.js"></script>
 <!-- footer placeholder -->
 
 </body>
diff --git a/lib/templates/_head.html b/lib/templates/_head.html
index 5fffa11..15580a9 100644
--- a/lib/templates/_head.html
+++ b/lib/templates/_head.html
@@ -12,16 +12,20 @@
   {{ #relCanonicalPrefix }}
   <link rel="canonical" href="{{{relCanonicalPrefix}}}/{{{self.href}}}">
   {{ /relCanonicalPrefix}}
+
+  {{#useBaseHref}}{{! TODO(jdkoren): remove when the useBaseHref option is removed.}}
   {{#htmlBase}}
   <!-- required because all the links are pseudo-absolute -->
   <base href="{{{htmlBase}}}">
   {{/htmlBase}}
+  {{/useBaseHref}}
 
+  {{! TODO(jdkoren): unwrap ^useBaseHref sections when the option is removed.}}
   <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500,400i,400,300|Source+Sans+Pro:400,300,700" rel="stylesheet">
   <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-  <link rel="stylesheet" href="static-assets/github.css">
-  <link rel="stylesheet" href="static-assets/styles.css">
-  <link rel="icon" href="static-assets/favicon.png">
+  <link rel="stylesheet" href="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/github.css">
+  <link rel="stylesheet" href="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/styles.css">
+  <link rel="icon" href="{{^useBaseHref}}%%__HTMLBASE_dartdoc_internal__%%{{/useBaseHref}}static-assets/favicon.png">
   <!-- header placeholder -->
 </head>
 
diff --git a/test/model_special_cases_test.dart b/test/model_special_cases_test.dart
index 8abf5af..7866edc 100644
--- a/test/model_special_cases_test.dart
+++ b/test/model_special_cases_test.dart
@@ -356,7 +356,8 @@
             hashCode.inheritance.any((c) => c.name == 'Interceptor'), isTrue);
         // If EventTarget really does start implementing hashCode, this will
         // fail.
-        expect(hashCode.href, equals('dart-core/Object/hashCode.html'));
+        expect(hashCode.href,
+            equals('${HTMLBASE_PLACEHOLDER}dart-core/Object/hashCode.html'));
         expect(
             hashCode.canonicalEnclosingContainer, equals(objectModelElement));
         expect(
@@ -387,7 +388,8 @@
     test('sdk library have formatted names', () {
       expect(dartAsyncLib.name, 'dart:async');
       expect(dartAsyncLib.dirName, 'dart-async');
-      expect(dartAsyncLib.href, 'dart-async/dart-async-library.html');
+      expect(dartAsyncLib.href,
+          '${HTMLBASE_PLACEHOLDER}dart-async/dart-async-library.html');
     });
   });
 
diff --git a/test/model_test.dart b/test/model_test.dart
index edae9ae..c044f03 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -251,18 +251,24 @@
     test('can invoke a tool and add a reference link', () {
       expect(invokeTool.documentation,
           contains('Yes it is a [Dog]! Is not a [ToolUser].'));
-      expect(invokeTool.documentationAsHtml,
-          contains('<a href="ex/ToolUser-class.html">ToolUser</a>'));
-      expect(invokeTool.documentationAsHtml,
-          contains('<a href="ex/Dog-class.html">Dog</a>'));
+      expect(
+          invokeTool.documentationAsHtml,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/ToolUser-class.html">ToolUser</a>'));
+      expect(
+          invokeTool.documentationAsHtml,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/Dog-class.html">Dog</a>'));
     });
     test(r'can invoke a tool with no $INPUT or args', () {
       expect(invokeToolNoInput.documentation, contains('Args: []'));
       expect(invokeToolNoInput.documentation,
           isNot(contains('This text should not appear in the output')));
       expect(invokeToolNoInput.documentation, isNot(contains('[Dog]')));
-      expect(invokeToolNoInput.documentationAsHtml,
-          isNot(contains('<a href="ex/Dog-class.html">Dog</a>')));
+      expect(
+          invokeToolNoInput.documentationAsHtml,
+          isNot(contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/Dog-class.html">Dog</a>')));
     });
     test('can invoke a tool multiple times in one comment block', () {
       RegExp envLine = RegExp(r'^Env: \{', multiLine: true);
@@ -372,14 +378,14 @@
       expect(
           renderer.renderCategoryLabel(category),
           '<span class="category superb cp-0 linked" title="This is part of the Superb Topic.">'
-          '<a href="topics/Superb-topic.html">Superb</a></span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}topics/Superb-topic.html">Superb</a></span>');
     });
 
     test('CategoryRendererHtml renders linkedName', () {
       Category category = packageGraph.publicPackages.first.categories.first;
       CategoryRendererHtml renderer = CategoryRendererHtml();
       expect(renderer.renderLinkedName(category),
-          '<a href="topics/Superb-topic.html">Superb</a>');
+          '<a href="${HTMLBASE_PLACEHOLDER}topics/Superb-topic.html">Superb</a>');
     });
   });
 
@@ -963,14 +969,14 @@
       test('Verify links inside of table headers', () {
         expect(
             docsAsHtml.contains(
-                '<th><a href="fake/Annotation-class.html">Annotation</a></th>'),
+                '<th><a href="${HTMLBASE_PLACEHOLDER}fake/Annotation-class.html">Annotation</a></th>'),
             isTrue);
       });
 
       test('Verify links inside of table body', () {
         expect(
             docsAsHtml.contains(
-                '<tbody><tr><td><a href="fake/DocumentWithATable/foo-constant.html">foo</a></td>'),
+                '<tbody><tr><td><a href="${HTMLBASE_PLACEHOLDER}fake/DocumentWithATable/foo-constant.html">foo</a></td>'),
             isTrue);
       });
 
@@ -995,49 +1001,49 @@
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to library: <a href="mylibpub/mylibpub-library.html">renamedLib</a>'));
+                'Link to library: <a href="${HTMLBASE_PLACEHOLDER}mylibpub/mylibpub-library.html">renamedLib</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to constructor (implied): <a href="mylibpub/YetAnotherHelper/YetAnotherHelper.html">new renamedLib.YetAnotherHelper()</a>'));
+                'Link to constructor (implied): <a href="${HTMLBASE_PLACEHOLDER}mylibpub/YetAnotherHelper/YetAnotherHelper.html">new renamedLib.YetAnotherHelper()</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to constructor (implied, no new): <a href="mylibpub/YetAnotherHelper/YetAnotherHelper.html">renamedLib.YetAnotherHelper()</a>'));
+                'Link to constructor (implied, no new): <a href="${HTMLBASE_PLACEHOLDER}mylibpub/YetAnotherHelper/YetAnotherHelper.html">renamedLib.YetAnotherHelper()</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to class: <a href="mylibpub/YetAnotherHelper-class.html">renamedLib.YetAnotherHelper</a>'));
+                'Link to class: <a href="${HTMLBASE_PLACEHOLDER}mylibpub/YetAnotherHelper-class.html">renamedLib.YetAnotherHelper</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to constructor (direct): <a href="mylibpub/YetAnotherHelper/YetAnotherHelper.html">renamedLib.YetAnotherHelper.YetAnotherHelper</a>'));
+                'Link to constructor (direct): <a href="${HTMLBASE_PLACEHOLDER}mylibpub/YetAnotherHelper/YetAnotherHelper.html">renamedLib.YetAnotherHelper.YetAnotherHelper</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to class member: <a href="mylibpub/YetAnotherHelper/getMoreContents.html">renamedLib.YetAnotherHelper.getMoreContents</a>'));
+                'Link to class member: <a href="${HTMLBASE_PLACEHOLDER}mylibpub/YetAnotherHelper/getMoreContents.html">renamedLib.YetAnotherHelper.getMoreContents</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to function: <a href="mylibpub/helperFunction.html">renamedLib.helperFunction</a>'));
+                'Link to function: <a href="${HTMLBASE_PLACEHOLDER}mylibpub/helperFunction.html">renamedLib.helperFunction</a>'));
         expect(
             aFunctionUsingRenamedLib.documentationAsHtml,
             contains(
-                'Link to overlapping prefix: <a href="csspub/theOnlyThingInTheLibrary.html">renamedLib2.theOnlyThingInTheLibrary</a>'));
+                'Link to overlapping prefix: <a href="${HTMLBASE_PLACEHOLDER}csspub/theOnlyThingInTheLibrary.html">renamedLib2.theOnlyThingInTheLibrary</a>'));
       });
 
       test('operator [] reference within a class works', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/BaseForDocComments/operator_get.html">operator []</a> '));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/BaseForDocComments/operator_get.html">operator []</a> '));
       });
 
       test('operator [] reference outside of a class works', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/SpecialList/operator_get.html">SpecialList.operator []</a> '));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/SpecialList/operator_get.html">SpecialList.operator []</a> '));
       }, skip: 'https://github.com/dart-lang/dartdoc/issues/1285');
 
       test('codeifies a class from the SDK', () {
@@ -1052,7 +1058,7 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/BaseForDocComments-class.html">BaseForDocComments</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/BaseForDocComments-class.html">BaseForDocComments</a>'));
       });
 
       test(
@@ -1061,7 +1067,7 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="anonymous_library/doesStuff.html">doesStuff</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}anonymous_library/doesStuff.html">doesStuff</a>'));
       });
 
       test(
@@ -1069,10 +1075,14 @@
           () {
         final Class helperClass =
             exLibrary.classes.firstWhere((c) => c.name == 'Helper');
-        expect(helperClass.documentationAsHtml,
-            contains('<a href="ex/Apple-class.html">Apple</a>'));
-        expect(helperClass.documentationAsHtml,
-            contains('<a href="ex/B-class.html">ex.B</a>'));
+        expect(
+            helperClass.documentationAsHtml,
+            contains(
+                '<a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>'));
+        expect(
+            helperClass.documentationAsHtml,
+            contains(
+                '<a href="${HTMLBASE_PLACEHOLDER}ex/B-class.html">ex.B</a>'));
       });
 
       test('link to override method in implementer from base class', () {
@@ -1081,7 +1091,7 @@
         expect(
             helperClass.documentationAsHtml,
             contains(
-                '<a href="override_class/BoxConstraints/debugAssertIsValid.html">BoxConstraints.debugAssertIsValid</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}override_class/BoxConstraints/debugAssertIsValid.html">BoxConstraints.debugAssertIsValid</a>'));
       });
 
       test(
@@ -1090,7 +1100,7 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="two_exports/BaseClass-class.html">BaseClass</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}two_exports/BaseClass-class.html">BaseClass</a>'));
       });
 
       test(
@@ -1099,30 +1109,35 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/NAME_WITH_TWO_UNDERSCORES-constant.html">NAME_WITH_TWO_UNDERSCORES</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/NAME_WITH_TWO_UNDERSCORES-constant.html">NAME_WITH_TWO_UNDERSCORES</a>'));
       });
 
       test('links to a method in this class', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/BaseForDocComments/anotherMethod.html">anotherMethod</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/BaseForDocComments/anotherMethod.html">anotherMethod</a>'));
       });
 
       test('links to a top-level function in this library', () {
         expect(
             docsAsHtml,
             contains(
-                '<a class="deprecated" href="fake/topLevelFunction.html">topLevelFunction</a>'));
+                '<a class="deprecated" href="${HTMLBASE_PLACEHOLDER}fake/topLevelFunction.html">topLevelFunction</a>'));
       });
 
       test('links to top-level function from an imported library', () {
         expect(
-            docsAsHtml, contains('<a href="ex/function1.html">function1</a>'));
+            docsAsHtml,
+            contains(
+                '<a href="${HTMLBASE_PLACEHOLDER}ex/function1.html">function1</a>'));
       });
 
       test('links to a class from an imported lib', () {
-        expect(docsAsHtml, contains('<a href="ex/Apple-class.html">Apple</a>'));
+        expect(
+            docsAsHtml,
+            contains(
+                '<a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>'));
       });
 
       test(
@@ -1131,14 +1146,14 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/incorrectDocReference-constant.html">incorrectDocReference</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/incorrectDocReference-constant.html">incorrectDocReference</a>'));
       });
 
       test('links to a top-level const from an imported lib', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="ex/incorrectDocReferenceFromEx-constant.html">incorrectDocReferenceFromEx</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}ex/incorrectDocReferenceFromEx-constant.html">incorrectDocReferenceFromEx</a>'));
       });
 
       test('links to a top-level variable with a prefix from an imported lib',
@@ -1146,27 +1161,42 @@
         expect(
             docsAsHtml,
             contains(
-                '<a href="csspub/theOnlyThingInTheLibrary.html">css.theOnlyThingInTheLibrary</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}csspub/theOnlyThingInTheLibrary.html">css.theOnlyThingInTheLibrary</a>'));
       });
 
       test('links to a name with a single underscore', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/NAME_SINGLEUNDERSCORE-constant.html">NAME_SINGLEUNDERSCORE</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/NAME_SINGLEUNDERSCORE-constant.html">NAME_SINGLEUNDERSCORE</a>'));
       });
 
       test('correctly escapes type parameters in links', () {
         expect(
             docsAsHtml,
             contains(
-                '<a href="fake/ExtraSpecialList-class.html">ExtraSpecialList&lt;Object&gt;</a>'));
+                '<a href="${HTMLBASE_PLACEHOLDER}fake/ExtraSpecialList-class.html">ExtraSpecialList&lt;Object&gt;</a>'));
       });
 
       test('correctly escapes type parameters in broken links', () {
         expect(docsAsHtml,
             contains('<code>ThisIsNotHereNoWay&lt;MyType&gt;</code>'));
       });
+
+      test('leaves relative href resulting in a broken link', () {
+        // Dartdoc does emit a brokenLink warning for this.
+        expect(docsAsHtml,
+            contains('<a href="SubForDocComments/localMethod.html">link</a>'));
+      });
+
+      test('leaves relative href resulting in a working link', () {
+        // Ideally doc comments should not make assumptions about Dartdoc output
+        // files, but unfortunately some do...
+        expect(
+            docsAsHtml,
+            contains(
+                '<a href="../SubForDocComments/localMethod.html">link</a>'));
+      });
     });
 
     test('multi-underscore names in brackets do not become italicized', () {
@@ -1174,7 +1204,7 @@
       expect(
           short.documentationAsHtml,
           contains(
-              '<a href="fake/NAME_WITH_TWO_UNDERSCORES-constant.html">NAME_WITH_TWO_UNDERSCORES</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/NAME_WITH_TWO_UNDERSCORES-constant.html">NAME_WITH_TWO_UNDERSCORES</a>'));
     });
 
     test('still has brackets inside code blocks', () {
@@ -1227,7 +1257,7 @@
     test('single ref to class', () {
       expect(
           B.documentationAsHtml.contains(
-              '<p>Extends class <a href="ex/Apple-class.html">Apple</a>, use <a href="ex/Apple/Apple.html">new Apple</a> or <a href="ex/Apple/Apple.fromString.html">new Apple.fromString</a></p>'),
+              '<p>Extends class <a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>, use <a href="${HTMLBASE_PLACEHOLDER}ex/Apple/Apple.html">new Apple</a> or <a href="${HTMLBASE_PLACEHOLDER}ex/Apple/Apple.fromString.html">new Apple.fromString</a></p>'),
           isTrue);
     });
 
@@ -1243,27 +1273,38 @@
       expect(extendedClass, isNotNull);
       String resolved = extendedClass.documentationAsHtml;
       expect(resolved, isNotNull);
-      expect(resolved,
-          contains('<a href="two_exports/BaseClass-class.html">BaseClass</a>'));
-      expect(resolved,
-          contains('Linking over to <a href="ex/Apple-class.html">Apple</a>'));
+      expect(
+          resolved,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}two_exports/BaseClass-class.html">BaseClass</a>'));
+      expect(
+          resolved,
+          contains(
+              'Linking over to <a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>'));
     });
 
     test('references to class and constructors', () {
       String comment = B.documentationAsHtml;
-      expect(comment,
-          contains('Extends class <a href="ex/Apple-class.html">Apple</a>'));
-      expect(
-          comment, contains('use <a href="ex/Apple/Apple.html">new Apple</a>'));
       expect(
           comment,
           contains(
-              '<a href="ex/Apple/Apple.fromString.html">new Apple.fromString</a>'));
+              'Extends class <a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>'));
+      expect(
+          comment,
+          contains(
+              'use <a href="${HTMLBASE_PLACEHOLDER}ex/Apple/Apple.html">new Apple</a>'));
+      expect(
+          comment,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/Apple/Apple.fromString.html">new Apple.fromString</a>'));
     });
 
     test('reference to class from another library', () {
       String comment = superAwesomeClass.documentationAsHtml;
-      expect(comment, contains('<a href="ex/Apple-class.html">Apple</a>'));
+      expect(
+          comment,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/Apple-class.html">Apple</a>'));
     });
 
     test('reference to method', () {
@@ -1271,7 +1312,7 @@
       expect(
           comment,
           equals(
-              '<p>link to method from class <a href="ex/Apple/m.html">Apple.m</a></p>'));
+              '<p>link to method from class <a href="${HTMLBASE_PLACEHOLDER}ex/Apple/m.html">Apple.m</a></p>'));
     });
 
     test(
@@ -1284,11 +1325,11 @@
       expect(
           notAMethodFromPrivateClass.documentationAsHtml,
           contains(
-              '<a href="fake/InheritingClassOne/aMethod.html">fake.InheritingClassOne.aMethod</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/InheritingClassOne/aMethod.html">fake.InheritingClassOne.aMethod</a>'));
       expect(
           notAMethodFromPrivateClass.documentationAsHtml,
           contains(
-              '<a href="fake/InheritingClassTwo/aMethod.html">fake.InheritingClassTwo.aMethod</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/InheritingClassTwo/aMethod.html">fake.InheritingClassTwo.aMethod</a>'));
     });
 
     test('legacy code blocks render correctly', () {
@@ -1451,35 +1492,35 @@
       expect(
           overrideByModifierClass.oneLineDoc,
           contains(
-              '<a href=\"fake/ModifierClass-class.html\">ModifierClass</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ModifierClass-class.html">ModifierClass</a>'));
       expect(
           overrideByModifierClass.canonicalModelElement.documentationAsHtml,
           contains(
-              '<a href=\"fake/ModifierClass-class.html\">ModifierClass</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ModifierClass-class.html">ModifierClass</a>'));
       expect(
           overrideByGenericMixin.oneLineDoc,
           contains(
-              '<a href=\"fake/GenericMixin-mixin.html\">GenericMixin</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/GenericMixin-mixin.html">GenericMixin</a>'));
       expect(
           overrideByGenericMixin.canonicalModelElement.documentationAsHtml,
           contains(
-              '<a href=\"fake/GenericMixin-mixin.html\">GenericMixin</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/GenericMixin-mixin.html">GenericMixin</a>'));
       expect(
           overrideByBoth.oneLineDoc,
           contains(
-              '<a href=\"fake/ModifierClass-class.html\">ModifierClass</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ModifierClass-class.html">ModifierClass</a>'));
       expect(
           overrideByBoth.oneLineDoc,
           contains(
-              '<a href=\"fake/GenericMixin-mixin.html\">GenericMixin</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/GenericMixin-mixin.html">GenericMixin</a>'));
       expect(
           overrideByBoth.canonicalModelElement.documentationAsHtml,
           contains(
-              '<a href=\"fake/ModifierClass-class.html\">ModifierClass</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ModifierClass-class.html">ModifierClass</a>'));
       expect(
           overrideByBoth.canonicalModelElement.documentationAsHtml,
           contains(
-              '<a href=\"fake/GenericMixin-mixin.html\">GenericMixin</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/GenericMixin-mixin.html">GenericMixin</a>'));
     });
   });
 
@@ -1629,11 +1670,12 @@
     });
 
     test('exported class should have hrefs from the current library', () {
-      expect(Dep.href, equals('ex/Deprecated-class.html'));
       expect(
-          Dep.instanceMethods[0].href, equals('ex/Deprecated/toString.html'));
-      expect(
-          Dep.instanceProperties[0].href, equals('ex/Deprecated/expires.html'));
+          Dep.href, equals('${HTMLBASE_PLACEHOLDER}ex/Deprecated-class.html'));
+      expect(Dep.instanceMethods[0].href,
+          equals('${HTMLBASE_PLACEHOLDER}ex/Deprecated/toString.html'));
+      expect(Dep.instanceProperties[0].href,
+          equals('${HTMLBASE_PLACEHOLDER}ex/Deprecated/expires.html'));
     });
 
     test('exported class should have linkedReturnType for the current library',
@@ -1641,8 +1683,10 @@
       Method returnCool = Cool.instanceMethods
           .firstWhere((m) => m.name == 'returnCool', orElse: () => null);
       expect(returnCool, isNotNull);
-      expect(returnCool.linkedReturnType,
-          equals('<a href="fake/Cool-class.html">Cool</a>'));
+      expect(
+          returnCool.linkedReturnType,
+          equals(
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/Cool-class.html">Cool</a>'));
     });
 
     test('F has a single instance method', () {
@@ -1885,14 +1929,18 @@
         () {
       expect(extensionReferencer.documentationAsHtml,
           contains('<code>_Shhh</code>'));
-      expect(extensionReferencer.documentationAsHtml,
-          contains('<a href="ex/FancyList.html">FancyList</a>'));
-      expect(extensionReferencer.documentationAsHtml,
-          contains('<a href="ex/AnExtension/call.html">AnExtension.call</a>'));
       expect(
           extensionReferencer.documentationAsHtml,
           contains(
-              '<a href="reexport_two/DocumentThisExtensionOnce.html">DocumentThisExtensionOnce</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/FancyList.html">FancyList</a>'));
+      expect(
+          extensionReferencer.documentationAsHtml,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/AnExtension/call.html">AnExtension.call</a>'));
+      expect(
+          extensionReferencer.documentationAsHtml,
+          contains(
+              '<a href="${HTMLBASE_PLACEHOLDER}reexport_two/DocumentThisExtensionOnce.html">DocumentThisExtensionOnce</a>'));
     });
 
     test('has a fully qualified name', () {
@@ -1909,7 +1957,7 @@
 
     test('member method has href', () {
       s = ext.instanceMethods.firstWhere((m) => m.name == 's');
-      expect(s.href, 'ex/AppleExtension/s.html');
+      expect(s.href, '${HTMLBASE_PLACEHOLDER}ex/AppleExtension/s.html');
     });
 
     test('has extended type', () {
@@ -2163,7 +2211,7 @@
       expect(
           params,
           '<span class="parameter" id="addCallback-param-callback">'
-          '<span class="type-annotation"><a href="fake/VoidCallback.html">VoidCallback</a></span> '
+          '<span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}fake/VoidCallback.html">VoidCallback</a></span> '
           '<span class="parameter-name">callback</span></span><wbr>');
 
       function =
@@ -2172,7 +2220,7 @@
       expect(
           params,
           '<span class="parameter" id="addCallback2-param-callback">'
-          '<span class="type-annotation"><a href="fake/Callback2.html">Callback2</a></span> '
+          '<span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}fake/Callback2.html">Callback2</a></span> '
           '<span class="parameter-name">callback</span></span><wbr>');
     });
 
@@ -2205,21 +2253,21 @@
       // TODO(jcollins-g): really, these shouldn't be called "parameters" in
       // the span class.
       expect(explicitSetter.linkedReturnType,
-          '<span class="parameter" id="explicitSetter=-param-f"><span class="type-annotation">dynamic</span> <span class="parameter-name">Function</span>(<span class="parameter" id="param-bar"><span class="type-annotation">int</span>, </span><wbr><span class="parameter" id="param-baz"><span class="type-annotation"><a href="fake/Cool-class.html">Cool</a></span>, </span><wbr><span class="parameter" id="param-macTruck"><span class="type-annotation">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span></span><wbr>)</span><wbr>');
+          '<span class="parameter" id="explicitSetter=-param-f"><span class="type-annotation">dynamic</span> <span class="parameter-name">Function</span>(<span class="parameter" id="param-bar"><span class="type-annotation">int</span>, </span><wbr><span class="parameter" id="param-baz"><span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}fake/Cool-class.html">Cool</a></span>, </span><wbr><span class="parameter" id="param-macTruck"><span class="type-annotation">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span></span><wbr>)</span><wbr>');
     });
 
     test('parameterized type from field is correctly displayed', () {
       Field aField = TemplatedInterface.instanceProperties
           .singleWhere((f) => f.name == 'aField');
       expect(aField.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">Stream<span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">Stream<span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span>&gt;</span>');
     });
 
     test('parameterized type from inherited field is correctly displayed', () {
       Field aInheritedField = TemplatedInterface.inheritedProperties
           .singleWhere((f) => f.name == 'aInheritedField');
       expect(aInheritedField.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2229,7 +2277,7 @@
           .singleWhere((f) => f.name == 'aGetter')
           .getter;
       expect(aGetter.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">Map<span class="signature">&lt;<wbr><span class="type-parameter">A</span>, <span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">String</span>&gt;</span></span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">Map<span class="signature">&lt;<wbr><span class="type-parameter">A</span>, <span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">String</span>&gt;</span></span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2239,7 +2287,7 @@
           .singleWhere((f) => f.name == 'aInheritedGetter')
           .getter;
       expect(aInheritedGetter.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2249,11 +2297,11 @@
           .singleWhere((f) => f.name == 'aInheritedSetter')
           .setter;
       expect(aInheritedSetter.allParameters.first.modelType.linkedName,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
       // TODO(jcollins-g): really, these shouldn't be called "parameters" in
       // the span class.
       expect(aInheritedSetter.enclosingCombo.linkedReturnType,
-          '<span class="parameter" id="aInheritedSetter=-param-thingToSet"><span class="type-annotation"><a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span></span><wbr>');
+          '<span class="parameter" id="aInheritedSetter=-param-thingToSet"><span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span></span><wbr>');
     });
 
     test(
@@ -2262,7 +2310,7 @@
       Method aMethodInterface = TemplatedInterface.allInstanceMethods
           .singleWhere((m) => m.name == 'aMethodInterface');
       expect(aMethodInterface.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2271,7 +2319,7 @@
       Method aInheritedMethod = TemplatedInterface.allInstanceMethods
           .singleWhere((m) => m.name == 'aInheritedMethod');
       expect(aInheritedMethod.linkedReturnType,
-          '<a href="ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/AnotherParameterizedClass-class.html">AnotherParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2281,7 +2329,7 @@
           .allInstanceMethods
           .singleWhere((m) => m.name == 'aTypedefReturningMethodInterface');
       expect(aTypedefReturningMethodInterface.linkedReturnType,
-          '<a href="ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">String</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">String</span>&gt;</span></span>&gt;</span>');
     });
 
     test(
@@ -2291,7 +2339,7 @@
           .allInstanceMethods
           .singleWhere((m) => m.name == 'aInheritedTypedefReturningMethod');
       expect(aInheritedTypedefReturningMethod.linkedReturnType,
-          '<a href="ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
     });
 
     test('parameterized types for inherited operator is correctly displayed',
@@ -2300,11 +2348,11 @@
           .inheritedOperators
           .singleWhere((m) => m.name == 'operator +');
       expect(aInheritedAdditionOperator.linkedReturnType,
-          '<a href="ex/ParameterizedClass-class.html">ParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
+          '<a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedClass-class.html">ParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span>');
       expect(
           ParameterRendererHtml()
               .renderLinkedParams(aInheritedAdditionOperator.parameters),
-          '<span class="parameter" id="+-param-other"><span class="type-annotation"><a href="ex/ParameterizedClass-class.html">ParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span> <span class="parameter-name">other</span></span><wbr>');
+          '<span class="parameter" id="+-param-other"><span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedClass-class.html">ParameterizedClass</a><span class="signature">&lt;<wbr><span class="type-parameter">List<span class="signature">&lt;<wbr><span class="type-parameter">int</span>&gt;</span></span>&gt;</span></span> <span class="parameter-name">other</span></span><wbr>');
     });
 
     test('', () {});
@@ -2409,14 +2457,17 @@
     test(
         'an inherited method from the core SDK has a href relative to the package class',
         () {
-      expect(inheritedClear.href, equals('ex/CatString/clear.html'));
+      expect(inheritedClear.href,
+          equals('${HTMLBASE_PLACEHOLDER}ex/CatString/clear.html'));
     });
 
     test(
         'an inherited method has linked to enclosed class name when superclass not in package',
         () {
-      expect(inheritedClear.linkedName,
-          equals('<a href="ex/CatString/clear.html">clear</a>'));
+      expect(
+          inheritedClear.linkedName,
+          equals(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/CatString/clear.html">clear</a>'));
     });
 
     test('has enclosing element', () {
@@ -2519,7 +2570,10 @@
     });
 
     test('if inherited, has a href relative to enclosed class', () {
-      expect(plus.href, equals('ex/SpecializedDuration/operator_plus.html'));
+      expect(
+          plus.href,
+          equals(
+              '${HTMLBASE_PLACEHOLDER}ex/SpecializedDuration/operator_plus.html'));
     });
 
     test('if inherited and superclass not in package, link to enclosed class',
@@ -2527,7 +2581,7 @@
       expect(
           plus.linkedName,
           equals(
-              '<a href="ex/SpecializedDuration/operator_plus.html">operator +</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/SpecializedDuration/operator_plus.html">operator +</a>'));
     });
   });
 
@@ -2764,8 +2818,10 @@
       expect(lengthX.oneLineDoc, equals('Returns a length.'));
       // TODO(jdkoren): This is left here to have at least one literal matching
       // test for extendedDocLink. Move this when extracting renderer tests.
-      expect(lengthX.extendedDocLink,
-          equals('<a href="fake/WithGetterAndSetter/lengthX.html">[...]</a>'));
+      expect(
+          lengthX.extendedDocLink,
+          equals(
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/WithGetterAndSetter/lengthX.html">[...]</a>'));
       expect(lengthX.documentation, contains('the fourth dimension'));
       expect(lengthX.documentation, isNot(contains('[...]')));
     });
@@ -2776,14 +2832,17 @@
     });
 
     test('inherited property has a linked name to superclass in package', () {
-      expect(mInB.linkedName, equals('<a href="ex/Apple/m.html">m</a>'));
+      expect(mInB.linkedName,
+          equals('<a href="${HTMLBASE_PLACEHOLDER}ex/Apple/m.html">m</a>'));
     });
 
     test(
         'inherited property has linked name to enclosed class, if superclass is not in package',
         () {
-      expect(isEmpty.linkedName,
-          equals('<a href="ex/CatString/isEmpty.html">isEmpty</a>'));
+      expect(
+          isEmpty.linkedName,
+          equals(
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/CatString/isEmpty.html">isEmpty</a>'));
     });
 
     test('inherited property has the enclosing class', () {
@@ -2917,7 +2976,7 @@
       expect(
           fieldWithTypedef.linkedReturnType,
           equals(
-              '<a href="ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">bool</span>&gt;</span>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">bool</span>&gt;</span>'));
     });
   });
 
@@ -2988,7 +3047,7 @@
       expect(
           complicatedReturn.linkedReturnType,
           equals(
-              '<a href="fake/ATypeTakingClass-class.html">ATypeTakingClass</a><span class="signature">&lt;<wbr><span class="type-parameter">String Function<span class="signature">(<span class="parameter" id="param-"><span class="type-annotation">int</span></span><wbr>)</span></span>&gt;</span>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ATypeTakingClass-class.html">ATypeTakingClass</a><span class="signature">&lt;<wbr><span class="type-parameter">String Function<span class="signature">(<span class="parameter" id="param-"><span class="type-annotation">int</span></span><wbr>)</span></span>&gt;</span>'));
     });
 
     test('@nodoc on simple property works', () {
@@ -3098,7 +3157,7 @@
 
     test('substrings of the constant values type are not linked (#1535)', () {
       expect(aName.constantValue,
-          'const <a href="ex/ExtendedShortName/ExtendedShortName.html">ExtendedShortName</a>(&quot;hello there&quot;)');
+          'const <a href="${HTMLBASE_PLACEHOLDER}ex/ExtendedShortName/ExtendedShortName.html">ExtendedShortName</a>(&quot;hello there&quot;)');
     });
 
     test('constant field values are escaped', () {
@@ -3142,7 +3201,7 @@
 
     test('MY_CAT is linked', () {
       expect(cat.constantValue,
-          'const <a href="ex/ConstantCat/ConstantCat.html">ConstantCat</a>(&#39;tabby&#39;)');
+          'const <a href="${HTMLBASE_PLACEHOLDER}ex/ConstantCat/ConstantCat.html">ConstantCat</a>(&#39;tabby&#39;)');
     });
 
     test('exported property', () {
@@ -3186,11 +3245,11 @@
       expect(
           referToADefaultConstructor.documentationAsHtml,
           contains(
-              '<a href="fake/ReferToADefaultConstructor-class.html">ReferToADefaultConstructor</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ReferToADefaultConstructor-class.html">ReferToADefaultConstructor</a>'));
       expect(
           referToADefaultConstructor.documentationAsHtml,
           contains(
-              '<a href="fake/ReferToADefaultConstructor/ReferToADefaultConstructor.html">ReferToADefaultConstructor.ReferToADefaultConstructor</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ReferToADefaultConstructor/ReferToADefaultConstructor.html">ReferToADefaultConstructor.ReferToADefaultConstructor</a>'));
     });
 
     test('displays generic parameters correctly', () {
@@ -3280,7 +3339,7 @@
       expect(
           ExtendsFutureVoid.linkedName,
           equals(
-              '<a href="fake/ExtendsFutureVoid-class.html">ExtendsFutureVoid</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ExtendsFutureVoid-class.html">ExtendsFutureVoid</a>'));
       DefinedElementType FutureVoid = ExtendsFutureVoid.publicSuperChain
           .firstWhere((c) => c.name == 'Future');
       expect(
@@ -3293,7 +3352,7 @@
       expect(
           ImplementsFutureVoid.linkedName,
           equals(
-              '<a href="fake/ImplementsFutureVoid-class.html">ImplementsFutureVoid</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ImplementsFutureVoid-class.html">ImplementsFutureVoid</a>'));
       DefinedElementType FutureVoid = ImplementsFutureVoid.publicInterfaces
           .firstWhere((c) => c.name == 'Future');
       expect(
@@ -3306,13 +3365,13 @@
       expect(
           ATypeTakingClassMixedIn.linkedName,
           equals(
-              '<a href="fake/ATypeTakingClassMixedIn-class.html">ATypeTakingClassMixedIn</a>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ATypeTakingClassMixedIn-class.html">ATypeTakingClassMixedIn</a>'));
       DefinedElementType ATypeTakingClassVoid = ATypeTakingClassMixedIn.mixins
           .firstWhere((c) => c.name == 'ATypeTakingClass');
       expect(
           ATypeTakingClassVoid.linkedName,
           equals(
-              '<a href="fake/ATypeTakingClass-class.html">ATypeTakingClass</a><span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>'));
+              '<a href="${HTMLBASE_PLACEHOLDER}fake/ATypeTakingClass-class.html">ATypeTakingClass</a><span class="signature">&lt;<wbr><span class="type-parameter">void</span>&gt;</span>'));
     });
   });
 
@@ -3355,7 +3414,7 @@
       expect(
           ParameterRendererHtml().renderLinkedParams(theConstructor.parameters),
           equals(
-              '<span class="parameter" id="-param-x"><span class="type-annotation"><a href="ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">double</span>&gt;</span></span> <span class="parameter-name">x</span></span><wbr>'));
+              '<span class="parameter" id="-param-x"><span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}ex/ParameterizedTypedef.html">ParameterizedTypedef</a><span class="signature">&lt;<wbr><span class="type-parameter">double</span>&gt;</span></span> <span class="parameter-name">x</span></span><wbr>'));
     });
 
     test('anonymous nested functions inside typedefs are handled', () {
@@ -3536,7 +3595,7 @@
       expect(
           params,
           equals(
-              '<span class="parameter" id="methodWithTypedefParam-param-p"><span class="type-annotation"><a href="ex/processMessage.html">processMessage</a></span> <span class="parameter-name">p</span></span><wbr>'));
+              '<span class="parameter" id="methodWithTypedefParam-param-p"><span class="type-annotation"><a href="${HTMLBASE_PLACEHOLDER}ex/processMessage.html">processMessage</a></span> <span class="parameter-name">p</span></span><wbr>'));
     });
   });
 
@@ -3615,7 +3674,7 @@
       expect(
           forAnnotation.annotations.first,
           equals(
-              '@<a href="ex/ForAnnotation-class.html">ForAnnotation</a>(&#39;my value&#39;)'));
+              '@<a href="${HTMLBASE_PLACEHOLDER}ex/ForAnnotation-class.html">ForAnnotation</a>(&#39;my value&#39;)'));
     });
 
     test('methods has the right annotation', () {
@@ -3628,7 +3687,7 @@
       expect(
           ctr.annotations[0],
           equals(
-              '@<a href="ex/Deprecated-class.html">Deprecated</a>(&quot;Internal use&quot;)'));
+              '@<a href="${HTMLBASE_PLACEHOLDER}ex/Deprecated-class.html">Deprecated</a>(&quot;Internal use&quot;)'));
     });
   });
 
diff --git a/testing/test_package/lib/fake.dart b/testing/test_package/lib/fake.dart
index 94ff87e..97a4795 100644
--- a/testing/test_package/lib/fake.dart
+++ b/testing/test_package/lib/fake.dart
@@ -887,6 +887,10 @@
   /// Reference containing a type parameter [ExtraSpecialList<Object>]
   ///
   /// Reference to something that doesn't exist containing a type parameter [ThisIsNotHereNoWay<MyType>]
+  ///
+  /// Link to a nonexistent file (erroneously expects base href): [link](SubForDocComments/localMethod.html)
+  ///
+  /// Link to an existing file: [link](../SubForDocComments/localMethod.html)
   String doAwesomeStuff(int value) => null;
 
   void anotherMethod() {}