@mirascript/textmate 0.1.82 → 0.1.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { LanguageRegistration } from '@shikijs/types';
2
+ import { createMiraScriptDocGrammar, createMiraScriptGrammar, createMiraScriptTemplateGrammar } from './grammar.ts';
3
+
4
+ export const mirascript = createMiraScriptGrammar();
5
+ export const mirascriptTemplate = createMiraScriptTemplateGrammar();
6
+ export const mirascriptDoc = createMiraScriptDocGrammar();
7
+
8
+ export { mirascriptTemplate as 'mirascript-template', mirascriptDoc as 'mirascript-doc' };
9
+
10
+ export const grammars: LanguageRegistration[] = [mirascript, mirascriptTemplate, mirascriptDoc];
@@ -0,0 +1,21 @@
1
+ import type { LanguageRegistration } from '@shikijs/types';
2
+
3
+ /** Shared TextMate language metadata. */
4
+ export type LanguageMetadata = Pick<LanguageRegistration, 'name' | 'aliases' | 'scopeName'>;
5
+
6
+ export const mirascriptLanguage = {
7
+ name: 'mirascript',
8
+ aliases: ['MiraScript', 'mira', 'Mira'],
9
+ scopeName: 'source.mira',
10
+ } satisfies LanguageMetadata;
11
+
12
+ export const mirascriptTemplateLanguage = {
13
+ name: 'mirascript-template',
14
+ aliases: ['MiraScript-Template', 'miratpl', 'MiraTpl'],
15
+ scopeName: 'text.miratpl',
16
+ } satisfies LanguageMetadata;
17
+
18
+ export const mirascriptDocLanguage = {
19
+ name: 'mirascript-doc',
20
+ scopeName: 'source.mira.doc',
21
+ } satisfies LanguageMetadata;
@@ -0,0 +1,53 @@
1
+ import { createHighlighter, type Highlighter } from 'shiki';
2
+ import { INITIAL, type StateStack } from 'shiki/textmate';
3
+ import test, { type ExecutionContext } from 'ava';
4
+ import { grammars } from '../src/index.ts';
5
+
6
+ let highlighter: Highlighter;
7
+
8
+ test.before(async () => {
9
+ highlighter = await createHighlighter({
10
+ langs: grammars,
11
+ themes: [],
12
+ });
13
+ });
14
+
15
+ test.after.always(() => highlighter.dispose());
16
+
17
+ /**
18
+ * Tokenize complete source while preserving TextMate state across lines.
19
+ */
20
+ export function tokenize(
21
+ code: string,
22
+ language = 'mirascript',
23
+ ): Array<{ line: number; text: string; scopes: string[] }> {
24
+ const grammar = highlighter.getLanguage(language);
25
+ let state: StateStack = INITIAL;
26
+ return code.split('\n').flatMap((line, lineIndex) => {
27
+ const result = grammar.tokenizeLine(line, state);
28
+ state = result.ruleStack;
29
+ return result.tokens.map((token) => ({
30
+ line: lineIndex,
31
+ text: line.slice(token.startIndex, token.endIndex),
32
+ scopes: token.scopes,
33
+ }));
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Assert that a selected textual token contains the expected scope.
39
+ */
40
+ export function expectScope(
41
+ t: ExecutionContext,
42
+ tokens: Array<{ text: string; scopes: string[] }>,
43
+ text: string,
44
+ scope: string,
45
+ occurrence = 0,
46
+ ): void {
47
+ const matching = tokens.filter((token) => token.text === text);
48
+ t.true(matching.length > occurrence, `Missing token ${JSON.stringify(text)} #${occurrence}`);
49
+ t.true(
50
+ matching[occurrence].scopes.includes(scope),
51
+ `${JSON.stringify(text)} should include ${scope}; got ${matching[occurrence].scopes.join(', ')}`,
52
+ );
53
+ }
@@ -0,0 +1,157 @@
1
+ import test from 'ava';
2
+ import { mirascriptLanguage } from '../../src/language.ts';
3
+ import { tokenize, expectScope } from '../_engine.ts';
4
+
5
+ test('separates documentation prefixes from bold and italic markup', (t) => {
6
+ const tokens = tokenize(
7
+ [
8
+ '/**',
9
+ ' * - *a*: 第一个操作数',
10
+ ' * - **b**: 第二个操作数',
11
+ ' * ```mirascript',
12
+ ' * matrix.add([1, 2], [3, 4]) // [4, 6]',
13
+ ' * ```',
14
+ ' */',
15
+ ].join('\n'),
16
+ );
17
+ expectScope(t, tokens, '*a*', 'markup.italic.documentation.mira');
18
+ expectScope(t, tokens, '**b**', 'markup.bold.documentation.mira');
19
+ for (const line of [1, 2, 3, 4, 5]) {
20
+ const prefix = tokens.find((token) => token.line === line && token.text.trim() === '*');
21
+ t.truthy(prefix, `Missing documentation prefix on line ${line}`);
22
+ t.is(prefix!.scopes.at(-1), 'comment.block.documentation.mira');
23
+ }
24
+ });
25
+
26
+ test('does not consume a documentation terminator as italic markup', (t) => {
27
+ const tokens = tokenize('/**\n * *1*/\nlet x = 12;');
28
+ expectScope(t, tokens, 'let', 'keyword.declaration.mira');
29
+ expectScope(t, tokens, 'x', 'variable.other.mira');
30
+ t.false(tokens.some((token) => token.text === '*1*' && token.scopes.includes('markup.italic.documentation.mira')));
31
+ });
32
+
33
+ test('ends a documentation fence before a prefixed comment terminator', (t) => {
34
+ const tokens = tokenize('/**\n * ```mirascript\n * */\n * ```');
35
+ const trailingLine = tokens.filter((token) => token.line === 3);
36
+ t.true(trailingLine.length > 0);
37
+ for (const token of trailingLine) {
38
+ t.false(token.scopes.includes('comment.block.documentation.mira'), token.scopes.join(', '));
39
+ t.false(token.scopes.includes('markup.fenced_code.block.mira'), token.scopes.join(', '));
40
+ }
41
+ });
42
+
43
+ test('closes documentation before an embedded string can consume its terminator', (t) => {
44
+ const tokens = tokenize("/**\n * ```mirascript\n * '*/\nlet recovered = 1;");
45
+ expectScope(t, tokens, 'let', 'keyword.declaration.mira');
46
+ expectScope(t, tokens, 'recovered', 'variable.other.mira');
47
+ const recovered = tokens.find((token) => token.text === 'recovered');
48
+ t.false(recovered!.scopes.includes('comment.block.documentation.mira'));
49
+ t.false(recovered!.scopes.includes('markup.fenced_code.block.mira'));
50
+ });
51
+
52
+ test('preserves multiline embedded source state between safe documentation lines', (t) => {
53
+ const tokens = tokenize("/**\n * ```mirascript\n * 'first\n * second'\n * ```\n */");
54
+ expectScope(t, tokens, 'second', 'string.quoted.single.mira');
55
+ });
56
+
57
+ test('highlights documentation fences for the MiraScript name, aliases, and an omitted tag', (t) => {
58
+ const tags = [undefined, mirascriptLanguage.name, ...(mirascriptLanguage.aliases ?? []), 'mIrAsCrIpT'];
59
+ for (const tag of tags) {
60
+ const tokens = tokenize(['/**', ` * \`\`\`${tag ?? ''}`, ' * matrix.identity(3)', ' * ```', ' */'].join('\n'));
61
+ if (tag) expectScope(t, tokens, tag, 'fenced_code.block.language.mira');
62
+ expectScope(t, tokens, 'identity', 'entity.name.function.member.mira');
63
+ }
64
+ });
65
+
66
+ test('supports documentation fences with three or more backticks', (t) => {
67
+ for (const [openingLength, closingLength] of [
68
+ [3, 3],
69
+ [4, 4],
70
+ [8, 8],
71
+ [12, 16],
72
+ ] as const) {
73
+ const opening = '`'.repeat(openingLength);
74
+ const closing = '`'.repeat(closingLength);
75
+ const tokens = tokenize(
76
+ [
77
+ '/**',
78
+ ` * ${opening}mirascript`,
79
+ ' * matrix.identity(3)',
80
+ ` * ${closing}`,
81
+ ' */',
82
+ 'let outside = 1;',
83
+ ].join('\n'),
84
+ );
85
+ expectScope(t, tokens, 'identity', 'entity.name.function.member.mira');
86
+ expectScope(t, tokens, 'outside', 'variable.other.mira');
87
+ }
88
+ });
89
+
90
+ test('keeps unknown documentation fence tags as unparsed code blocks', (t) => {
91
+ const tokens = tokenize(
92
+ ['/**', ' * ````javascript', ' * let value = call(1);', ' * ````', ' */', 'let outside = 1;'].join('\n'),
93
+ );
94
+ expectScope(t, tokens, 'javascript', 'fenced_code.block.language.mira');
95
+ const body = tokens.find((token) => token.line === 2 && token.text.includes('let value'));
96
+ t.truthy(body);
97
+ t.true(body!.scopes.includes('markup.fenced_code.block.mira'));
98
+ t.true(body!.scopes.includes('markup.raw.block.mira'));
99
+ t.false(body!.scopes.includes('keyword.declaration.mira'));
100
+ t.false(body!.scopes.includes('entity.name.function.mira'));
101
+ expectScope(t, tokens, 'outside', 'variable.other.mira');
102
+ });
103
+
104
+ test('highlights MiraScript fenced code inside documentation comments', (t) => {
105
+ const tokens = tokenize(
106
+ [
107
+ '/**',
108
+ ' * 创建一个单位矩阵',
109
+ ' *',
110
+ ' * - `..size`: 矩阵的维度',
111
+ ' *',
112
+ ' * ### 示例',
113
+ ' * ```mirascript',
114
+ ' * matrix.identity(3) // [[1, 0, 0], [0, 1, 0], [0, 0, 1]]',
115
+ ' * ```',
116
+ ' * @returns the matrix',
117
+ ' */',
118
+ 'let outside = 2;',
119
+ ].join('\n'),
120
+ );
121
+ expectScope(t, tokens, 'mirascript', 'fenced_code.block.language.mira');
122
+ expectScope(t, tokens, 'identity', 'entity.name.function.member.mira');
123
+ expectScope(t, tokens, '3', 'constant.numeric.float.mira');
124
+ expectScope(t, tokens, '// [[1, 0, 0], [0, 1, 0], [0, 0, 1]]', 'comment.line.double-slash.mira');
125
+ expectScope(t, tokens, '@returns', 'storage.type.class.documentation.mira');
126
+ expectScope(t, tokens, 'let', 'keyword.declaration.mira');
127
+ expectScope(t, tokens, 'outside', 'variable.other.mira');
128
+ });
129
+
130
+ test('ends an unterminated documentation fence at the comment boundary', (t) => {
131
+ const tokens = tokenize('/**\n * ```mirascript\n * fn call() { nil }\n */\nlet recovered = 1;');
132
+ expectScope(t, tokens, 'call', 'entity.name.function.mira');
133
+ expectScope(t, tokens, 'let', 'keyword.declaration.mira');
134
+ expectScope(t, tokens, 'recovered', 'variable.other.mira');
135
+ const recovered = tokens.find((token) => token.text === 'recovered');
136
+ t.false(recovered!.scopes.includes('comment.block.documentation.mira'));
137
+ });
138
+
139
+ test('highlights documentation comments inside documentation mode', (t) => {
140
+ const tokens = tokenize(
141
+ [
142
+ '/**',
143
+ ' * **bold** and *italic*',
144
+ ' * ```mirascript',
145
+ ' * matrix.identity(3)',
146
+ ' * ```',
147
+ ' */',
148
+ 'fn recovered(value: number) -> number',
149
+ ].join('\n'),
150
+ 'mirascript-doc',
151
+ );
152
+ expectScope(t, tokens, '**bold**', 'markup.bold.documentation.mira');
153
+ expectScope(t, tokens, '*italic*', 'markup.italic.documentation.mira');
154
+ expectScope(t, tokens, 'identity', 'entity.name.function.member.mira');
155
+ expectScope(t, tokens, 'recovered', 'entity.name.function.mira');
156
+ expectScope(t, tokens, 'number', 'support.type.builtin.mira');
157
+ });
@@ -0,0 +1,372 @@
1
+ import test from 'ava';
2
+ import { tokenize, expectScope } from '../_engine.ts';
3
+
4
+ test('highlights types', (t) => {
5
+ const types = ['boolean', 'true', 'false', 'number', 'string', 'array', 'record', 'extern', 'any', 'nil'];
6
+ const tokens = tokenize(types.map((type) => `fn _${type}(v: ${type}) -> ${type}`).join('\n'), 'mirascript-doc');
7
+ for (const type of types) {
8
+ expectScope(t, tokens, type, 'support.type.builtin.mira');
9
+ }
10
+ });
11
+
12
+ test('highlights generated documentation syntax', (t) => {
13
+ const tokens = tokenize(
14
+ [
15
+ '\0(parameter) mut value',
16
+ '\0(parameter) plain',
17
+ '(field) description',
18
+ 'let immutable',
19
+ 'const @constant',
20
+ 'let mut mutable',
21
+ 'item: /* <extern function> */ fn(arg: number) -> string',
22
+ 'fn transform<T>(value: record<string, T>, callback: fn(result: T) -> boolean) -> T[] | nil',
23
+ 'reflected: type(MyValue)',
24
+ '(field?: number, nested: (name: string))',
25
+ ].join('\n'),
26
+ 'mirascript-doc',
27
+ );
28
+ expectScope(t, tokens, '(parameter)', 'entity.name.label.mira');
29
+ expectScope(t, tokens, '(field)', 'entity.name.label.mira');
30
+ expectScope(t, tokens, 'value', 'variable.emphasis.mira');
31
+ expectScope(t, tokens, 'plain', 'variable.other.constant.emphasis.mira');
32
+ expectScope(t, tokens, 'immutable', 'variable.other.constant.mira');
33
+ expectScope(t, tokens, '@constant', 'variable.other.constant.mira');
34
+ expectScope(t, tokens, 'mutable', 'variable.other.readwrite.mira');
35
+ expectScope(t, tokens, 'extern', 'keyword.declaration.extern.mira');
36
+ expectScope(t, tokens, 'function', 'keyword.js');
37
+ expectScope(t, tokens, 'transform', 'entity.name.function.mira');
38
+ expectScope(t, tokens, 'record', 'support.type.builtin.mira');
39
+ expectScope(t, tokens, 'callback', 'entity.name.function.emphasis.mira');
40
+ expectScope(t, tokens, 'boolean', 'support.type.builtin.mira');
41
+ expectScope(t, tokens, 'MyValue', 'variable.other.mira');
42
+ expectScope(t, tokens, 'field', 'variable.emphasis.mira');
43
+ expectScope(t, tokens, 'nested', 'variable.emphasis.mira');
44
+ expectScope(t, tokens, 'name', 'variable.other.property.mira');
45
+ t.false(
46
+ tokens.filter((token) => token.line === 9).some((token) => token.scopes.includes('entity.name.label.mira')),
47
+ );
48
+ expectScope(t, tokens, '->', 'keyword.operator.type.mira');
49
+ });
50
+
51
+ test('distinguishes doc declarations, globals, and nested function types', (t) => {
52
+ const tokens = tokenize(
53
+ [
54
+ 'mod matrix {',
55
+ ' pub fn determinant(data: array | record) -> number',
56
+ '}',
57
+ '\0(global) mod matrix',
58
+ '\0PI',
59
+ '\0(global) PI',
60
+ '\0(global) fn map(',
61
+ ' data: array | record,',
62
+ ' f: fn(value: any, key: number | string, input: type(data)) -> any,',
63
+ ') -> type(data)',
64
+ ].join('\n'),
65
+ 'mirascript-doc',
66
+ );
67
+ expectScope(t, tokens, 'mod', 'keyword.control.module.mira', 0);
68
+ expectScope(t, tokens, 'matrix', 'entity.name.namespace.mira', 0);
69
+ expectScope(t, tokens, 'pub', 'keyword.control.module.mira');
70
+ expectScope(t, tokens, 'PI', 'variable.other.constant.mira', 0);
71
+ expectScope(t, tokens, 'PI', 'variable.other.constant.mira', 1);
72
+ expectScope(t, tokens, '(global)', 'entity.name.label.mira', 0);
73
+ expectScope(t, tokens, 'fn', 'keyword.declaration.function.mira', 0);
74
+ expectScope(t, tokens, 'fn', 'support.type.function.mira', 2);
75
+ expectScope(t, tokens, 'data', 'variable.emphasis.mira', 1);
76
+ expectScope(t, tokens, 'f', 'entity.name.function.emphasis.mira');
77
+ expectScope(t, tokens, 'value', 'variable.emphasis.mira');
78
+ expectScope(t, tokens, 'type', 'support.type.type.mira');
79
+ expectScope(t, tokens, 'data', 'variable.other.mira', 2);
80
+ });
81
+
82
+ test('keeps nested types in unlabelled and module function signatures', (t) => {
83
+ const tokens = tokenize(
84
+ [
85
+ 'fn map(',
86
+ ' data: array | record,',
87
+ ' f: fn(value: any, key: number | string, input: type(data)) -> any,',
88
+ ') -> type(data)',
89
+ 'mod matrix {',
90
+ ' pub fn entrywise(',
91
+ ' a: any | any[] | any[][],',
92
+ ' b: any | any[] | any[][],',
93
+ ' f: fn(a: any, b: any) -> any,',
94
+ ' ) -> any | any[] | any[][];',
95
+ '}',
96
+ ].join('\n'),
97
+ 'mirascript-doc',
98
+ );
99
+ const declarationFns = tokens.filter((token) => token.text === 'fn' && (token.line === 0 || token.line === 5));
100
+ const typeFns = tokens.filter((token) => token.text === 'fn' && (token.line === 2 || token.line === 8));
101
+ t.is(declarationFns.length, 2);
102
+ t.is(typeFns.length, 2);
103
+ for (const token of declarationFns) {
104
+ t.true(token.scopes.includes('keyword.declaration.function.mira'), token.scopes.join(', '));
105
+ t.false(token.scopes.includes('support.type.function.mira'), token.scopes.join(', '));
106
+ }
107
+ for (const token of typeFns) {
108
+ t.true(token.scopes.includes('support.type.function.mira'), token.scopes.join(', '));
109
+ t.false(token.scopes.includes('keyword.declaration.function.mira'), token.scopes.join(', '));
110
+ }
111
+ for (const token of tokens.filter((token) => token.text === 'type')) {
112
+ t.true(token.scopes.includes('support.type.type.mira'), token.scopes.join(', '));
113
+ }
114
+ t.is(tokens.filter((token) => token.text === 'type').length, 2);
115
+ for (const token of tokens.filter((token) => token.text === 'data' && (token.line === 2 || token.line === 3))) {
116
+ t.true(token.scopes.includes('variable.other.mira'), token.scopes.join(', '));
117
+ }
118
+ t.is(tokens.filter((token) => token.text === 'data' && (token.line === 2 || token.line === 3)).length, 2);
119
+ });
120
+
121
+ test('keeps tuple and array element types inside their type context', (t) => {
122
+ const tokens = tokenize(
123
+ [
124
+ 'fn size(matrix: any[][]) -> [number, number]',
125
+ 'fn identity(..size: [number] | [number, number]) -> number[][]',
126
+ ].join('\n'),
127
+ 'mirascript-doc',
128
+ );
129
+ expectScope(t, tokens, 'matrix', 'variable.emphasis.mira');
130
+ expectScope(t, tokens, 'size', 'variable.emphasis.mira', 1);
131
+ for (const token of tokens.filter((candidate) => candidate.text === 'number')) {
132
+ t.true(
133
+ token.scopes.includes('support.type.builtin.mira'),
134
+ `${JSON.stringify(token.text)} should stay a built-in type; got ${token.scopes.join(', ')}`,
135
+ );
136
+ }
137
+ t.is(tokens.filter((token) => token.text === 'number').length, 6);
138
+ });
139
+
140
+ test('highlights serialized extern record values in documentation mode', (t) => {
141
+ const tokens = tokenize(
142
+ [
143
+ '(global) globalThis = /* <extern Window> */ (',
144
+ ' event: nil,',
145
+ ' customElements: /* <extern CustomElementRegistry> */ (',
146
+ ' define: /* <extern function> */,',
147
+ ' get: /* <extern function getValue> */,',
148
+ ' iterate: /* <extern function*> */,',
149
+ ' resolve: /* <extern async function resolveValue> */,',
150
+ ' initialize: /* <extern async function* initializeValue> */,',
151
+ ' Constructor: /* <extern class> */,',
152
+ ' Widget: /* <extern class HTMLElement> */',
153
+ ' ),',
154
+ ' ../* x162 */',
155
+ ')',
156
+ ].join('\n'),
157
+ 'mirascript-doc',
158
+ );
159
+ expectScope(t, tokens, '(global)', 'entity.name.label.mira');
160
+ expectScope(t, tokens, 'globalThis', 'variable.other.mira');
161
+ expectScope(t, tokens, 'event', 'variable.other.property.mira');
162
+ expectScope(t, tokens, 'nil', 'constant.language.mira');
163
+ expectScope(t, tokens, 'customElements', 'variable.other.property.mira');
164
+ expectScope(t, tokens, 'define', 'entity.name.function.mira');
165
+ expectScope(t, tokens, 'get', 'entity.name.function.mira');
166
+ expectScope(t, tokens, 'iterate', 'entity.name.function.mira');
167
+ expectScope(t, tokens, 'resolve', 'entity.name.function.mira');
168
+ expectScope(t, tokens, 'initialize', 'entity.name.function.mira');
169
+ expectScope(t, tokens, 'Constructor', 'entity.name.type.mira');
170
+ expectScope(t, tokens, 'Widget', 'entity.name.type.mira');
171
+ expectScope(t, tokens, 'Window', 'entity.name.type.js');
172
+ expectScope(t, tokens, 'CustomElementRegistry', 'entity.name.type.js');
173
+ for (const token of tokens.filter((candidate) => candidate.text === 'function')) {
174
+ t.true(token.scopes.includes('keyword.js'));
175
+ }
176
+ t.is(tokens.filter((token) => token.text === 'function').length, 5);
177
+ for (const token of tokens.filter((candidate) => candidate.text === 'async')) {
178
+ t.true(token.scopes.includes('keyword.js'));
179
+ }
180
+ t.is(tokens.filter((token) => token.text === 'async').length, 2);
181
+ for (const token of tokens.filter((candidate) => candidate.text === '*')) {
182
+ t.true(token.scopes.includes('keyword.operator.generator.js'));
183
+ }
184
+ t.is(tokens.filter((token) => token.text === '*').length, 2);
185
+ expectScope(t, tokens, 'getValue', 'entity.name.function.js');
186
+ expectScope(t, tokens, 'resolveValue', 'entity.name.function.js');
187
+ expectScope(t, tokens, 'initializeValue', 'entity.name.function.js');
188
+ expectScope(t, tokens, 'class', 'keyword.js', 0);
189
+ expectScope(t, tokens, 'class', 'keyword.js', 1);
190
+ expectScope(t, tokens, 'HTMLElement', 'entity.name.type.js');
191
+ expectScope(t, tokens, ' x162 ', 'comment.block.mira');
192
+ for (const delimiter of tokens.filter((token) => ['/*', '*/'].includes(token.text))) {
193
+ t.true(delimiter.scopes.includes('comment.block.mira'), delimiter.scopes.join(', '));
194
+ }
195
+ for (const delimiter of tokens.filter((token) => ['<', '>'].includes(token.text))) {
196
+ t.true(
197
+ delimiter.scopes.some((scope) => scope.startsWith('punctuation.definition.tag.')),
198
+ delimiter.scopes.join(', '),
199
+ );
200
+ }
201
+ });
202
+
203
+ test('highlights inline and comment-only documentation tags', (t) => {
204
+ const tokens = tokenize(
205
+ [
206
+ '<module matrix / arbitrary name>',
207
+ '<function global.to-string (value)>',
208
+ ' <extern async function* request animation frame> ',
209
+ '/* <module matrix> */',
210
+ '/*<function render>*/',
211
+ ].join('\n'),
212
+ 'mirascript-doc',
213
+ );
214
+ expectScope(t, tokens, 'module', 'keyword.declaration.module.mira', 0);
215
+ expectScope(t, tokens, 'module', 'keyword.declaration.module.mira', 1);
216
+ expectScope(t, tokens, 'matrix / arbitrary name', 'entity.name.namespace.mira');
217
+ expectScope(t, tokens, 'matrix', 'entity.name.namespace.mira');
218
+ expectScope(t, tokens, 'function', 'keyword.declaration.function.mira', 0);
219
+ expectScope(t, tokens, 'function', 'keyword.declaration.function.mira', 2);
220
+ expectScope(t, tokens, 'global.to-string (value)', 'entity.name.function.mira');
221
+ expectScope(t, tokens, 'render', 'entity.name.function.mira');
222
+ expectScope(t, tokens, 'extern', 'keyword.declaration.extern.mira');
223
+ expectScope(t, tokens, 'async', 'keyword.js');
224
+ expectScope(t, tokens, 'function', 'keyword.js', 1);
225
+ expectScope(t, tokens, '*', 'keyword.operator.generator.js');
226
+ expectScope(t, tokens, 'request animation frame', 'entity.name.function.js');
227
+ t.is(tokens.filter((token) => token.text === '<').length, 5);
228
+ t.is(tokens.filter((token) => token.text === '>').length, 5);
229
+ for (const delimiter of tokens.filter((token) => token.text === '<')) {
230
+ t.true(delimiter.scopes.includes('punctuation.definition.tag.begin.mira'), delimiter.scopes.join(', '));
231
+ }
232
+ for (const delimiter of tokens.filter((token) => token.text === '>')) {
233
+ t.true(delimiter.scopes.includes('punctuation.definition.tag.end.mira'), delimiter.scopes.join(', '));
234
+ }
235
+ });
236
+
237
+ test('highlights multiple inline tags while keeping tag delimiters tight', (t) => {
238
+ const tokens = tokenize(
239
+ ['value = <module matrix>', '<extern Array(3)> [1, <extern Object>, [1, <extern Object>]]'].join('\n'),
240
+ 'mirascript-doc',
241
+ );
242
+ expectScope(t, tokens, 'module', 'keyword.declaration.module.mira');
243
+ expectScope(t, tokens, 'matrix', 'entity.name.namespace.mira');
244
+ expectScope(t, tokens, 'Array', 'entity.name.type.js');
245
+ expectScope(t, tokens, '(', 'punctuation.section.parens.begin.mira');
246
+ expectScope(t, tokens, '3', 'constant.numeric.mira');
247
+ expectScope(t, tokens, ')', 'punctuation.section.parens.end.mira');
248
+ expectScope(t, tokens, 'Object', 'entity.name.type.js', 0);
249
+ expectScope(t, tokens, 'Object', 'entity.name.type.js', 1);
250
+ t.is(tokens.filter((token) => token.text === 'extern').length, 3);
251
+ t.true(tokens.some((token) => token.scopes.includes('meta.documentation.tag.mira')));
252
+
253
+ const invalidTokens = tokenize(
254
+ [
255
+ '/* prefix <module matrix> */',
256
+ '< module matrix>',
257
+ '<module matrix >',
258
+ '/* <module matrix> suffix */',
259
+ '/*',
260
+ '<module matrix>',
261
+ '*/',
262
+ ].join('\n'),
263
+ 'mirascript-doc',
264
+ );
265
+ t.false(
266
+ invalidTokens.some((token) => token.scopes.includes('meta.documentation.tag.mira')),
267
+ invalidTokens
268
+ .map((token) => `${token.line}:${JSON.stringify(token.text)} ${token.scopes.join(', ')}`)
269
+ .join('\n'),
270
+ );
271
+ t.false(
272
+ invalidTokens.some((token) => token.scopes.some((scope) => scope.startsWith('punctuation.definition.tag.'))),
273
+ );
274
+
275
+ const sourceTokens = tokenize('<module matrix>\n/* <extern Navigator> */');
276
+ t.false(sourceTokens.some((token) => token.scopes.includes('meta.documentation.tag.mira')));
277
+ t.false(
278
+ sourceTokens.some((token) => token.scopes.some((scope) => scope.startsWith('punctuation.definition.tag.'))),
279
+ );
280
+ });
281
+
282
+ test('highlights extern tags while preserving surrounding document declarations', (t) => {
283
+ const tokens = tokenize(
284
+ [
285
+ 'let navigator = /* <extern Navigator> */ (',
286
+ ' scheduling: /* <extern Scheduling> */,',
287
+ ' getGamepads: /* <extern function> */,',
288
+ ');',
289
+ 'let AbortController = /* <extern class AbortController> */;',
290
+ ].join('\n'),
291
+ 'mirascript-doc',
292
+ );
293
+ expectScope(t, tokens, 'navigator', 'variable.other.constant.mira');
294
+ expectScope(t, tokens, 'scheduling', 'variable.other.property.mira');
295
+ expectScope(t, tokens, 'getGamepads', 'entity.name.function.mira');
296
+ expectScope(t, tokens, 'AbortController', 'variable.other.constant.mira', 0);
297
+ expectScope(t, tokens, 'extern', 'keyword.declaration.extern.mira');
298
+ expectScope(t, tokens, 'Navigator', 'entity.name.type.js');
299
+ expectScope(t, tokens, 'Scheduling', 'entity.name.type.js');
300
+ expectScope(t, tokens, 'function', 'keyword.js');
301
+ expectScope(t, tokens, 'class', 'keyword.js');
302
+ expectScope(t, tokens, 'AbortController', 'entity.name.type.js', 1);
303
+ for (const delimiter of tokens.filter((token) => ['/*', '*/'].includes(token.text))) {
304
+ t.true(delimiter.scopes.includes('comment.block.mira'), delimiter.scopes.join(', '));
305
+ }
306
+ });
307
+
308
+ test('highlights extern tags nested in document value arrays', (t) => {
309
+ const tokens = tokenize(
310
+ ['a: /* <extern Array(3)> */ [', ' 1,', ' /* <extern Object> */,', ' /* <extern Array(2)> */', ']'].join(
311
+ '\n',
312
+ ),
313
+ 'mirascript-doc',
314
+ );
315
+ expectScope(t, tokens, 'a', 'variable.other.property.mira');
316
+ expectScope(t, tokens, 'Array', 'entity.name.type.js', 0);
317
+ expectScope(t, tokens, 'Array', 'entity.name.type.js', 1);
318
+ expectScope(t, tokens, 'Object', 'entity.name.type.js');
319
+ expectScope(t, tokens, '3', 'constant.numeric.mira');
320
+ expectScope(t, tokens, '2', 'constant.numeric.mira');
321
+ for (const delimiter of tokens.filter((token) => ['(', ')'].includes(token.text))) {
322
+ t.true(
323
+ delimiter.scopes.includes(`punctuation.section.parens.${delimiter.text === '(' ? 'begin' : 'end'}.mira`),
324
+ );
325
+ }
326
+ t.is(tokens.filter((token) => token.text === 'extern').length, 3);
327
+ for (const delimiter of tokens.filter((token) => ['/*', '*/'].includes(token.text))) {
328
+ t.true(delimiter.scopes.includes('comment.block.mira'), delimiter.scopes.join(', '));
329
+ }
330
+ });
331
+
332
+ test('highlights line-leading field declarations and infers callable and class field names', (t) => {
333
+ const tokens = tokenize(
334
+ [
335
+ '(field) navigator: /* <extern Navigator> */',
336
+ 'scheduling: /* <extern Scheduling> */',
337
+ 'getGamepads: /* <extern function> */',
338
+ 'AbortController: /* <extern class AbortController> */;',
339
+ '(field) requestAnimationFrame: /* <extern function> */;',
340
+ '(field) 1: value',
341
+ '(field) "invalid-name": value',
342
+ '2: /* <extern function> */;',
343
+ '"class-name": /* <extern class HTMLElement> */;',
344
+ ].join('\n'),
345
+ 'mirascript-doc',
346
+ );
347
+ expectScope(t, tokens, '(field)', 'entity.name.label.mira');
348
+ expectScope(t, tokens, 'navigator', 'variable.other.property.mira');
349
+ expectScope(t, tokens, 'scheduling', 'variable.other.property.mira');
350
+ expectScope(t, tokens, 'getGamepads', 'entity.name.function.mira');
351
+ expectScope(t, tokens, 'AbortController', 'entity.name.type.mira', 0);
352
+ expectScope(t, tokens, 'AbortController', 'entity.name.type.js', 1);
353
+ expectScope(t, tokens, 'requestAnimationFrame', 'entity.name.function.mira');
354
+ expectScope(t, tokens, '1', 'variable.other.property.mira');
355
+ expectScope(t, tokens, '"invalid-name"', 'variable.other.property.mira');
356
+ expectScope(t, tokens, '2', 'entity.name.function.mira');
357
+ expectScope(t, tokens, '"class-name"', 'entity.name.type.mira');
358
+ t.true(tokens.some((token) => token.scopes.includes('meta.documentation.field.mira')));
359
+
360
+ const indented = tokenize(
361
+ [
362
+ ' (field) indented: /* <extern function> */',
363
+ ' plain: /* <extern class HTMLElement> */',
364
+ ' (global) value = /* <extern Window> */',
365
+ '(field) legacy = /* <extern function> */',
366
+ 'legacy = /* <extern class HTMLElement> */',
367
+ ].join('\n'),
368
+ 'mirascript-doc',
369
+ );
370
+ t.false(indented.some((token) => token.scopes.includes('meta.documentation.field.mira')));
371
+ t.false(indented.some((token) => token.scopes.includes('meta.documentation.global-value.mira')));
372
+ });