%YAML 1.2
---
name: HTML
file_extensions:
  - html
  - htm
  - shtml
  - xhtml
first_line_match: (?i)<(!DOCTYPE\s*)?html
scope: text.html.basic

variables:
  attribute_char: (?:[^ "'>/=\x00-\x1f\x7f-\x9f])
  unquoted_attribute_value: (?:[^\s<>/''"]|/(?!>))+
  not_equals_lookahead: (?=\s*[^\s=])

  # https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state
  tag_name: '[[:alpha:]_][^/>\s]*'

  block_tag_name: |-
    (?ix:
      address|applet|article|aside|blockquote|center|dd|dir|div|dl|dt|figcaption|figure|footer|frame|frameset|h1|h2|h3|h4|h5|h6|header|iframe|menu|nav|noframes|object|ol|p|pre|section|ul
    )\b

  inline_tag_name: |-
    (?ix:
      abbr|acronym|area|audio|b|base|basefont|bdi|bdo|big|br|canvas|caption|cite|code|del|details|dfn|dialog|em|font|head|html|i|img|ins|isindex|kbd|li|link|map|mark|menu|menuitem|meta|noscript|param|picture|q|rp|rt|rtc|ruby|s|samp|script|small|source|span|strike|strong|style|sub|summary|sup|time|title|track|tt|u|var|video|wbr
    )\b

  form_tag_name: |-
    (?ix:
      button|datalist|input|label|legend|meter|optgroup|option|output|progress|select|template|textarea
    )\b

  javascript_mime_type: |-
    (?ix:
      # https://mimesniff.spec.whatwg.org/#javascript-mime-type
      (?:application|text)/(?:x-)?(?:java|ecma)script
      | text/javascript1\.[0-5]
      | text/jscript
      | text/livescript
    )

contexts:
  immediately-pop:
    - match: ''
      pop: true

  else-pop:
    - match: (?=\S)
      pop: true

  main:
    - include: comment
    - include: cdata
    - include: doctype
    - match: (<\?)(xml)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.xml.html
      push:
        - meta_scope: meta.tag.preprocessor.xml.html
        - match: '\?>'
          scope: punctuation.definition.tag.end.html
          pop: true
        - include: tag-generic-attribute
        - include: string-double-quoted
        - include: string-single-quoted
    - match: (</?)([a-z_][a-z0-9:_]*-[a-z0-9:_-]+)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.custom.html
      push:
        - meta_scope: meta.tag.custom.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (<)((?i:style))\b
      captures:
        0: meta.tag.style.begin.html
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.style.html
      push: style-css
    - match: '(<)((?i:script))\b'
      captures:
        0: meta.tag.script.begin.html
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.script.html
      push: script-javascript
    - match: (</?)((?i:body|head|html)\b)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.structure.any.html
      push:
        - meta_scope: meta.tag.structure.any.html
        - include: tag-end
        - include: tag-attributes
    - match: (</?)({{block_tag_name}})
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.block.any.html
      push:
        - meta_scope: meta.tag.block.any.html
        - include: tag-end
        - include: tag-attributes
    - match: (</?)((?i:hr)\b)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.block.any.html
      push:
        - meta_scope: meta.tag.block.any.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)((?i:form|fieldset)\b)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.block.form.html
      push:
        - meta_scope: meta.tag.block.form.html
        - include: tag-end
        - include: tag-attributes
    - match: (</?)({{inline_tag_name}})
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.inline.any.html
      push:
        - meta_scope: meta.tag.inline.any.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)({{form_tag_name}})
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.inline.form.html
      push:
        - meta_scope: meta.tag.inline.form.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)((?i:a)\b)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.inline.a.html
      push:
        - meta_scope: meta.tag.inline.a.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)((?i:col|colgroup|table|tbody|td|tfoot|th|thead|tr)\b)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.inline.table.html
      push:
        - meta_scope: meta.tag.inline.table.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)([A-Za-z0-9:_]+-[A-Za-z0-9:_-]+)
      captures:
        1: punctuation.definition.tag.begin.html
        2: invalid.illegal.uppercase-custom-tag-name.html
      push:
        - meta_scope: meta.tag.custom.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - match: (</?)([a-zA-Z0-9:]+)
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.other.html
      push:
        - meta_scope: meta.tag.other.html
        - include: tag-end-maybe-self-closing
        - include: tag-attributes
    - include: entities
    - match: <>
      scope: invalid.illegal.incomplete.html

  entities:
    - match: (&#[xX])\h+(;)
      scope: constant.character.entity.hexadecimal.html
      captures:
        1: punctuation.definition.entity.html
        2: punctuation.terminator.entity.html
    - match: (&#)[0-9]+(;)
      scope: constant.character.entity.decimal.html
      captures:
        1: punctuation.definition.entity.html
        2: punctuation.terminator.entity.html
    - match: (&)[a-zA-Z0-9]+(;)
      scope: constant.character.entity.named.html
      captures:
        1: punctuation.definition.entity.html
        2: punctuation.terminator.entity.html

  cdata:
    - match: (<!\[)(CDATA)(\[)
      captures:
        1: punctuation.definition.tag.begin.html
        2: keyword.declaration.cdata.html
        3: punctuation.definition.tag.begin.html
      push:
        - meta_scope: meta.tag.sgml.cdata.html
        - meta_content_scope: string.unquoted.cdata.html
        - match: ']]>'
          scope: punctuation.definition.tag.end.html
          pop: true
    - match: ']]>'
      scope: invalid.illegal.missing-entity.html

  comment:
    - match: (<!--)(-?>)?
      captures:
        1: punctuation.definition.comment.begin.html
        2: invalid.illegal.bad-comments-or-CDATA.html
      push:
        - meta_scope: comment.block.html
        - match: '(<!-)?(--\s*>)'
          captures:
            1: invalid.illegal.bad-comments-or-CDATA.html
            2: punctuation.definition.comment.end.html
          pop: true
        - match: '<!--(?!-?>)|--!>'
          scope: invalid.illegal.bad-comments-or-CDATA.html

  doctype:
    - match: (<!)(DOCTYPE)
      captures:
        1: punctuation.definition.tag.begin.html
        2: keyword.declaration.doctype.html
      push:
        - doctype-meta
        - doctype-content
        - doctype-content-type
        - doctype-name

  doctype-meta:
    - meta_scope: meta.tag.sgml.doctype.html
    - match: '>'
      scope: punctuation.definition.tag.end.html
      pop: true

  doctype-name:
    - match: '{{tag_name}}'
      scope: constant.language.doctype.html
      pop: true
    - include: else-pop

  doctype-content-type:
    - match: (?:PUBLIC|SYSTEM)\b
      scope: keyword.content.external.html
      pop: true
    - include: else-pop

  doctype-content:
    - match: \[
      scope: punctuation.section.brackets.begin.html
      set:
        - meta_scope: meta.brackets.html meta.internal-subset.xml.html
        - match: \]
          scope: punctuation.section.brackets.end.html
          pop: true
        - include: comment
    - include: string-double-quoted
    - include: string-single-quoted
    - include: else-pop

  style-css:
    - meta_content_scope: meta.tag.style.begin.html
    - include: style-common
    - match: '>'
      scope: punctuation.definition.tag.end.html
      set:
        - include: style-close-tag
        - match: (?=\S)
          embed: scope:source.css
          embed_scope: source.css.embedded.html
          escape: (?i)(?=(?:-->\s*)?</style)

  style-other:
    - meta_content_scope: meta.tag.style.begin.html
    - include: style-common
    - match: '>'
      scope: punctuation.definition.tag.end.html
      set:
        - include: style-close-tag

  style-close-tag:
    - match: (?i)(</)(style)(>)
      scope: meta.tag.style.end.html
      captures:
        1: punctuation.definition.tag.begin.html
        2: entity.name.tag.style.html
        3: punctuation.definition.tag.end.html
      pop: true

  style-common:
    - include: style-type-attribute
    - include: tag-attributes
    - include: tag-end-self-closing

  style-type-attribute:
    - match: (?i)\btype\b
      scope: meta.attribute-with-value.html entity.other.attribute-name.html
      set:
        - meta_content_scope: meta.tag.style.begin.html meta.attribute-with-value.html
        - match: =
          scope: punctuation.separator.key-value.html
          set:
            - meta_content_scope: meta.tag.style.begin.html meta.attribute-with-value.html
            - include: style-type-decider
        - match: (?=\S)
          set: style-css

  style-type-decider:
    - match: (?i)(?=text/css(?!{{unquoted_attribute_value}})|'text/css'|"text/css")
      set:
        - style-css
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?i)(?=>|''|"")
      set:
        - style-css
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?=\S)
      set:
        - style-other
        - tag-generic-attribute-meta
        - tag-generic-attribute-value

  script-javascript:
    - meta_content_scope: meta.tag.script.begin.html
    - include: script-common
    - match: '>'
      scope: punctuation.definition.tag.end.html
      set:
        - include: script-close-tag
        - match: (?=\S)
          embed: scope:source.js
          embed_scope: source.js.embedded.html
          escape: (?i)(?=(?:-->\s*)?</script)

  script-html:
    - meta_content_scope: meta.tag.script.begin.html
    - include: script-common
    - match: '>'
      scope: punctuation.definition.tag.end.html
      set:
        - meta_content_scope: text.html.embedded.html
        - include: main
        - include: script-close-tag

  script-other:
    - meta_content_scope: meta.tag.script.begin.html
    - include: script-common
    - match: '>'
      scope: punctuation.definition.tag.end.html
      set:
        - include: script-close-tag

  script-close-tag:
    - match: <!--
      scope: comment.block.html punctuation.definition.comment.begin.html
    - match: (?i)(?:(-->)\s*)?(</)(script)(>)
      scope: meta.tag.script.end.html
      captures:
        1: comment.block.html punctuation.definition.comment.end.html
        2: punctuation.definition.tag.begin.html
        3: entity.name.tag.script.html
        4: punctuation.definition.tag.end.html
      pop: true

  script-common:
    - include: script-type-attribute
    - include: tag-attributes
    - include: tag-end-self-closing

  script-type-attribute:
    - match: (?i)\btype\b
      scope: meta.attribute-with-value.html entity.other.attribute-name.html
      set:
        - meta_content_scope: meta.tag.script.begin.html meta.attribute-with-value.html
        - match: =
          scope: punctuation.separator.key-value.html
          set:
            - meta_content_scope: meta.tag.script.begin.html meta.attribute-with-value.html
            - include: script-type-decider
        - match: (?=\S)
          set: script-javascript

  script-type-decider:
    - match: (?i)(?={{javascript_mime_type}}(?!{{unquoted_attribute_value}})|'{{javascript_mime_type}}'|"{{javascript_mime_type}}")
      set:
        - script-javascript
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?i)(?=module(?!{{unquoted_attribute_value}})|'module'|"module")
      set:
        - script-javascript
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?i)(?=>|''|"")
      set:
        - script-javascript
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?i)(?=text/html(?!{{unquoted_attribute_value}})|'text/html'|"text/html")
      set:
        - script-html
        - tag-generic-attribute-meta
        - tag-generic-attribute-value
    - match: (?=\S)
      set:
        - script-other
        - tag-generic-attribute-meta
        - tag-generic-attribute-value

  string-double-quoted:
    - match: '"'
      scope: punctuation.definition.string.begin.html
      push:
        - meta_scope: string.quoted.double.html
        - match: '"'
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
  string-single-quoted:
    - match: "'"
      scope: punctuation.definition.string.begin.html
      push:
        - meta_scope: string.quoted.single.html
        - match: "'"
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities

  tag-generic-attribute:
    - match: (?={{attribute_char}})
      scope: entity.other.attribute-name.html
      push:
        - tag-generic-attribute-meta
        - tag-generic-attribute-equals
        - generic-attribute-name

  generic-attribute-name:
    - meta_scope: entity.other.attribute-name.html
    - match: (?!{{attribute_char}})
      pop: true

  tag-generic-attribute-meta:
    - meta_scope: meta.attribute-with-value.html
    - include: immediately-pop

  tag-generic-attribute-equals:
    - match: '='
      scope: punctuation.separator.key-value.html
      set: tag-generic-attribute-value
    - match: '{{not_equals_lookahead}}'
      pop: true

  tag-generic-attribute-value:
    - match: '"'
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.double.html
        - match: '"'
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: "'"
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.single.html
        - match: "'"
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: '{{unquoted_attribute_value}}'
      scope: string.unquoted.html
      pop: true
    - include: else-pop

  tag-class-attribute:
    - match: '\bclass\b'
      scope: entity.other.attribute-name.class.html
      push:
        - tag-class-attribute-meta
        - tag-class-attribute-equals

  tag-class-attribute-meta:
    - meta_scope: meta.attribute-with-value.class.html
    - include: immediately-pop

  tag-class-attribute-equals:
    - match: '='
      scope: punctuation.separator.key-value.html
      set: tag-class-attribute-value
    - match: '{{not_equals_lookahead}}'
      pop: true

  tag-class-attribute-value:
    - match: '"'
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.double.html
        - meta_content_scope: meta.class-name.html
        - match: '"'
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: "'"
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.single.html
        - meta_content_scope: meta.class-name.html
        - match: "'"
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: '{{unquoted_attribute_value}}'
      scope: string.unquoted.html meta.class-name.html
      pop: true
    - include: else-pop

  tag-id-attribute:
    - match: '\bid\b'
      scope: entity.other.attribute-name.id.html
      push:
        - tag-id-attribute-meta
        - tag-id-attribute-equals

  tag-id-attribute-meta:
    - meta_scope: meta.attribute-with-value.id.html
    - include: immediately-pop

  tag-id-attribute-equals:
    - match: '='
      scope: punctuation.separator.key-value.html
      set: tag-id-attribute-value
    - match: '{{not_equals_lookahead}}'
      pop: true

  tag-id-attribute-value:
    - match: '"'
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.double.html
        - meta_content_scope: meta.toc-list.id.html
        - match: '"'
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: "'"
      scope: punctuation.definition.string.begin.html
      set:
        - meta_scope: string.quoted.single.html
        - meta_content_scope: meta.toc-list.id.html
        - match: "'"
          scope: punctuation.definition.string.end.html
          pop: true
        - include: entities
    - match: '{{unquoted_attribute_value}}'
      scope: string.unquoted.html meta.toc-list.id.html
      pop: true
    - include: else-pop

  tag-style-attribute:
    - match: '\bstyle\b'
      scope: entity.other.attribute-name.style.html
      push:
        - tag-style-attribute-meta
        - tag-style-attribute-equals

  tag-style-attribute-meta:
    - meta_scope: meta.attribute-with-value.style.html
    - include: immediately-pop

  tag-style-attribute-equals:
    - match: '='
      scope: punctuation.separator.key-value.html
      set: tag-style-attribute-value
    - match: '{{not_equals_lookahead}}'
      pop: true

  tag-style-attribute-value:
    - match: '"'
      scope: string.quoted.double punctuation.definition.string.begin.html
      embed: scope:source.css#rule-list-body
      embed_scope: source.css
      escape: '"'
      escape_captures:
        0: string.quoted.double punctuation.definition.string.end.html
    - match: "'"
      scope: string.quoted.single punctuation.definition.string.begin.html
      embed: scope:source.css#rule-list-body
      embed_scope: source.css
      escape: "'"
      escape_captures:
        0: string.quoted.single punctuation.definition.string.end.html
    - include: else-pop

  tag-event-attribute:
    - match: |-
        (?x)\bon(
          abort|autocomplete|autocompleteerror|auxclick|blur|cancel|canplay
          |canplaythrough|change|click|close|contextmenu|cuechange|dblclick|drag
          |dragend|dragenter|dragexit|dragleave|dragover|dragstart|drop
          |durationchange|emptied|ended|error|focus|input|invalid|keydown
          |keypress|keyup|load|loadeddata|loadedmetadata|loadstart|mousedown
          |mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|mousewheel
          |pause|play|playing|progress|ratechange|reset|resize|scroll|seeked
          |seeking|select|show|sort|stalled|submit|suspend|timeupdate|toggle
          |volumechange|waiting
        )\b
      scope: entity.other.attribute-name.event.html
      push:
        - tag-event-attribute-meta
        - tag-event-attribute-equals

  tag-event-attribute-meta:
    - meta_scope: meta.attribute-with-value.event.html
    - include: immediately-pop

  tag-event-attribute-equals:
    - match: '='
      scope: punctuation.separator.key-value.html
      set: tag-event-attribute-value
    - match: '{{not_equals_lookahead}}'
      pop: true

  tag-event-attribute-value:
    - match: '"'
      scope: string.quoted.double punctuation.definition.string.begin.html
      embed: scope:source.js
      embed_scope: meta.attribute-with-value.event.html
      escape: '"'
      escape_captures:
        0: string.quoted.double punctuation.definition.string.end.html
    - match: "'"
      scope: string.quoted.single punctuation.definition.string.begin.html meta.attribute-with-value.event.html
      embed: scope:source.js
      embed_scope: meta.attribute-with-value.event.html
      escape: "'"
      escape_captures:
        0: string.quoted.single punctuation.definition.string.end.html
    - include: else-pop

  # This is to prevent breaking syntaxes referencing the old context name
  tag-stuff:
    - include: tag-attributes

  tag-attributes:
    - include: tag-id-attribute
    - include: tag-class-attribute
    - include: tag-style-attribute
    - include: tag-event-attribute
    - include: tag-generic-attribute

  tag-end:
    - match: '>'
      scope: punctuation.definition.tag.end.html
      pop: true

  tag-end-self-closing:
    - match: '/>'
      scope: punctuation.definition.tag.end.html
      pop: true

  tag-end-maybe-self-closing:
    - match: '/?>'
      scope: punctuation.definition.tag.end.html
      pop: true