void validateHtml()

in pkg/pub_validations/lib/html/html_validation.dart [30:124]


void validateHtml(Node root) {
  List<Element> elements;
  List<Element> links;
  List<Element> scripts;
  List<Element> buttons;

  if (root is DocumentFragment) {
    elements = root.querySelectorAll('*');
    links = root.querySelectorAll('a');
    scripts = root.querySelectorAll('script');
    buttons = root.querySelectorAll('button');
  } else if (root is Document) {
    _validateCanonicalLink(root.querySelector('head')!);
    elements = root.querySelectorAll('*');
    links = root.querySelectorAll('a');
    scripts = root.querySelectorAll('script');
    buttons = root.querySelectorAll('button');
  } else {
    throw AssertionError('Unknown html element type: $root');
  }

  // No inline JS attribute
  for (Element elem in elements) {
    for (final attr in elem.attributes.keys) {
      final name = attr.toString();
      if (name.toLowerCase().startsWith('on')) {
        throw AssertionError(
            'No inline JS attribute is allowed, found: ${elem.outerHtml}.');
      }
    }
  }

  // All <a target="_blank"> links must have rel="noopener"
  for (Element elem in links) {
    if (elem.attributes['target'] == '_blank') {
      if (!elem.attributes.containsKey('rel')) {
        throw AssertionError(
            '_blank links must have rel=noopener, found: ${elem.outerHtml}.');
      }
      final rel = elem.attributes['rel']!;
      if (!rel.split(' ').contains('noopener')) {
        throw AssertionError(
            '_blank links must have rel=noopener, found: ${elem.outerHtml}.');
      }
    }
  }

  // No inline script tag.
  for (Element elem in scripts) {
    if (elem.attributes['type'] == 'application/ld+json') {
      if (elem.attributes.length != 1) {
        throw AssertionError(
            'Only a single attribute is allowed on ld+json, found: ${elem.outerHtml}');
      }
      if (elem.text.trim().isEmpty) {
        throw AssertionError('ld+json element must not be empty.');
      }
      // trigger parsing of the content
      final map = json.decode(elem.text) as Map;
      final context = map['@context'];
      if (context is String) {
        final isHttpOrHttps =
            context.startsWith('http://') || context.startsWith('https://');
        if (!isHttpOrHttps) {
          throw AssertionError('Invalid @context value: $map');
        }
      }
    } else {
      final src = elem.attributes['src'];
      if (src == null || src.isEmpty) {
        throw AssertionError(
            'script tag must have src attribute, found: ${elem.parent?.outerHtml}');
      }
      if (elem.text.trim().isNotEmpty) {
        throw AssertionError(
            'script tag must text content must be empty, found: ${elem.outerHtml}');
      }
    }
  }

  // Lighthouse flags buttons that don't have text content or an `aria-label` property.
  for (final elem in buttons) {
    final text = elem.attributes['aria-label']?.trim() ?? elem.text.trim();
    if (text.isEmpty) {
      // Exempt buttons in dartdoc output:
      // TODO: remove after dartdoc content is updated.
      if (elem.outerHtml ==
          '<button id=\"sidenav-left-toggle\" type=\"button\">&nbsp;</button>') {
        continue;
      }
      throw AssertionError(
          'button tag text content or aria-label must not be empty, found: ${elem.outerHtml}');
    }
  }
}