@mirascript/textmate 0.1.83 → 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/.c8rc.json +6 -0
- package/ava.config.js +4 -0
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +206 -86
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mirascript-doc.tmLanguage.json +303 -84
- package/dist/mirascript-template.tmLanguage.json +1 -1
- package/dist/mirascript.tmLanguage.json +1 -1
- package/package.json +7 -7
- package/src/grammar.ts +210 -86
- package/src/index.ts +1 -1
- package/tests/_engine.ts +53 -0
- package/tests/grammar/documentation-comments.ts +157 -0
- package/tests/grammar/documentation-mode.ts +372 -0
- package/tests/grammar/language.ts +59 -0
- package/tests/grammar/strings.ts +35 -0
- package/tests/js-compatibility.ts +22 -0
- package/tests/language.ts +9 -0
- package/tests/tsconfig.json +3 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { tokenize, expectScope } from '../_engine.ts';
|
|
3
|
+
|
|
4
|
+
test('classifies declarations, parameters, properties, calls, and Unicode identifiers', (t) => {
|
|
5
|
+
const tokens = tokenize('mod 数学 { fn 加法(mut 左, ..其余) { 对象.方法(左).属性 } }');
|
|
6
|
+
expectScope(t, tokens, 'mod', 'keyword.control.module.mira');
|
|
7
|
+
expectScope(t, tokens, '数学', 'entity.name.namespace.mira');
|
|
8
|
+
expectScope(t, tokens, '加法', 'entity.name.function.mira');
|
|
9
|
+
expectScope(t, tokens, '左', 'variable.emphasis.mira');
|
|
10
|
+
expectScope(t, tokens, '其余', 'variable.other.constant.emphasis.mira');
|
|
11
|
+
expectScope(t, tokens, '方法', 'entity.name.function.member.mira');
|
|
12
|
+
expectScope(t, tokens, '属性', 'variable.other.property.mira');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('classifies keyword and numeric families', (t) => {
|
|
16
|
+
const tokens = tokenize('if true and value not in global { let @常量 = 0xCA_FE; 1.5e+2 }');
|
|
17
|
+
expectScope(t, tokens, 'if', 'keyword.control.mira');
|
|
18
|
+
expectScope(t, tokens, 'true', 'constant.language.mira');
|
|
19
|
+
expectScope(t, tokens, 'and', 'keyword.operator.wordlike.mira');
|
|
20
|
+
expectScope(t, tokens, 'not', 'keyword.operator.wordlike.mira');
|
|
21
|
+
expectScope(t, tokens, 'in', 'keyword.operator.wordlike.mira');
|
|
22
|
+
expectScope(t, tokens, 'global', 'variable.language.mira');
|
|
23
|
+
expectScope(t, tokens, '@常量', 'variable.other.constant.mira');
|
|
24
|
+
expectScope(t, tokens, '0xCA_FE', 'constant.numeric.hex.mira');
|
|
25
|
+
expectScope(t, tokens, '1.5e+2', 'constant.numeric.float.mira');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('does not classify control keywords followed by parentheses as functions', (t) => {
|
|
29
|
+
const tokens = tokenize('if (condition) { case (value) { call(value) } }');
|
|
30
|
+
expectScope(t, tokens, 'if', 'keyword.control.mira');
|
|
31
|
+
expectScope(t, tokens, 'case', 'keyword.control.mira');
|
|
32
|
+
expectScope(t, tokens, 'call', 'entity.name.function.mira');
|
|
33
|
+
for (const keyword of ['if', 'case']) {
|
|
34
|
+
const token = tokens.find((candidate) => candidate.text === keyword);
|
|
35
|
+
t.false(token!.scopes.includes('entity.name.function.mira'));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('only classifies type as a keyword in its two contextual forms', (t) => {
|
|
40
|
+
const tokens = tokenize('type(value); type Value; let type = 1; type + 1; object.type(); value::type();');
|
|
41
|
+
expectScope(t, tokens, 'type', 'keyword.operator.expression.mira', 0);
|
|
42
|
+
expectScope(t, tokens, 'type', 'keyword.operator.expression.mira', 1);
|
|
43
|
+
expectScope(t, tokens, 'type', 'variable.other.mira', 2);
|
|
44
|
+
expectScope(t, tokens, 'type', 'variable.other.mira', 3);
|
|
45
|
+
expectScope(t, tokens, 'type', 'entity.name.function.member.mira', 4);
|
|
46
|
+
expectScope(t, tokens, 'type', 'keyword.operator.expression.mira', 5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('marks invalid numeric and escape sequences', (t) => {
|
|
50
|
+
const tokens = tokenize(String.raw`let a = 0xGG; let b = "\q";`);
|
|
51
|
+
expectScope(t, tokens, '0xGG', 'invalid.illegal.numeric.mira');
|
|
52
|
+
expectScope(t, tokens, String.raw`\q`, 'invalid.illegal.escape.mira');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('keeps multiline rule state', (t) => {
|
|
56
|
+
const tokens = tokenize('/* first\nsecond */\n"first ${\nvalue\n}"');
|
|
57
|
+
expectScope(t, tokens, 'second ', 'comment.block.mira');
|
|
58
|
+
expectScope(t, tokens, 'value', 'variable.other.mira');
|
|
59
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { tokenize, expectScope } from '../_engine.ts';
|
|
3
|
+
|
|
4
|
+
test('handles nested interpolation and format strings', (t) => {
|
|
5
|
+
const tokens = tokenize('"value ${ if ok { fn_call((1 + 2)) } } / $(value:>8[.]2f)"');
|
|
6
|
+
expectScope(t, tokens, '${', 'punctuation.definition.template-expression.begin.mira');
|
|
7
|
+
expectScope(t, tokens, 'fn_call', 'entity.name.function.mira');
|
|
8
|
+
expectScope(t, tokens, '$(', 'punctuation.definition.template-expression.begin.mira');
|
|
9
|
+
expectScope(t, tokens, ':', 'punctuation.separator.format.mira');
|
|
10
|
+
expectScope(t, tokens, '>8', 'string.unquoted.format.mira');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('matches the exact interpolation width in verbatim strings', (t) => {
|
|
14
|
+
for (const width of [1, 2, 3, 16]) {
|
|
15
|
+
const ats = '@'.repeat(width);
|
|
16
|
+
const dollars = '$'.repeat(width);
|
|
17
|
+
const shorter = '$'.repeat(Math.max(1, width - 1));
|
|
18
|
+
const tokens = tokenize(`${ats}"literal ${shorter}name ${dollars}name"${ats}`);
|
|
19
|
+
expectScope(t, tokens, dollars, 'punctuation.definition.template-expression.begin.mira');
|
|
20
|
+
expectScope(t, tokens, 'name', 'variable.other.mira', width === 1 ? 1 : 0);
|
|
21
|
+
if (width > 1) {
|
|
22
|
+
const literal = tokens.find((token) => token.text.includes(`${shorter}name`));
|
|
23
|
+
t.true(literal!.scopes.includes('string.quoted.double.verbatim.mira'));
|
|
24
|
+
t.false(literal!.scopes.includes('meta.interpolation.simple.mira'));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('highlights template text and embedded MiraScript', (t) => {
|
|
30
|
+
const tokens = tokenize('Hello $name: ${ fn_call(1) }', 'mirascript-template');
|
|
31
|
+
expectScope(t, tokens, 'Hello ', 'string.unquoted.template.mira');
|
|
32
|
+
expectScope(t, tokens, '$', 'punctuation.definition.template-expression.begin.mira');
|
|
33
|
+
expectScope(t, tokens, 'name', 'variable.other.mira');
|
|
34
|
+
expectScope(t, tokens, 'fn_call', 'entity.name.function.mira');
|
|
35
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHighlighterCore, type HighlighterCore } from 'shiki';
|
|
2
|
+
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs';
|
|
3
|
+
import test from 'ava';
|
|
4
|
+
import { grammars } from '../src/index.ts';
|
|
5
|
+
|
|
6
|
+
test('grammars can be loaded', async (t) => {
|
|
7
|
+
const highlighter = await createHighlighterCore({
|
|
8
|
+
langs: grammars,
|
|
9
|
+
themes: [],
|
|
10
|
+
engine: createJavaScriptRegexEngine(),
|
|
11
|
+
});
|
|
12
|
+
const mirascript = highlighter.getLanguage('mirascript');
|
|
13
|
+
t.truthy(mirascript);
|
|
14
|
+
|
|
15
|
+
const mirascriptTemplate = highlighter.getLanguage('mirascript-template');
|
|
16
|
+
t.truthy(mirascriptTemplate);
|
|
17
|
+
|
|
18
|
+
const mirascriptDoc = highlighter.getLanguage('mirascript-doc');
|
|
19
|
+
t.truthy(mirascriptDoc);
|
|
20
|
+
|
|
21
|
+
highlighter.dispose();
|
|
22
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { grammars } from '../src/index.ts';
|
|
3
|
+
import { mirascriptLanguage } from '../src/language.ts';
|
|
4
|
+
|
|
5
|
+
test('uses the shared MiraScript language metadata', (t) => {
|
|
6
|
+
t.is(grammars[0].name, mirascriptLanguage.name);
|
|
7
|
+
t.is(grammars[0].scopeName, mirascriptLanguage.scopeName);
|
|
8
|
+
t.deepEqual(grammars[0].aliases, mirascriptLanguage.aliases);
|
|
9
|
+
});
|