%YAML 1.2
---
name: JSX
scope: source.jsx
version: 2

extends: JavaScript.sublime-syntax

file_extensions:
  - jsx

first_line_match: |-
  (?xi:
    ^ \s* // .*? -\*- .*? \bjsx\b .*? -\*-  # editorconfig
  )

variables:
  jsx_identifier_part: (?:{{identifier_part}}|-)
  jsx_identifier_break: (?!{{jsx_identifier_part}})
  jsx_identifier: '{{identifier_start}}{{jsx_identifier_part}}*{{jsx_identifier_break}}'

  jsx_tag_name_start: '{{identifier_start}}'

contexts:
  expression-begin:
    - meta_prepend: true
    - include: jsx-tag

  jsx-interpolation:
    - match: (?={/\*)
      branch_point: jsx-interpolation-comment
      branch:
        - jsx-interpolation-comment
        - jsx-interpolation-plain
    - match: (?={)
      push: jsx-interpolation-plain

  jsx-interpolation-comment:
    - match: ({)(/\*)
      captures:
        1: punctuation.section.interpolation.begin.js
        2: punctuation.definition.comment.begin.js
      set: jsx-interpolation-comment-body

  jsx-interpolation-comment-body:
    - meta_include_prototype: false
    - meta_scope: meta.interpolation.js comment.block.js
    - match: (\*/)(})
      captures:
        1: punctuation.definition.comment.end.js
        2: punctuation.section.interpolation.end.js
      pop: 1
    - match: (?=\*/)
      fail: jsx-interpolation-comment

  jsx-interpolation-plain:
    - match: \{
      scope: punctuation.section.interpolation.begin.js
      set: jsx-interpolation-plain-body

  jsx-interpolation-plain-body:
    - meta_scope: meta.interpolation.js
    - meta_content_scope: source.js.embedded.jsx
    - match: \}
      scope: punctuation.section.interpolation.end.js
      pop: 1
    - include: expressions

  jsx-meta:
    - meta_include_prototype: false
    - meta_scope: meta.jsx.js
    - include: immediately-pop

  jsx-tag:
    - match: <(?=\s*[/>{{jsx_tag_name_start}}])
      scope: punctuation.definition.tag.begin.js
      set:
        - jsx-meta
        - jsx-tag-begin

  jsx-tag-begin:
    - meta_include_prototype: false
    - meta_scope: meta.tag.js
    - match: /
      scope: punctuation.definition.tag.begin.js
      set:
        - jsx-expect-unmatched-tag-end
        - jsx-tag-name
    - match: (?=\S)
      set:
        - jsx-tag-attributes
        - jsx-tag-name

  jsx-expect-unmatched-tag-end:
    - meta_include_prototype: false
    - meta_scope: invalid.illegal.unmatched-tag.js
    - meta_content_scope: meta.tag.js
    - include: jsx-expect-tag-end

  jsx-child-tag:
    - match: <(?=\s*[/>{{jsx_tag_name_start}}])
      scope: punctuation.definition.tag.begin.js
      set: jsx-child-tag-begin

  jsx-child-tag-begin:
    - meta_include_prototype: false
    - meta_scope: meta.tag.js
    - match: /
      scope: punctuation.definition.tag.begin.js
      set:
        - jsx-expect-tag-end
        - jsx-tag-name
    - match: (?=\S)
      set:
        - jsx-body
        - jsx-tag-attributes
        - jsx-tag-name

  jsx-expect-tag-end:
    - meta_content_scope: meta.tag.js
    - match: '>{2,}' # ignore invalid punctuation
    - match: '>'
      scope: meta.tag.js punctuation.definition.tag.end.js
      pop: 1
    - include: else-pop

  jsx-tag-attributes:
    - meta_content_scope: meta.tag.attributes.js
    - match: '>{2,}' # ignore invalid punctuation
    - match: '>'
      scope: meta.tag.js punctuation.definition.tag.end.js
      set: jsx-body
    - match: '/'
      scope: meta.tag.js punctuation.definition.tag.end.js
      set: jsx-expect-tag-end
    - match: '{{jsx_identifier}}'
      scope: entity.other.attribute-name.js
    - match: '='
      scope: punctuation.separator.key-value.js
    - match: \"
      scope: punctuation.definition.string.begin.js
      push: jsx-double-quoted-string-body
    - match: \'
      scope: punctuation.definition.string.begin.js
      push: jsx-single-quoted-string-body
    - include: jsx-interpolation

  jsx-double-quoted-string-body:
    - meta_include_prototype: false
    - meta_scope: meta.string.js string.quoted.double.js
    - match: \"
      scope: punctuation.definition.string.end.js
      pop: 1
    - include: jsx-html-escapes

  jsx-single-quoted-string-body:
    - meta_include_prototype: false
    - meta_scope: meta.string.js string.quoted.single.js
    - match: \'
      scope: punctuation.definition.string.end.js
      pop: 1
    - include: jsx-html-escapes

  jsx-html-escapes:
    - match: (&)#?[[:alnum:]]+(;)
      scope: constant.character.escape.js
      captures:
        1: punctuation.definition.entity.js
        2: punctuation.definition.entity.js

  jsx-tag-name:
    - meta_include_prototype: false
    - match: ''
      set:
        - jsx-tag-name-meta
        - jsx-tag-name-end
        - jsx-tag-name-component-possibly-native

  jsx-tag-name-meta:
    - clear_scopes: 1
    - meta_include_prototype: false
    - meta_scope: meta.tag.name.js
    - include: immediately-pop

  jsx-tag-name-end:
    - match: '[:.]'
      scope: punctuation.accessor.js
      push: jsx-tag-name-component
    - include: else-pop

  jsx-tag-name-component:
    - match: '{{jsx_identifier}}'
      scope: entity.name.tag.component.js
      pop: 1
    - include: else-pop

  jsx-tag-name-component-possibly-native:
    - match: '[[:lower:]]{{jsx_identifier_part}}*{{jsx_identifier_break}}(?!{{nothing}}[.:])'
      scope: entity.name.tag.native.js
      pop: 1
    - include: jsx-tag-name-component

  jsx-body:
    - meta_include_prototype: false
    - include: jsx-child-tag
    - include: jsx-html-escapes
    - include: jsx-interpolation