yggdrasil/syntaxes/JSX.sublime-syntax
xfy c6d27f0ae0 feat(highlight): 新增 JSX 与 TSX 语法高亮
新建 syntaxes/JSX.sublime-syntax(纯 JS + JSX)与
syntaxes/TSX.sublime-syntax(TypeScript + JSX),两者共享同一套
JSX 规则:标签名(区分大写组件/小写 HTML 标签)、属性名、属性值
(字符串与 {expr} 表达式)、自闭合 />、闭合标签、嵌套子标签与
JSX 片段 <>...</>。

从 TypeScript.sublime-syntax 移除 tsx 扩展名:此前 tsx 命中的是
没有 JSX 支持的 TS 语法,遮蔽了新建的 TSX 语法。

新增回归测试覆盖 jsx/tsx 的标签名与属性名识别。
2026-06-16 15:45:38 +08:00

228 lines
6.6 KiB
YAML

%YAML 1.2
---
# https://react.dev/learn/writing-markup-with-jsx
# JavaScript + JSX. Pure JS (no TypeScript types) with JSX tag support.
name: JSX
scope: source.jsx
file_extensions:
- jsx
variables:
identifier: '[A-Za-z_$][A-Za-z0-9_$]*'
line_break: '(?:\x0D\x0A?|\x0A)'
contexts:
main:
- include: whitespace
- include: comments
- include: decorator
- include: jsx
- include: strings
- include: numbers
- include: declaration-keywords
- include: control-keywords
- include: constants
- include: function-calls
- include: property-access
- include: punctuation
- include: identifiers
whitespace:
- match: '[\t ]+'
comments:
- match: '/\*'
scope: punctuation.definition.comment.jsx
push:
- meta_scope: comment.block.jsx
- match: '\*/'
scope: punctuation.definition.comment.jsx
pop: 1
- match: '//'
scope: punctuation.definition.comment.jsx
push:
- meta_scope: comment.line.double-slash.jsx
- match: '{{line_break}}'
pop: 1
decorator:
- match: '@{{identifier}}(?:\.{{identifier}})*'
scope: entity.other.attribute-name.jsx
strings:
- match: '`'
scope: punctuation.definition.string.begin.jsx
push:
- meta_scope: string.quoted.other.jsx
- match: '\$\{'
scope: punctuation.section.interpolation.begin.jsx
push:
- clear_scopes: 1
- meta_scope: meta.interpolation.jsx
- include: main
- match: '\}'
scope: punctuation.section.interpolation.end.jsx
pop: 1
- match: '\\[nrt`\\$]'
scope: constant.character.escape.jsx
- match: '`'
scope: punctuation.definition.string.end.jsx
pop: 1
- match: '"'
scope: punctuation.definition.string.begin.jsx
push:
- meta_scope: string.quoted.double.jsx
- match: '\\[nrt"\\]'
scope: constant.character.escape.jsx
- match: '"'
scope: punctuation.definition.string.end.jsx
pop: 1
- match: '{{line_break}}'
scope: invalid.illegal.unclosed-string.jsx
pop: 1
- match: "'"
scope: punctuation.definition.string.begin.jsx
push:
- meta_scope: string.quoted.single.jsx
- match: "\\\\[nrt'\\\\]"
scope: constant.character.escape.jsx
- match: "'"
scope: punctuation.definition.string.end.jsx
pop: 1
numbers:
- match: '\b0[xX][0-9A-Fa-f_]+'
scope: constant.numeric.integer.hexadecimal.jsx
- match: '\b0[bB][01_]+'
scope: constant.numeric.integer.binary.jsx
- match: '\b0[oO][0-7_]+'
scope: constant.numeric.integer.octal.jsx
- match: '\b[0-9][0-9_]*(?:\.[0-9][0-9_]*)?(?:[eE][-+]?[0-9]+)?\b'
scope: constant.numeric.decimal.jsx
declaration-keywords:
- match: '\b(?:var|let|const|function|class|import|export|from|as|default|extends|implements)\b'
scope: keyword.declaration.jsx
control-keywords:
- match: '\b(?:if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally|await|yield|new|delete|typeof|instanceof|in|of)\b'
scope: keyword.control.jsx
- match: '\b(?:async|await)\b'
scope: keyword.control.async.jsx
- match: '\b(?:true|false|null|undefined|this|super)\b'
scope: constant.language.jsx
constants:
- match: '\b(?:console|Math|JSON|Object|Number|String|Boolean|Symbol|window|document|globalThis|process|React)\b'
scope: support.class.jsx
- match: '\b[A-Z][A-Z0-9_]+\b'
scope: variable.other.constant.jsx
function-calls:
- match: '\b({{identifier}})\s*(?=\()'
captures:
1: variable.function.jsx
property-access:
- match: '\.({{identifier}})\s*(?=\()'
captures:
1: variable.function.jsx
- match: '\.({{identifier}})\b'
captures:
1: variable.other.property.jsx
punctuation:
- match: '[\(\)\{\}\[\];,]'
scope: punctuation.jsx
- match: '=>'
scope: storage.type.function.arrow.jsx
- match: '[-+*/%=<>!&|?~^]+'
scope: keyword.operator.jsx
identifiers:
- match: '\b[A-Z][A-Za-z0-9_$]*\b'
scope: entity.name.type.jsx
- match: '{{identifier}}'
scope: variable.jsx
# ============ JSX 规则 ============
jsx:
- match: '(<)((?:[A-Z][A-Za-z0-9_$]*|[a-z][A-Za-z0-9-]*))'
captures:
1: punctuation.definition.tag.begin.jsx
2: entity.name.tag.jsx
push:
- meta_scope: meta.tag.jsx
- include: jsx-attributes
- match: '/>'
scope: punctuation.definition.tag.end.jsx
pop: 1
- match: '>'
scope: punctuation.definition.tag.end.jsx
pop: jsx-body
- match: '(</)((?:[A-Z][A-Za-z0-9_$]*|[a-z][A-Za-z0-9-]*))\s*(>)'
scope: meta.tag.jsx
captures:
1: punctuation.definition.tag.begin.jsx
2: entity.name.tag.jsx
3: punctuation.definition.tag.end.jsx
- match: '(<)(>)'
captures:
1: punctuation.definition.tag.begin.jsx
2: punctuation.definition.tag.end.jsx
push: jsx-body
jsx-attributes:
- match: '\b([A-Za-z_][A-Za-z0-9_-]*)\s*(=)'
captures:
1: entity.other.attribute-name.jsx
2: keyword.operator.assignment.jsx
push:
- match: '"'
scope: punctuation.definition.string.begin.jsx
push:
- meta_scope: string.quoted.double.jsx
- match: '"'
scope: punctuation.definition.string.end.jsx
pop: 1
- match: "'"
scope: punctuation.definition.string.begin.jsx
push:
- meta_scope: string.quoted.single.jsx
- match: "'"
scope: punctuation.definition.string.end.jsx
pop: 1
- match: '\{'
scope: punctuation.section.embedded.begin.jsx
push:
- clear_scopes: 1
- meta_scope: meta.embedded.expression.jsx
- include: main
- match: '\}'
scope: punctuation.section.embedded.end.jsx
pop: 1
- match: '(?=\S)'
pop: 1
- include: whitespace
- match: '\b[A-Za-z_][A-Za-z0-9_-]*\b'
scope: entity.other.attribute-name.jsx
jsx-body:
- meta_include_prototype: false
- match: '(?=</)'
pop: 1
- include: jsx-text-interpolation
- include: jsx
jsx-text-interpolation:
- match: '\{'
scope: punctuation.section.embedded.begin.jsx
push:
- meta_scope: meta.embedded.expression.jsx
- include: main
- match: '\}'
scope: punctuation.section.embedded.end.jsx
pop: 1