@astryxdesign/core 0.6.3-canary.8f908ec → 0.6.3-canary.98a2e4e
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"highlightRanges.d.ts","sourceRoot":"","sources":["../../src/CodeBlock/highlightRanges.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAc,SAAS,EAAC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"highlightRanges.d.ts","sourceRoot":"","sources":["../../src/CodeBlock/highlightRanges.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAc,SAAS,EAAC,MAAM,aAAa,CAAC;AA8FxD,UAAU,UAAU;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;CACtB;AAgDD,iBAAS,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAIjD;AA4BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,SAAS,EAAE,GACtB,MAAM,IAAI,CAsHZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAChB,UAAU,EAAE,CAqBd;AAMD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,WAAW,EACf,UAAU,EAAE,SAAS,EAAE,GACtB,MAAM,IAAI,CA2DZ;AAeD;;GAEG;AACH,OAAO,EAAC,aAAa,EAAC,CAAC"}
|
|
@@ -52,20 +52,9 @@ function ensureDynamicHighlightType(tokenType) {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// ident VALUE, escapes and all), so dotted, non-ASCII, `_private`, and
|
|
59
|
-
// digit-led token types keep their colours — while nothing a token stream
|
|
60
|
-
// carries can step outside its ident and shape the rule.
|
|
61
|
-
const name = CSS.escape(`astryx-${tokenType}`);
|
|
62
|
-
const colorVar = `var(${CSS.escape(`--color-syntax-${tokenType}`)}, currentColor)`;
|
|
63
|
-
try {
|
|
64
|
-
dynamicStyleSheet.insertRule(`.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`);
|
|
65
|
-
} catch {
|
|
66
|
-
// An engine that still refuses the rule costs that type its colour
|
|
67
|
-
// (ranges paint with currentColor) — never the whole code block.
|
|
68
|
-
}
|
|
55
|
+
const name = `astryx-${tokenType}`;
|
|
56
|
+
const colorVar = `var(--color-syntax-${tokenType}, currentColor)`;
|
|
57
|
+
dynamicStyleSheet.insertRule(`.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`);
|
|
69
58
|
}
|
|
70
59
|
|
|
71
60
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.6.3-canary.
|
|
3
|
+
"version": "0.6.3-canary.98a2e4e",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -692,8 +692,8 @@
|
|
|
692
692
|
"react-dom": ">=19.0.0"
|
|
693
693
|
},
|
|
694
694
|
"devDependencies": {
|
|
695
|
-
"@astryxdesign/a11y-spec": "0.6.3-canary.
|
|
696
|
-
"@astryxdesign/cli": "0.6.3-canary.
|
|
695
|
+
"@astryxdesign/a11y-spec": "0.6.3-canary.98a2e4e",
|
|
696
|
+
"@astryxdesign/cli": "0.6.3-canary.98a2e4e",
|
|
697
697
|
"@babel/cli": "^7.29.7",
|
|
698
698
|
"@babel/core": "^7.29.7",
|
|
699
699
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -4,18 +4,14 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
|
4
4
|
import {applyHighlightRangesChunked} from './highlightRanges';
|
|
5
5
|
import type {TokenLine} from './tokenizer';
|
|
6
6
|
|
|
7
|
-
// Mock CSS Highlight API
|
|
8
|
-
// under test escapes generated names with it, so jsdom's implementation is
|
|
9
|
-
// carried over when the CSS global is replaced.
|
|
7
|
+
// Mock CSS Highlight API
|
|
10
8
|
class MockHighlight extends Set<Range> {}
|
|
11
9
|
const mockHighlightsMap = new Map<string, MockHighlight>();
|
|
12
|
-
const realCssEscape = globalThis.CSS.escape;
|
|
13
10
|
|
|
14
11
|
beforeEach(() => {
|
|
15
12
|
mockHighlightsMap.clear();
|
|
16
13
|
|
|
17
14
|
globalThis.CSS = {
|
|
18
|
-
escape: realCssEscape,
|
|
19
15
|
highlights: {
|
|
20
16
|
get: (name: string) => mockHighlightsMap.get(name),
|
|
21
17
|
set: (name: string, h: MockHighlight) => mockHighlightsMap.set(name, h),
|
|
@@ -48,30 +44,6 @@ function createCodeElement(lines: string[]): HTMLElement {
|
|
|
48
44
|
return code;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
/**
|
|
52
|
-
* The dynamic stylesheet is module-level state shared by every test in this
|
|
53
|
-
* file, so each test drives unique token types and looks its rules up by the
|
|
54
|
-
* escaped name rather than by position.
|
|
55
|
-
*/
|
|
56
|
-
function dynamicRules(): CSSStyleRule[] {
|
|
57
|
-
const dynamicSheet = document.querySelector<HTMLStyleElement>(
|
|
58
|
-
'style[data-astryx-highlight-dynamic]',
|
|
59
|
-
);
|
|
60
|
-
return Array.from(dynamicSheet?.sheet?.cssRules ?? []) as CSSStyleRule[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Exactly the two highlight selectors and nothing else: an escaped ident may
|
|
65
|
-
* contain any character, but only behind a backslash, so an unescaped `)`
|
|
66
|
-
* that closed the function early (and the `, body` after it) cannot match.
|
|
67
|
-
*/
|
|
68
|
-
const HIGHLIGHT_ONLY_SELECTOR =
|
|
69
|
-
/^\.astryx-code-block code::highlight\((?:[^()\\]|\\.)+\), \.astryx-codeeditor code::highlight\((?:[^()\\]|\\.)+\)$/;
|
|
70
|
-
|
|
71
|
-
function highlightRule(name: string): string {
|
|
72
|
-
return `.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name})`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
47
|
describe('applyHighlightRangesChunked', () => {
|
|
76
48
|
it('creates ranges for tokens on each line', () => {
|
|
77
49
|
const codeEl = createCodeElement(['const x = 1;', 'let y = 2;']);
|
|
@@ -95,120 +67,6 @@ describe('applyHighlightRangesChunked', () => {
|
|
|
95
67
|
expect(kwHighlight!.size).toBe(0);
|
|
96
68
|
});
|
|
97
69
|
|
|
98
|
-
it('keeps every parser-accepted tokenizer type coloured under its raw name', () => {
|
|
99
|
-
// The dotted type that crashed the block, plus names a narrow allowlist
|
|
100
|
-
// would wrongly reject even though the CSS parser accepts them all.
|
|
101
|
-
const types = ['keyword.control.sql', '_private', 'キーワード', '9start'];
|
|
102
|
-
const codeEl = createCodeElement(types.map(() => 'line'));
|
|
103
|
-
const tokenLines: TokenLine[] = types.map(type => [
|
|
104
|
-
{type, start: 0, end: 4},
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
|
-
const mockStyle = document.createElement('style');
|
|
108
|
-
mockStyle.setAttribute('data-astryx-highlight-styles', '');
|
|
109
|
-
document.head.appendChild(mockStyle);
|
|
110
|
-
const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
|
|
111
|
-
|
|
112
|
-
const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
|
|
113
|
-
|
|
114
|
-
// Ranges register under the RAW names: a ::highlight() selector matches
|
|
115
|
-
// by ident value, so the escaped rule below still paints them.
|
|
116
|
-
for (const type of types) {
|
|
117
|
-
expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// One rule per type, with only the characters that need it escaped.
|
|
121
|
-
expect(insertRule.mock.calls.map(([rule]) => rule)).toEqual([
|
|
122
|
-
`${highlightRule('astryx-keyword\\.control\\.sql')} { color: var(--color-syntax-keyword\\.control\\.sql, currentColor); }`,
|
|
123
|
-
`${highlightRule('astryx-_private')} { color: var(--color-syntax-_private, currentColor); }`,
|
|
124
|
-
`${highlightRule('astryx-キーワード')} { color: var(--color-syntax-キーワード, currentColor); }`,
|
|
125
|
-
`${highlightRule('astryx-9start')} { color: var(--color-syntax-9start, currentColor); }`,
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
cleanup();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('escapes the highlight name so a token type cannot leave its ::highlight() selector', () => {
|
|
132
|
-
// Unescaped, this type closes ::highlight() early and the CSS parser
|
|
133
|
-
// accepts what follows as a valid second selector plus a declaration
|
|
134
|
-
// block, with the trailing comment opener swallowing the rest of the
|
|
135
|
-
// generated rule text. The result is a rule that styles <body>.
|
|
136
|
-
const type = 'sel), body { background: red } /*';
|
|
137
|
-
const escapedName =
|
|
138
|
-
'astryx-sel\\)\\,\\ body\\ \\{\\ background\\:\\ red\\ \\}\\ \\/\\*';
|
|
139
|
-
const codeEl = createCodeElement(['line']);
|
|
140
|
-
const tokenLines: TokenLine[] = [[{type, start: 0, end: 4}]];
|
|
141
|
-
|
|
142
|
-
const mockStyle = document.createElement('style');
|
|
143
|
-
mockStyle.setAttribute('data-astryx-highlight-styles', '');
|
|
144
|
-
document.head.appendChild(mockStyle);
|
|
145
|
-
const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
|
|
146
|
-
|
|
147
|
-
const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
|
|
148
|
-
|
|
149
|
-
expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
|
|
150
|
-
|
|
151
|
-
// The exact rule text handed to the engine: both generated names fully
|
|
152
|
-
// escaped, one colour declaration, nothing of the payload's making.
|
|
153
|
-
expect(insertRule).toHaveBeenCalledTimes(1);
|
|
154
|
-
expect(insertRule).toHaveBeenCalledWith(
|
|
155
|
-
`${highlightRule(escapedName)} { color: var(--color-syntax-sel\\)\\,\\ body\\ \\{\\ background\\:\\ red\\ \\}\\ \\/\\*, currentColor); }`,
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// And as parsed: the rule for this type keeps the payload inside the
|
|
159
|
-
// ident, and no rule in the sheet has any selector beyond the two
|
|
160
|
-
// highlight selectors.
|
|
161
|
-
const rules = dynamicRules();
|
|
162
|
-
const rule = rules.find(r => r.selectorText.includes('astryx-sel'));
|
|
163
|
-
expect(rule?.selectorText).toBe(highlightRule(escapedName));
|
|
164
|
-
for (const r of rules) {
|
|
165
|
-
expect(r.selectorText).toMatch(HIGHLIGHT_ONLY_SELECTOR);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
cleanup();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('escapes the custom property so a token type cannot add declarations', () => {
|
|
172
|
-
// Unescaped, this type closes var() early and the CSS parser accepts
|
|
173
|
-
// what follows as two more valid declarations.
|
|
174
|
-
const type = 'p); background: red; --x: var(--y';
|
|
175
|
-
const escapedName =
|
|
176
|
-
'astryx-p\\)\\;\\ background\\:\\ red\\;\\ --x\\:\\ var\\(--y';
|
|
177
|
-
const escapedColor =
|
|
178
|
-
'var(--color-syntax-p\\)\\;\\ background\\:\\ red\\;\\ --x\\:\\ var\\(--y, currentColor)';
|
|
179
|
-
const codeEl = createCodeElement(['line']);
|
|
180
|
-
const tokenLines: TokenLine[] = [[{type, start: 0, end: 4}]];
|
|
181
|
-
|
|
182
|
-
const mockStyle = document.createElement('style');
|
|
183
|
-
mockStyle.setAttribute('data-astryx-highlight-styles', '');
|
|
184
|
-
document.head.appendChild(mockStyle);
|
|
185
|
-
const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
|
|
186
|
-
|
|
187
|
-
const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
|
|
188
|
-
|
|
189
|
-
expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
|
|
190
|
-
|
|
191
|
-
expect(insertRule).toHaveBeenCalledTimes(1);
|
|
192
|
-
expect(insertRule).toHaveBeenCalledWith(
|
|
193
|
-
`${highlightRule(escapedName)} { color: ${escapedColor}; }`,
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
// As parsed: exactly one declaration, `color`, whose value is the
|
|
197
|
-
// escaped custom property with its fallback.
|
|
198
|
-
const rule = dynamicRules().find(r => r.selectorText.includes('astryx-p'));
|
|
199
|
-
expect(rule).toBeDefined();
|
|
200
|
-
expect(rule!.selectorText).toBe(highlightRule(escapedName));
|
|
201
|
-
const declared = Array.from({length: rule!.style.length}, (_, i) =>
|
|
202
|
-
rule!.style.item(i),
|
|
203
|
-
);
|
|
204
|
-
expect(declared).toEqual(['color']);
|
|
205
|
-
expect(rule!.style.getPropertyValue('color')).toBe(escapedColor);
|
|
206
|
-
expect(rule!.style.getPropertyValue('background')).toBe('');
|
|
207
|
-
expect(rule!.style.getPropertyValue('--x')).toBe('');
|
|
208
|
-
|
|
209
|
-
cleanup();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
70
|
it('handles empty token lines', () => {
|
|
213
71
|
const codeEl = createCodeElement(['', 'const x = 1;']);
|
|
214
72
|
const tokenLines: TokenLine[] = [[], [{type: 'keyword', start: 0, end: 5}]];
|
|
@@ -64,21 +64,11 @@ function ensureDynamicHighlightType(tokenType: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const name = CSS.escape(`astryx-${tokenType}`);
|
|
73
|
-
const colorVar = `var(${CSS.escape(`--color-syntax-${tokenType}`)}, currentColor)`;
|
|
74
|
-
try {
|
|
75
|
-
dynamicStyleSheet.insertRule(
|
|
76
|
-
`.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`,
|
|
77
|
-
);
|
|
78
|
-
} catch {
|
|
79
|
-
// An engine that still refuses the rule costs that type its colour
|
|
80
|
-
// (ranges paint with currentColor) — never the whole code block.
|
|
81
|
-
}
|
|
67
|
+
const name = `astryx-${tokenType}`;
|
|
68
|
+
const colorVar = `var(--color-syntax-${tokenType}, currentColor)`;
|
|
69
|
+
dynamicStyleSheet.insertRule(
|
|
70
|
+
`.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`,
|
|
71
|
+
);
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
// ---------------------------------------------------------------------------
|