%YAML 1.2
---
# Typst syntax highlighting for Fresh editor
# Based on the official Typst TextMate grammar from https://github.com/typst/typst
name: Typst
file_extensions:
  - typ
scope: source.typst

contexts:
  main:
    - include: comments
    - include: markup

  comments:
    # Block comments (nestable)
    - match: '/\*'
      scope: punctuation.definition.comment.typst
      push:
        - meta_scope: comment.block.typst
        - match: '\*/'
          scope: punctuation.definition.comment.typst
          pop: true
        - include: comments
    # Line comments
    - match: '//.*$'
      scope: comment.line.double-slash.typst

  markup:
    - include: comments
    - include: escape
    - include: headings
    - include: raw-block
    - include: raw-inline
    - include: math
    - include: bold
    - include: italic
    - include: link
    - include: label
    - include: reference
    - include: list-unnumbered
    - include: list-numbered
    - include: list-description
    - include: hash-keywords
    - include: hash-function
    - include: hash-variable
    - include: special-chars

  escape:
    - match: '\\([\\\/\[\]{}#*_=~`$\-.]|u\{[0-9a-zA-Z]*\})'
      scope: constant.character.escape.content.typst

  special-chars:
    # Em dash
    - match: '---'
      scope: constant.character.escape.typst
    # En dash
    - match: '--'
      scope: constant.character.escape.typst
    # Ellipsis
    - match: '\.\.\.'
      scope: constant.character.escape.typst
    # Non-breaking space
    - match: '~'
      scope: constant.character.escape.typst

  headings:
    - match: '^\s*(=+)\s+'
      captures:
        1: punctuation.definition.heading.typst
      push:
        - meta_scope: markup.heading.typst
        - match: '$'
          pop: true
        - include: markup

  bold:
    - match: '\*'
      scope: punctuation.definition.bold.typst
      push:
        - meta_scope: markup.bold.typst
        - match: '\*'
          scope: punctuation.definition.bold.typst
          pop: true
        - include: markup

  italic:
    - match: '(?<=\s|^)_(?=\S)'
      scope: punctuation.definition.italic.typst
      push:
        - meta_scope: markup.italic.typst
        - match: '_'
          scope: punctuation.definition.italic.typst
          pop: true
        - match: '$'
          pop: true
        - include: markup

  raw-block:
    - match: '(`{3,})\s*(\w+)?$'
      captures:
        1: punctuation.definition.raw.typst
        2: constant.other.language-name.typst
      push:
        - meta_scope: markup.raw.block.typst
        - match: '\1'
          scope: punctuation.definition.raw.typst
          pop: true

  raw-inline:
    - match: '`'
      scope: punctuation.definition.raw.typst
      push:
        - meta_scope: markup.raw.inline.typst
        - match: '`'
          scope: punctuation.definition.raw.typst
          pop: true

  math:
    - match: '\$'
      scope: punctuation.definition.string.math.typst
      push:
        - meta_scope: string.other.math.typst
        - match: '\$'
          scope: punctuation.definition.string.math.typst
          pop: true

  link:
    - match: 'https?://[0-9a-zA-Z~/%#&='',.;\+\?\-_]*'
      scope: markup.underline.link.typst

  label:
    - match: '<[a-zA-Z_][a-zA-Z0-9_\-]*>'
      scope: entity.other.label.typst

  reference:
    - match: '(@)[a-zA-Z_][a-zA-Z0-9_\-]*'
      scope: entity.other.reference.typst
      captures:
        1: punctuation.definition.reference.typst

  list-unnumbered:
    - match: '^\s*(-)\s+'
      captures:
        1: punctuation.definition.list.unnumbered.typst

  list-numbered:
    - match: '^\s*([0-9]*\.|\+)\s+'
      captures:
        1: punctuation.definition.list.numbered.typst

  list-description:
    - match: '^\s*(/)\s+([^:]*:)'
      captures:
        1: punctuation.definition.list.description.typst
        2: markup.list.term.typst

  hash-keywords:
    # Statement keywords that start code mode until end of line
    - match: '(#)(let|set|show|context)\b'
      captures:
        0: keyword.other.typst
        1: punctuation.definition.keyword.typst
      push: code-line
    # Control flow
    - match: '(#)(if|else)\b'
      captures:
        0: keyword.control.conditional.typst
        1: punctuation.definition.keyword.typst
      push: code-line
    # Loops
    - match: '(#)(for|while)\b'
      captures:
        0: keyword.control.loop.typst
        1: punctuation.definition.keyword.typst
      push: code-line
    # Break/continue
    - match: '(#)(break|continue)\b'
      captures:
        0: keyword.control.loop.typst
        1: punctuation.definition.keyword.typst
    # Import/include
    - match: '(#)(import|include|export)\b'
      captures:
        0: keyword.control.import.typst
        1: punctuation.definition.keyword.typst
      push: code-line
    # Return
    - match: '(#)(return)\b'
      captures:
        0: keyword.control.flow.typst
        1: punctuation.definition.keyword.typst
    # as/in standalone keywords
    - match: '(#)(as|in)\b'
      captures:
        0: keyword.other.typst
        1: punctuation.definition.keyword.typst

  hash-function:
    # Function call with parentheses or content block
    - match: '(#)[a-zA-Z_][a-zA-Z0-9_\-]*!?(?=[\[\(])'
      scope: entity.name.function.typst
      captures:
        1: punctuation.definition.function.typst

  hash-variable:
    # Variable interpolation
    - match: '(#)[a-zA-Z_][.a-zA-Z0-9_\-]*'
      scope: entity.other.interpolated.typst
      captures:
        1: punctuation.definition.variable.typst

  code-line:
    - match: '$'
      pop: true
    - match: '(?=\])'
      pop: true
    - include: code

  code:
    - include: comments
    # Code block
    - match: '\{'
      scope: punctuation.definition.block.code.typst
      push:
        - match: '\}'
          scope: punctuation.definition.block.code.typst
          pop: true
        - include: code
    # Content block (back to markup mode)
    - match: '\['
      scope: punctuation.definition.block.content.typst
      push:
        - match: '\]'
          scope: punctuation.definition.block.content.typst
          pop: true
        - include: markup
    # Parenthesized group
    - match: '\('
      scope: punctuation.definition.group.typst
      push:
        - match: '\)'
          scope: punctuation.definition.group.typst
          pop: true
        - include: arguments
    # Strings
    - match: '"'
      scope: punctuation.definition.string.typst
      push:
        - meta_scope: string.quoted.double.typst
        - match: '"'
          scope: punctuation.definition.string.typst
          pop: true
        - match: '\\([\\\"nrt]|u\{?[0-9a-zA-Z]*\}?)'
          scope: constant.character.escape.string.typst
    # Math in code
    - match: '\$'
      scope: punctuation.definition.string.math.typst
      push:
        - meta_scope: string.other.math.typst
        - match: '\$'
          scope: punctuation.definition.string.math.typst
          pop: true
    # Code keywords
    - match: '\b(let|as|in|set|show|context)\b'
      scope: keyword.other.typst
    - match: '\b(if|else)\b'
      scope: keyword.control.conditional.typst
    - match: '\b(for|while|break|continue)\b'
      scope: keyword.control.loop.typst
    - match: '\b(import|include|export)\b'
      scope: keyword.control.import.typst
    - match: '\b(return)\b'
      scope: keyword.control.flow.typst
    # Constants
    - match: '\bnone\b'
      scope: constant.language.none.typst
    - match: '\bauto\b'
      scope: constant.language.auto.typst
    - match: '\b(true|false)\b'
      scope: constant.language.boolean.typst
    # Numbers with units
    - match: '\b(\d*)?\.?\d+([eE][+-]?\d+)?(mm|pt|cm|in|em)\b'
      scope: constant.numeric.length.typst
    - match: '\b(\d*)?\.?\d+([eE][+-]?\d+)?(rad|deg)\b'
      scope: constant.numeric.angle.typst
    - match: '\b(\d*)?\.?\d+([eE][+-]?\d+)?%'
      scope: constant.numeric.percentage.typst
    - match: '\b(\d*)?\.?\d+([eE][+-]?\d+)?fr'
      scope: constant.numeric.fr.typst
    - match: '\b0x[0-9a-fA-F]+\b'
      scope: constant.numeric.integer.typst
    - match: '\b0b[01]+\b'
      scope: constant.numeric.integer.typst
    - match: '\b0o[0-7]+\b'
      scope: constant.numeric.integer.typst
    - match: '\b\d+\.\d+([eE][+-]?\d+)?\b'
      scope: constant.numeric.float.typst
    - match: '\b\d+\b'
      scope: constant.numeric.integer.typst
    # Operators
    - match: '=>|\.\.'
      scope: keyword.operator.typst
    - match: '==|!=|<=|<|>=|>'
      scope: keyword.operator.relational.typst
    - match: '\+=|-=|\*=|/=|='
      scope: keyword.operator.assignment.typst
    - match: '\+|-|\*|/'
      scope: keyword.operator.arithmetic.typst
    - match: '\b(and|or|not)\b'
      scope: keyword.operator.word.typst
    # Function calls
    - match: '\b[a-zA-Z_][a-zA-Z0-9_\-]*!?(?=[\[\(])'
      scope: entity.name.function.typst
    # Variables
    - match: '\b[a-zA-Z_][a-zA-Z0-9_\-]*\b'
      scope: variable.other.typst

  arguments:
    # Named arguments
    - match: '\b[a-zA-Z_][a-zA-Z0-9_\-]*(?=\s*:)'
      scope: variable.parameter.typst
    - include: code
