diff --git a/src/highlight.rs b/src/highlight.rs
index a1b2c86..a2243ae 100644
--- a/src/highlight.rs
+++ b/src/highlight.rs
@@ -284,6 +284,23 @@ mod tests {
);
}
+ #[test]
+ fn highlight_code_jsx_tags_and_attributes() {
+ // JSX 标签名与属性名都应被识别。
+ let code = "const el = ;";
+ for lang in &["jsx", "tsx"] {
+ let result = highlight_code(code, Some(lang));
+ assert!(
+ result.contains("entity name tag"),
+ "{lang} JSX 标签名未识别: {result}"
+ );
+ assert!(
+ result.contains("attribute"),
+ "{lang} JSX 属性名未识别: {result}"
+ );
+ }
+ }
+
#[test]
fn highlight_code_typescript_resolves_ts_alias() {
// 别名 "ts" 与 "typescript" 输出应一致。
diff --git a/syntaxes/JSX.sublime-syntax b/syntaxes/JSX.sublime-syntax
new file mode 100644
index 0000000..5694fdd
--- /dev/null
+++ b/syntaxes/JSX.sublime-syntax
@@ -0,0 +1,227 @@
+%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
diff --git a/syntaxes/TSX.sublime-syntax b/syntaxes/TSX.sublime-syntax
new file mode 100644
index 0000000..de3d352
--- /dev/null
+++ b/syntaxes/TSX.sublime-syntax
@@ -0,0 +1,270 @@
+%YAML 1.2
+---
+# https://www.typescriptlang.org/docs/handbook/jsx.html
+# TypeScript + JSX (TSX). Based on TypeScript syntax but with JSX tag support.
+
+name: TSX
+scope: source.tsx
+
+file_extensions:
+ - tsx
+
+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: modifiers
+ - include: type-declaration
+ - include: function-declaration
+ - include: types
+ - include: constants
+ - include: function-calls
+ - include: property-access
+ - include: punctuation
+ - include: identifiers
+
+ whitespace:
+ - match: '[\t ]+'
+
+ comments:
+ - match: '/\*'
+ scope: punctuation.definition.comment.tsx
+ push:
+ - meta_scope: comment.block.tsx
+ - match: '\*/'
+ scope: punctuation.definition.comment.tsx
+ pop: 1
+ - match: '//'
+ scope: punctuation.definition.comment.tsx
+ push:
+ - meta_scope: comment.line.double-slash.tsx
+ - match: '{{line_break}}'
+ pop: 1
+
+ decorator:
+ - match: '@{{identifier}}(?:\.{{identifier}})*'
+ scope: entity.other.attribute-name.tsx
+
+ strings:
+ - match: '`'
+ scope: punctuation.definition.string.begin.tsx
+ push:
+ - meta_scope: string.quoted.other.tsx
+ - match: '\$\{'
+ scope: punctuation.section.interpolation.begin.tsx
+ push:
+ - clear_scopes: 1
+ - meta_scope: meta.interpolation.tsx
+ - include: main
+ - match: '\}'
+ scope: punctuation.section.interpolation.end.tsx
+ pop: 1
+ - match: '\\[nrt`\\$]'
+ scope: constant.character.escape.tsx
+ - match: '`'
+ scope: punctuation.definition.string.end.tsx
+ pop: 1
+ - match: '"'
+ scope: punctuation.definition.string.begin.tsx
+ push:
+ - meta_scope: string.quoted.double.tsx
+ - match: '\\[nrt"\\]'
+ scope: constant.character.escape.tsx
+ - match: '"'
+ scope: punctuation.definition.string.end.tsx
+ pop: 1
+ - match: '{{line_break}}'
+ scope: invalid.illegal.unclosed-string.tsx
+ pop: 1
+ - match: "'"
+ scope: punctuation.definition.string.begin.tsx
+ push:
+ - meta_scope: string.quoted.single.tsx
+ - match: "\\\\[nrt'\\\\]"
+ scope: constant.character.escape.tsx
+ - match: "'"
+ scope: punctuation.definition.string.end.tsx
+ pop: 1
+
+ numbers:
+ - match: '\b0[xX][0-9A-Fa-f_]+'
+ scope: constant.numeric.integer.hexadecimal.tsx
+ - match: '\b0[bB][01_]+'
+ scope: constant.numeric.integer.binary.tsx
+ - match: '\b0[oO][0-7_]+'
+ scope: constant.numeric.integer.octal.tsx
+ - match: '\b[0-9][0-9_]*(?:\.[0-9][0-9_]*)?(?:[eE][-+]?[0-9]+)?\b'
+ scope: constant.numeric.decimal.tsx
+
+ declaration-keywords:
+ - match: '\b(?:var|let|const|function|class|interface|type|enum|namespace|module|import|export|from|as|default)\b'
+ scope: keyword.declaration.tsx
+ - match: '\b(?:new|delete|typeof|instanceof|in|of|keyof|infer|is|readonly|asserts)\b'
+ scope: keyword.operator.type.tsx
+
+ control-keywords:
+ - match: '\b(?:if|else|for|while|do|switch|case|break|continue|return|throw|try|catch|finally|await|yield)\b'
+ scope: keyword.control.tsx
+ - match: '\b(?:async|await)\b'
+ scope: keyword.control.async.tsx
+ - match: '\b(?:true|false|null|undefined|this|super)\b'
+ scope: constant.language.tsx
+
+ modifiers:
+ - match: '\b(?:public|private|protected|static|abstract|readonly|override|declare|get|set)\b'
+ scope: storage.modifier.tsx
+
+ type-declaration:
+ - match: '\b(type)\s+({{identifier}})'
+ captures:
+ 1: keyword.declaration.tsx
+ 2: entity.name.type.tsx
+ - match: '\b(interface)\s+({{identifier}})'
+ captures:
+ 1: keyword.declaration.tsx
+ 2: entity.name.type.tsx
+ - match: '\b(enum)\s+({{identifier}})'
+ captures:
+ 1: keyword.declaration.tsx
+ 2: entity.name.type.tsx
+
+ function-declaration:
+ - match: '\b(function)\s+\*?\s*({{identifier}})'
+ captures:
+ 1: keyword.declaration.tsx
+ 2: entity.name.function.tsx
+
+ types:
+ - match: '\b(?:string|number|boolean|void|object|symbol|bigint|never|unknown|any)\b'
+ scope: support.type.primitive.tsx
+ - match: '\b(?:Array|Promise|Map|Set|Date|RegExp|Error|Record|Partial|Required|Readonly|Pick|Omit|ReturnType|Parameters|InstanceType|ReadonlyArray)\b'
+ scope: support.type.tsx
+
+ constants:
+ - match: '\b(?:console|Math|JSON|Object|Number|String|Boolean|Symbol|window|document|globalThis|process)\b'
+ scope: support.class.tsx
+ - match: '\b[A-Z][A-Z0-9_]+\b'
+ scope: variable.other.constant.tsx
+
+ function-calls:
+ - match: '\b({{identifier}})\s*(?=\()'
+ captures:
+ 1: variable.function.tsx
+
+ property-access:
+ - match: '\.({{identifier}})\s*(?=\()'
+ captures:
+ 1: variable.function.tsx
+ - match: '\.({{identifier}})\b'
+ captures:
+ 1: variable.other.property.tsx
+
+ punctuation:
+ - match: '[\(\)\{\}\[\];,]'
+ scope: punctuation.tsx
+ - match: '=>'
+ scope: storage.type.function.arrow.tsx
+ - match: '[-+*/%=<>!&|?~^]+'
+ scope: keyword.operator.tsx
+
+ identifiers:
+ - match: '\b[A-Z][A-Za-z0-9_$]*\b'
+ scope: entity.name.type.tsx
+ - match: '{{identifier}}'
+ scope: variable.tsx
+
+ # ============ JSX 规则 ============
+ jsx:
+ # 自闭合标签