@mui/internal-docs-infra 0.8.1-canary.0 → 0.9.1-canary.0

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.
@@ -5,6 +5,7 @@ import { createStarryNight } from '@wooorm/starry-night';
5
5
  import { CodeContext } from "./CodeContext.mjs";
6
6
  import { extensionMap, grammars } from "../pipeline/parseSource/grammars.mjs";
7
7
  import { starryNightGutter } from "../pipeline/parseSource/addLineGutters.mjs";
8
+ import { extendSyntaxTokens } from "../pipeline/parseSource/extendSyntaxTokens.mjs";
8
9
  // Import the heavy functions
9
10
  import { loadCodeFallback } from "../pipeline/loadCodeVariant/loadCodeFallback.mjs";
10
11
  import { loadCodeVariant } from "../pipeline/loadCodeVariant/loadCodeVariant.mjs";
@@ -49,7 +50,9 @@ export function CodeProvider({
49
50
  }]
50
51
  };
51
52
  }
52
- const highlighted = starryNight.highlight(source, extensionMap[fileType]);
53
+ const grammarScope = extensionMap[fileType];
54
+ const highlighted = starryNight.highlight(source, grammarScope);
55
+ extendSyntaxTokens(highlighted, grammarScope);
53
56
  const sourceLines = source.split(/\r?\n|\r/);
54
57
  starryNightGutter(highlighted, sourceLines); // mutates the tree to add line gutters
55
58
 
@@ -38,10 +38,10 @@ export declare function abstractCreateDemo<T extends {}>(options: AbstractCreate
38
38
  }, meta: CreateDemoMeta | undefined): React.ComponentType<T> & {
39
39
  Title: React.ComponentType;
40
40
  };
41
- export declare function createDemoFactory<T extends {}>(options: AbstractCreateDemoOptions<T>): (url: string, component: React.ComponentType<{}>, meta?: CreateDemoMeta | undefined) => React.ComponentType<T> & {
42
- Title: React.ComponentType<{}>;
41
+ export declare function createDemoFactory<T extends {}>(options: AbstractCreateDemoOptions<T>): (url: string, component: React.ComponentType, meta?: CreateDemoMeta) => React.ComponentType<T> & {
42
+ Title: React.ComponentType;
43
43
  };
44
- export declare function createDemoWithVariantsFactory<T extends {}>(options: AbstractCreateDemoOptions<T>): (url: string, variants: Record<string, React.ComponentType<{}>>, meta?: CreateDemoMeta | undefined) => React.ComponentType<T> & {
45
- Title: React.ComponentType<{}>;
44
+ export declare function createDemoWithVariantsFactory<T extends {}>(options: AbstractCreateDemoOptions<T>): (url: string, variants: Record<string, React.ComponentType>, meta?: CreateDemoMeta) => React.ComponentType<T> & {
45
+ Title: React.ComponentType;
46
46
  };
47
47
  export {};
@@ -26,7 +26,7 @@ type AbstractCreateDemoClientOptions = {
26
26
  export declare function abstractCreateDemoClient(options: AbstractCreateDemoClientOptions, url: string, meta?: CreateDemoClientMeta): React.ComponentType<{
27
27
  children: React.ReactNode;
28
28
  }>;
29
- export declare function createDemoClientFactory(options: AbstractCreateDemoClientOptions): (url: string, meta?: CreateDemoClientMeta | undefined) => React.ComponentType<{
29
+ export declare function createDemoClientFactory(options: AbstractCreateDemoClientOptions): (url: string, meta?: CreateDemoClientMeta) => React.ComponentType<{
30
30
  children: React.ReactNode;
31
31
  }>;
32
32
  export {};
package/cli/index.mjs CHANGED
@@ -7,4 +7,4 @@ function getVersion() {
7
7
  }
8
8
  yargs().scriptName('docs-infra').usage('$0 <command> [args]').command(runValidate).demandCommand(1, 'You need at least one command before moving on').strict().help()
9
9
  // MUI_VERSION is set through the code-infra build command.
10
- .version("0.8.0" || getVersion()).parse(hideBin(process.argv));
10
+ .version("0.9.0" || getVersion()).parse(hideBin(process.argv));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.8.1-canary.0",
3
+ "version": "0.9.1-canary.0",
4
4
  "author": "MUI Team",
5
5
  "description": "MUI Infra - internal documentation creation tools.",
6
6
  "license": "MIT",
@@ -633,5 +633,5 @@
633
633
  "bin": {
634
634
  "docs-infra": "./cli/index.mjs"
635
635
  },
636
- "gitSha": "a2817e4da1ff70e691443c39bc53141feea9122a"
636
+ "gitSha": "3feee347096f50b441cad3087743bd1c5aef8651"
637
637
  }
@@ -2,26 +2,24 @@ import type { Root as HastRoot } from 'hast';
2
2
  /**
3
3
  * A rehype plugin that enhances inline code elements in three ways:
4
4
  *
5
- * 1. **Tag bracket wrapping**: Wraps HTML tag angle brackets into the
6
- * syntax highlighting span, so `<div>` is styled as one unit.
5
+ * 1. **Tag bracket wrapping**: Wraps HTML/JSX tag patterns (opening bracket,
6
+ * tag-name span, closing bracket) in a wrapper span. HTML tags (`pl-ent`)
7
+ * get `<span class="di-ht">`, JSX component tags (`pl-c1`) get
8
+ * `<span class="di-jt">`. The original `pl-*` spans are preserved
9
+ * inside — no semantic information is destroyed.
7
10
  *
8
11
  * 2. **Token reclassification**: Corrects misidentified token classes,
9
12
  * e.g., `function` marked as `pl-en` is changed to `pl-k` (keyword).
10
13
  *
11
- * 3. **Nullish value styling**: Adds the `di-n` class to spans containing
12
- * `undefined`, `null`, `""`, or `''` for distinct visual treatment.
14
+ * 3. **Built-in type enhancement** (TypeScript only): Reclassifies standalone
15
+ * type keywords (`string`, `number`, `void`, etc.) from `pl-smi`/`pl-k`
16
+ * to `pl-c1 di-bt`, matching `extendSyntaxTokens` output in type context.
13
17
  *
14
18
  * Transforms patterns like:
15
19
  * `<code>&lt;<span class="pl-ent">div</span>&gt;</code>`
16
20
  *
17
21
  * Into:
18
- * `<code><span class="pl-ent">&lt;div&gt;</span></code>`
19
- *
20
- * And:
21
- * `<code><span class="pl-c1">undefined</span></code>`
22
- *
23
- * Into:
24
- * `<code><span class="di-n">undefined</span></code>`
22
+ * `<code><span class="di-ht">&lt;<span class="pl-ent">div</span>&gt;</span></code>`
25
23
  *
26
24
  * **Important**: This plugin should run after syntax highlighting plugins
27
25
  * (like transformHtmlCodeInline) as it modifies the structure
@@ -1,19 +1,19 @@
1
1
  import { visit } from 'unist-util-visit';
2
+ import { getShallowTextContent } from "../loadServerTypes/hastTypeUtils.mjs";
3
+ import { getLanguageCapabilities } from "../enhanceCodeTypes/getLanguageCapabilities.mjs";
4
+ import { BUILT_IN_TYPES } from "../parseSource/extendSyntaxTokens.mjs";
2
5
 
3
6
  /**
4
- * Classes whose spans represent tag names that should have their
5
- * surrounding angle brackets wrapped into the same span.
6
- * - pl-ent: HTML entity tag (e.g., div, span)
7
- * - pl-c1: Syntax constant (e.g., React components like Box, Stack)
8
- */
9
- const TAG_NAME_CLASSES = ['pl-ent', 'pl-c1'];
10
-
11
- /**
12
- * Values that should be styled with the nullish class (di-n).
13
- * These are special values that benefit from distinct styling
14
- * to visually distinguish them from regular code.
7
+ * Maps tag-name span classes to their wrapper class.
8
+ * - pl-ent (HTML entity tag like div, span) → di-ht (HTML tag)
9
+ * - pl-c1 (syntax constant like Box, Stack) → di-jt (JSX tag)
10
+ *
11
+ * When the element also has `di-jsx`, the wrapper is always `di-jt`.
15
12
  */
16
- const NULLISH_VALUES = ['undefined', 'null', '""', "''"];
13
+ const TAG_NAME_CLASS_MAP = {
14
+ 'pl-ent': 'di-ht',
15
+ 'pl-c1': 'di-jt'
16
+ };
17
17
 
18
18
  /**
19
19
  * Map of class → text values that should be reclassified to a different class.
@@ -27,14 +27,30 @@ const CLASS_RECLASSIFICATIONS = {
27
27
  };
28
28
 
29
29
  /**
30
- * Checks if an element has any of the tag name classes.
30
+ * Returns the wrapper class for a tag-name element, or undefined if not a tag name.
31
+ * If the element has `di-jsx`, always returns `di-jt` (JSX component tag).
31
32
  */
32
- function hasTagNameClass(element) {
33
+ function getTagWrapperClass(element) {
33
34
  const className = element.properties?.className;
34
35
  if (!Array.isArray(className)) {
35
- return false;
36
+ return undefined;
37
+ }
38
+ let baseWrapper;
39
+ let hasDiJsx = false;
40
+ for (const cls of className) {
41
+ if (typeof cls === 'string') {
42
+ if (TAG_NAME_CLASS_MAP[cls] && !baseWrapper) {
43
+ baseWrapper = TAG_NAME_CLASS_MAP[cls];
44
+ }
45
+ if (cls === 'di-jsx') {
46
+ hasDiJsx = true;
47
+ }
48
+ }
49
+ }
50
+ if (hasDiJsx) {
51
+ return 'di-jt';
36
52
  }
37
- return className.some(c => typeof c === 'string' && TAG_NAME_CLASSES.includes(c));
53
+ return baseWrapper;
38
54
  }
39
55
 
40
56
  /**
@@ -106,7 +122,17 @@ function findClosingBracket(text) {
106
122
  }
107
123
 
108
124
  /**
109
- * Wraps HTML tag angle brackets into their associated tag name spans.
125
+ * Wraps HTML/JSX tag patterns in a wrapper span that groups the opening bracket,
126
+ * tag-name span, and closing bracket into one element.
127
+ *
128
+ * - HTML tags (pl-ent) get `<span class="di-ht">` (HTML tag)
129
+ * - JSX component tags (pl-c1 with di-jsx) get `<span class="di-jt">` (JSX tag)
130
+ *
131
+ * Expects the pattern: text(`<`) + span(tagName) + text(`>`)
132
+ * where `extendSyntaxTokens` has already normalized bracket spans to text nodes.
133
+ *
134
+ * The original `pl-*` spans are preserved intact inside the wrapper — no
135
+ * semantic information is destroyed.
110
136
  *
111
137
  * This function processes nodes iteratively, but when text is split during
112
138
  * enhancement, it re-inserts the remaining text back into the processing queue
@@ -130,11 +156,57 @@ function enhanceTagBrackets(children) {
130
156
  match,
131
157
  prefix
132
158
  } = endsWithOpenBracket(textNode.value);
133
- if (match && hasTagNameClass(nextElement)) {
134
- // Check if there's a closing bracket after the span
135
- const afterSpan = queue[1];
136
- const closingBracket = afterSpan && afterSpan.type === 'text' ? findClosingBracket(afterSpan.value) : null;
137
- if (closingBracket) {
159
+ const wrapperClass = match ? getTagWrapperClass(nextElement) : undefined;
160
+ if (wrapperClass) {
161
+ // Scan forward past the tag name span to find the closing bracket text node.
162
+ // It may be immediately after (simple tags like <div>) or separated by
163
+ // attribute spans (e.g. <div className="x">).
164
+ // Stop scanning if we hit a text node containing '<' (new tag context).
165
+ let closingBracketIndex = -1;
166
+ let closingBracket = null;
167
+ for (let scanIdx = 1; scanIdx < queue.length; scanIdx += 1) {
168
+ const scanNode = queue[scanIdx];
169
+ if (scanNode.type === 'text') {
170
+ const scanText = scanNode.value;
171
+ closingBracket = findClosingBracket(scanText);
172
+ if (closingBracket) {
173
+ const matchEnd = closingBracket.position + closingBracket.suffix.length;
174
+ if (closingBracket.position === 0 || matchEnd === scanText.length) {
175
+ // > at the start or end of text is a tag-close token
176
+ closingBracketIndex = scanIdx;
177
+ break;
178
+ }
179
+ // The earliest > is in the middle of text — not a tag-close
180
+ // token. Check for a > at the end of the text instead.
181
+ closingBracket = null;
182
+ if (scanText.endsWith(' />')) {
183
+ closingBracket = {
184
+ position: scanText.length - 3,
185
+ suffix: ' />'
186
+ };
187
+ } else if (scanText.endsWith('/>')) {
188
+ closingBracket = {
189
+ position: scanText.length - 2,
190
+ suffix: '/>'
191
+ };
192
+ } else if (scanText.endsWith('>')) {
193
+ closingBracket = {
194
+ position: scanText.length - 1,
195
+ suffix: '>'
196
+ };
197
+ }
198
+ if (closingBracket) {
199
+ closingBracketIndex = scanIdx;
200
+ break;
201
+ }
202
+ }
203
+ // A '<' in text before any '>' means a new tag context — stop scanning
204
+ if (scanText.includes('<')) {
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ if (closingBracket && closingBracketIndex !== -1) {
138
210
  // Add the text before the < (if any)
139
211
  const textBeforeBracket = textNode.value.slice(0, -prefix.length);
140
212
  if (textBeforeBracket) {
@@ -144,33 +216,42 @@ function enhanceTagBrackets(children) {
144
216
  });
145
217
  }
146
218
 
147
- // Create enhanced span with brackets included
148
- // Include any attributes/content between the tag name and closing bracket
149
- const afterText = afterSpan.value;
150
- const contentBeforeClose = afterText.slice(0, closingBracket.position);
151
- const enhancedSpan = {
219
+ // Build the wrapper children: bracket text + tag name span + intermediate nodes + closing text
220
+ const closingTextNode = queue[closingBracketIndex];
221
+ const contentBeforeClose = closingTextNode.value.slice(0, closingBracket.position);
222
+ const wrapperChildren = [{
223
+ type: 'text',
224
+ value: prefix
225
+ }];
226
+
227
+ // Add the tag name span and any intermediate nodes (attributes, etc.)
228
+ for (let takeIdx = 0; takeIdx <= closingBracketIndex; takeIdx += 1) {
229
+ if (takeIdx === closingBracketIndex) {
230
+ // Last node is the text containing >; include content before + bracket
231
+ wrapperChildren.push({
232
+ type: 'text',
233
+ value: contentBeforeClose + closingBracket.suffix
234
+ });
235
+ } else {
236
+ wrapperChildren.push(queue[takeIdx]);
237
+ }
238
+ }
239
+ const wrapperSpan = {
152
240
  type: 'element',
153
241
  tagName: 'span',
154
242
  properties: {
155
- ...nextElement.properties
243
+ className: [wrapperClass]
156
244
  },
157
- children: [{
158
- type: 'text',
159
- value: prefix
160
- }, ...nextElement.children, {
161
- type: 'text',
162
- value: contentBeforeClose + closingBracket.suffix
163
- }]
245
+ children: wrapperChildren
164
246
  };
165
- newChildren.push(enhancedSpan);
247
+ newChildren.push(wrapperSpan);
166
248
 
167
- // Remove the span and the text with > from the queue
168
- queue.shift(); // Remove the span
169
- queue.shift(); // Remove the text with >
249
+ // Remove all consumed nodes from the queue
250
+ const textAfterBracket = closingTextNode.value.slice(closingBracket.position + closingBracket.suffix.length);
251
+ queue.splice(0, closingBracketIndex + 1);
170
252
 
171
253
  // If there's remaining text after the closing bracket, re-insert it at the front of the queue
172
254
  // so it can be processed for the next pattern (e.g., consecutive tags)
173
- const textAfterBracket = afterText.slice(closingBracket.position + closingBracket.suffix.length);
174
255
  if (textAfterBracket) {
175
256
  queue.unshift({
176
257
  type: 'text',
@@ -188,17 +269,6 @@ function enhanceTagBrackets(children) {
188
269
  return newChildren;
189
270
  }
190
271
 
191
- /**
192
- * Gets the text content of an element's first text child.
193
- */
194
- function getFirstTextValue(element) {
195
- const firstChild = element.children[0];
196
- if (firstChild && firstChild.type === 'text') {
197
- return firstChild.value;
198
- }
199
- return undefined;
200
- }
201
-
202
272
  /**
203
273
  * Reclassifies spans whose class + text content indicate a wrong token type.
204
274
  * For example, `<span class="pl-en">function</span>` is reclassified to
@@ -213,7 +283,7 @@ function reclassifyTokens(children) {
213
283
  if (!Array.isArray(className)) {
214
284
  continue;
215
285
  }
216
- const text = getFirstTextValue(child);
286
+ const text = getShallowTextContent(child);
217
287
  if (!text) {
218
288
  continue;
219
289
  }
@@ -227,29 +297,45 @@ function reclassifyTokens(children) {
227
297
  }
228
298
 
229
299
  /**
230
- * Enhances nullish values (`undefined`, `null`, `""`, `''`) by adding the `di-n`
231
- * class to their containing span elements. This allows CSS to style these
232
- * values distinctly from regular code, improving readability.
300
+ * Reclassifies `pl-smi` and `pl-k` spans whose text is a built-in type keyword
301
+ * (e.g. `string`, `number`, `void`) to `pl-c1 di-bt`.
233
302
  *
234
- * Mirrors the behavior of base-ui's `rehypeInlineCode` plugin, but uses
235
- * CSS classes (from the prettylights/docs-infra extension system) instead
236
- * of inline styles.
303
+ * Only applies to TypeScript-family languages, matching the contract in
304
+ * `extendSyntaxTokens` which gates `di-bt` on `isTs`.
305
+ *
306
+ * Starry Night tokenizes standalone type keywords inconsistently when there is
307
+ * no surrounding type context: most (`string`, `number`, …) become `pl-smi`
308
+ * (identifier), while `void` becomes `pl-k` (keyword). In inline code this is
309
+ * the common case — e.g. `` `string` `` — so we reclassify them to match the
310
+ * output of `type x = string` (where starry-night produces `pl-c1`) and add
311
+ * `di-bt` for semantic styling.
312
+ *
313
+ * For `pl-k` tokens (like `void`), we only reclassify when the token is the
314
+ * sole child of the code element to avoid mis-highlighting the unary `void`
315
+ * operator in expressions like `void fn()`.
237
316
  */
238
- function enhanceNullishValues(children) {
239
- for (const child of children) {
317
+ function enhanceBuiltInTypes(children) {
318
+ for (let index = 0; index < children.length; index += 1) {
319
+ const child = children[index];
240
320
  if (child.type !== 'element' || child.tagName !== 'span') {
241
321
  continue;
242
322
  }
243
- const text = getFirstTextValue(child);
244
- if (text && NULLISH_VALUES.includes(text)) {
245
- const className = child.properties?.className;
246
- if (Array.isArray(className)) {
247
- // Replace existing classes with di-n since nullish styling should take precedence
248
- child.properties.className = ['di-n'];
249
- } else {
250
- child.properties = child.properties || {};
251
- child.properties.className = ['di-n'];
252
- }
323
+ const className = child.properties?.className;
324
+ if (!Array.isArray(className)) {
325
+ continue;
326
+ }
327
+ const smiIndex = className.indexOf('pl-smi');
328
+ // Only reclassify pl-k when it is the only child (standalone keyword),
329
+ // so the void *operator* in multi-token expressions is left alone.
330
+ const kIndex = smiIndex === -1 && children.length === 1 ? className.indexOf('pl-k') : -1;
331
+ const targetIndex = smiIndex !== -1 ? smiIndex : kIndex;
332
+ if (targetIndex === -1) {
333
+ continue;
334
+ }
335
+ const text = getShallowTextContent(child);
336
+ if (text && BUILT_IN_TYPES.has(text)) {
337
+ className[targetIndex] = 'pl-c1';
338
+ className.push('di-bt');
253
339
  }
254
340
  }
255
341
  }
@@ -257,26 +343,24 @@ function enhanceNullishValues(children) {
257
343
  /**
258
344
  * A rehype plugin that enhances inline code elements in three ways:
259
345
  *
260
- * 1. **Tag bracket wrapping**: Wraps HTML tag angle brackets into the
261
- * syntax highlighting span, so `<div>` is styled as one unit.
346
+ * 1. **Tag bracket wrapping**: Wraps HTML/JSX tag patterns (opening bracket,
347
+ * tag-name span, closing bracket) in a wrapper span. HTML tags (`pl-ent`)
348
+ * get `<span class="di-ht">`, JSX component tags (`pl-c1`) get
349
+ * `<span class="di-jt">`. The original `pl-*` spans are preserved
350
+ * inside — no semantic information is destroyed.
262
351
  *
263
352
  * 2. **Token reclassification**: Corrects misidentified token classes,
264
353
  * e.g., `function` marked as `pl-en` is changed to `pl-k` (keyword).
265
354
  *
266
- * 3. **Nullish value styling**: Adds the `di-n` class to spans containing
267
- * `undefined`, `null`, `""`, or `''` for distinct visual treatment.
355
+ * 3. **Built-in type enhancement** (TypeScript only): Reclassifies standalone
356
+ * type keywords (`string`, `number`, `void`, etc.) from `pl-smi`/`pl-k`
357
+ * to `pl-c1 di-bt`, matching `extendSyntaxTokens` output in type context.
268
358
  *
269
359
  * Transforms patterns like:
270
360
  * `<code>&lt;<span class="pl-ent">div</span>&gt;</code>`
271
361
  *
272
362
  * Into:
273
- * `<code><span class="pl-ent">&lt;div&gt;</span></code>`
274
- *
275
- * And:
276
- * `<code><span class="pl-c1">undefined</span></code>`
277
- *
278
- * Into:
279
- * `<code><span class="di-n">undefined</span></code>`
363
+ * `<code><span class="di-ht">&lt;<span class="pl-ent">div</span>&gt;</span></code>`
280
364
  *
281
365
  * **Important**: This plugin should run after syntax highlighting plugins
282
366
  * (like transformHtmlCodeInline) as it modifies the structure
@@ -308,8 +392,10 @@ export default function enhanceCodeInline() {
308
392
  // Reclassify misidentified tokens (e.g., pl-en "function" → pl-k)
309
393
  reclassifyTokens(node.children);
310
394
 
311
- // Enhance nullish values (adds di-n class for distinct styling)
312
- enhanceNullishValues(node.children);
395
+ // Reclassify standalone built-in type keywords (TypeScript only)
396
+ if (getLanguageCapabilities(node).supportsTypes) {
397
+ enhanceBuiltInTypes(node.children);
398
+ }
313
399
  });
314
400
  };
315
401
  }
@@ -1,22 +1,6 @@
1
1
  import type { Element } from 'hast';
2
- /**
3
- * Language capabilities derived from the code element's `language-*` class.
4
- *
5
- * - `ts`/`typescript`: types ✓, JSX ✗, JS semantics ✓
6
- * - `tsx`: types ✓, JSX ✓, JS semantics ✓
7
- * - `js`/`javascript`: types ✗, JSX ✗, JS semantics ✓
8
- * - `jsx`: types ✗, JSX ✓, JS semantics ✓
9
- * - `css`/`scss`/`less`/`sass`: CSS semantics ✓
10
- * - no class / unknown: all ✗
11
- */
12
- export interface LanguageCapabilities {
13
- /** Whether `type Name` and `const name: Name =` syntax is recognized. */
14
- supportsTypes: boolean;
15
- /** Whether JSX `<Component prop={}>` syntax is recognized. */
16
- supportsJsx: boolean;
17
- /** Which platform semantics apply: `'js'` for function calls / JS patterns, `'css'` for CSS patterns, or `undefined` for unknown languages. */
18
- semantics?: 'js' | 'css';
19
- }
2
+ import type { LanguageCapabilities } from "../parseSource/languageCapabilities.mjs";
3
+ export type { LanguageCapabilities };
20
4
  /**
21
5
  * Detects language capabilities from a `<code>` element's class list.
22
6
  * Looks for a `language-*` class following standard markdown fenced-code conventions.
@@ -1,16 +1,4 @@
1
1
  import { getClassName } from "./hastUtils.mjs";
2
-
3
- /**
4
- * Language capabilities derived from the code element's `language-*` class.
5
- *
6
- * - `ts`/`typescript`: types ✓, JSX ✗, JS semantics ✓
7
- * - `tsx`: types ✓, JSX ✓, JS semantics ✓
8
- * - `js`/`javascript`: types ✗, JSX ✗, JS semantics ✓
9
- * - `jsx`: types ✗, JSX ✓, JS semantics ✓
10
- * - `css`/`scss`/`less`/`sass`: CSS semantics ✓
11
- * - no class / unknown: all ✗
12
- */
13
-
14
2
  const BASE_CAPABILITIES = {
15
3
  supportsTypes: false,
16
4
  supportsJsx: false
@@ -876,26 +876,18 @@ function detectCssImport(sourceText, pos, cssResult, cssExternals, cssFilePath,
876
876
  // Parse the @import statement
877
877
  const importResult = parseCssImportStatement(sourceText, pos);
878
878
  if (importResult.modulePath && importResult.pathStart !== undefined && importResult.pathEnd !== undefined) {
879
- // In CSS, imports are relative unless they have a protocol/hostname
880
- // Examples of external: "http://...", "https://...", "//example.com/style.css"
881
- // Examples of relative: "print.css", "./local.css", "../parent.css"
879
+ // In CSS, imports are relative unless they have a protocol, hostname,
880
+ // or are scoped npm packages (start with @scope/)
882
881
  const hasProtocol = /^https?:\/\//.test(importResult.modulePath);
883
882
  const hasHostname = /^\/\//.test(importResult.modulePath);
884
- const isExternal = hasProtocol || hasHostname;
883
+ const isScopedPackage = /^@[^/]+\//.test(importResult.modulePath);
884
+ const isRelative = !hasProtocol && !hasHostname && !isScopedPackage;
885
885
  const position = {
886
886
  start: positionMapper(importResult.pathStart),
887
887
  end: positionMapper(importResult.pathEnd)
888
888
  };
889
- if (isExternal) {
890
- if (!cssExternals[importResult.modulePath]) {
891
- cssExternals[importResult.modulePath] = {
892
- names: [],
893
- positions: []
894
- };
895
- }
896
- cssExternals[importResult.modulePath].positions.push(position);
897
- } else {
898
- // Treat as relative import - normalize the path if it doesn't start with ./ or ../
889
+ if (isRelative) {
890
+ // Normalize bare filenames (e.g. "reset.css") to relative paths
899
891
  let normalizedPath = importResult.modulePath;
900
892
  if (!normalizedPath.startsWith('./') && !normalizedPath.startsWith('../')) {
901
893
  normalizedPath = `./${normalizedPath}`;
@@ -909,6 +901,14 @@ function detectCssImport(sourceText, pos, cssResult, cssExternals, cssFilePath,
909
901
  };
910
902
  }
911
903
  cssResult[importResult.modulePath].positions.push(position);
904
+ } else {
905
+ if (!cssExternals[importResult.modulePath]) {
906
+ cssExternals[importResult.modulePath] = {
907
+ names: [],
908
+ positions: []
909
+ };
910
+ }
911
+ cssExternals[importResult.modulePath].positions.push(position);
912
912
  }
913
913
  }
914
914
  return {
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Default file extensions for JavaScript/TypeScript modules that can be resolved
3
3
  */
4
- export declare const JAVASCRIPT_MODULE_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mdx", ".d.ts"];
4
+ export declare const JAVASCRIPT_MODULE_EXTENSIONS: readonly ['.ts', '.tsx', '.js', '.jsx', '.mdx', '.d.ts'];
5
5
  /**
6
6
  * Extension priority for type-only imports - prioritize .d.ts first
7
7
  */
8
- export declare const TYPE_IMPORT_EXTENSIONS: readonly [".d.ts", ".ts", ".tsx", ".js", ".jsx", ".mdx"];
8
+ export declare const TYPE_IMPORT_EXTENSIONS: readonly ['.d.ts', '.ts', '.tsx', '.js', '.jsx', '.mdx'];
9
9
  /**
10
10
  * Extension priority for value imports - standard priority with .d.ts last
11
11
  */
12
- export declare const VALUE_IMPORT_EXTENSIONS: readonly [".ts", ".tsx", ".js", ".jsx", ".mdx", ".d.ts"];
12
+ export declare const VALUE_IMPORT_EXTENSIONS: readonly ['.ts', '.tsx', '.js', '.jsx', '.mdx', '.d.ts'];
13
13
  /**
14
14
  * Checks if a file path or import path represents a JavaScript/TypeScript module
15
15
  * @param path - The file path or import path to check
@@ -0,0 +1,15 @@
1
+ import type { Root } from 'hast';
2
+ /**
3
+ * TypeScript built-in type keywords that starry-night classifies as `pl-c1`.
4
+ * These are language primitives from the TypeScript specification.
5
+ */
6
+ export declare const BUILT_IN_TYPES: Set<string>;
7
+ /**
8
+ * Extends a syntax-highlighted HAST tree with additional `di-*` CSS classes
9
+ * for fine-grained styling control. All extensions are **additive** — existing
10
+ * `pl-*` classes from starry-night are never removed.
11
+ *
12
+ * @param tree - The HAST root node produced by starry-night's `highlight()`
13
+ * @param grammarScope - The grammar scope used for highlighting (e.g., 'source.tsx', 'source.css')
14
+ */
15
+ export declare function extendSyntaxTokens(tree: Root, grammarScope: string): void;
@@ -0,0 +1,422 @@
1
+ import { getShallowTextContent } from "../loadServerTypes/hastTypeUtils.mjs";
2
+ import { getLanguageCapabilitiesFromScope } from "./languageCapabilities.mjs";
3
+
4
+ /**
5
+ * Classes that can represent CSS attribute selector names inside `[...]`.
6
+ * Current starry-night uses `pl-c1`, but a future fix may use `pl-e`.
7
+ */
8
+ const CSS_ATTR_SELECTOR_CLASSES = new Set(['pl-c1', 'pl-e']);
9
+
10
+ /**
11
+ * TypeScript built-in type keywords that starry-night classifies as `pl-c1`.
12
+ * These are language primitives from the TypeScript specification.
13
+ */
14
+ export const BUILT_IN_TYPES = new Set(['string', 'number', 'boolean', 'void', 'never', 'symbol', 'object', 'any', 'unknown', 'bigint']);
15
+
16
+ /**
17
+ * Checks whether a `pl-c1` token's text represents a numeric value.
18
+ *
19
+ * Since starry-night already classified the token as a constant (`pl-c1`),
20
+ * we only need to distinguish numbers from named constants like `Button` or `color`.
21
+ * A simple first-character check is sufficient: numbers start with a digit,
22
+ * optional `-` sign, or `.` followed by a digit.
23
+ *
24
+ * Matches: `42`, `3.14`, `-1`, `.5`, `0xFF`, `100px`, `50%`, `3em`
25
+ * Does not match: `color`, `red`, `Button`, `NaN`, `Infinity`
26
+ */
27
+ function isNumericConstant(text) {
28
+ if (text.length === 0) {
29
+ return false;
30
+ }
31
+ const start = text[0] === '-' ? 1 : 0;
32
+ if (start >= text.length) {
33
+ return false;
34
+ }
35
+ const charCode = text.charCodeAt(start);
36
+
37
+ // Starts with a digit (0-9)
38
+ if (charCode >= 48 && charCode <= 57) {
39
+ return true;
40
+ }
41
+
42
+ // Starts with '.' followed by a digit
43
+ if (charCode === 46 && start + 1 < text.length) {
44
+ const nextCharCode = text.charCodeAt(start + 1);
45
+ return nextCharCode >= 48 && nextCharCode <= 57;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Gets the first CSS class from an element's className array.
52
+ */
53
+ function getFirstClass(element) {
54
+ const className = element.properties?.className;
55
+ if (Array.isArray(className) && typeof className[0] === 'string') {
56
+ return className[0];
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ /**
62
+ * Adds a CSS class to an element's className array (additive, never removes existing classes).
63
+ */
64
+ function addClass(element, cls) {
65
+ if (!element.properties) {
66
+ element.properties = {};
67
+ }
68
+ if (Array.isArray(element.properties.className)) {
69
+ element.properties.className.push(cls);
70
+ } else {
71
+ element.properties.className = [cls];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Replaces one CSS class with another in an element's className array.
77
+ */
78
+ function replaceClass(element, oldCls, newCls) {
79
+ const className = element.properties?.className;
80
+ if (Array.isArray(className)) {
81
+ const idx = className.indexOf(oldCls);
82
+ if (idx !== -1) {
83
+ className[idx] = newCls;
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Enhances `pl-c1` (constant) spans with more specific `di-*` classes
90
+ * based on the text content.
91
+ *
92
+ * Language-agnostic:
93
+ * - Numbers → `di-num`
94
+ * - Booleans (`true`, `false`) → `di-bool`
95
+ * - Nullish (`null`, `undefined`) → `di-n`
96
+ *
97
+ * JS/TS family only (`isJs`):
98
+ * - `this`, `super` → `di-this`
99
+ *
100
+ * TS family only (`isTs`):
101
+ * - Built-in type keywords (`string`, `number`, etc.) → `di-bt`
102
+ */
103
+ function enhanceConstantSpan(element, isJs, isTs) {
104
+ const text = getShallowTextContent(element);
105
+ if (!text) {
106
+ return;
107
+ }
108
+ if (text === 'true' || text === 'false') {
109
+ addClass(element, 'di-bool');
110
+ } else if (text === 'null' || text === 'undefined') {
111
+ addClass(element, 'di-n');
112
+ } else if (isNumericConstant(text)) {
113
+ addClass(element, 'di-num');
114
+ } else if (isJs && (text === 'this' || text === 'super')) {
115
+ addClass(element, 'di-this');
116
+ } else if (isTs && BUILT_IN_TYPES.has(text)) {
117
+ addClass(element, 'di-bt');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Enhances `pl-s` (string) spans for empty string literals (`""`, `''`)
123
+ * by adding the `di-n` (nullish) class.
124
+ *
125
+ * Starry-night tokenizes an empty string as exactly two `pl-pds` quote-delimiter
126
+ * spans with no content between them:
127
+ * `<span class="pl-s"><span class="pl-pds">"</span><span class="pl-pds">"</span></span>`
128
+ * so we can detect it structurally without recursively serializing the text.
129
+ */
130
+ function enhanceStringSpan(element) {
131
+ const {
132
+ children
133
+ } = element;
134
+ if (children.length !== 2) {
135
+ return;
136
+ }
137
+ const [open, close] = children;
138
+ if (open.type === 'element' && getFirstClass(open) === 'pl-pds' && close.type === 'element' && getFirstClass(close) === 'pl-pds') {
139
+ addClass(element, 'di-n');
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Single-pass enhancement of a HAST children array. Processes each child exactly
145
+ * once, applying all per-element and sibling-context enhancements in one iteration.
146
+ * Recursively enhances nested elements.
147
+ *
148
+ * Per-element enhancements (applied to individual spans):
149
+ * - `pl-c1` → `di-num`, `di-bool`, `di-n`, `di-this`, `di-bt` via enhanceConstantSpan
150
+ * - `pl-s` → `di-n` for empty strings via enhanceStringSpan
151
+ *
152
+ * Sibling-context enhancements (depend on neighbor nodes or positional state):
153
+ * - CSS `&` nesting selector → wraps in `pl-ent` span
154
+ * - CSS `[attr]` → `di-da` on attribute name spans
155
+ * - CSS `property: value` → `di-cp` / `di-cv` based on colon position
156
+ * - HTML/JSX `<tag attr=value>` → `di-ak`, `di-ae`, `di-av`
157
+ * - JSX `<Component>` → `di-jsx` on component name spans
158
+ */
159
+ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
160
+ // CSS declaration state: tracks position relative to { } : ; [ ]
161
+ let cssInsideBlock = false;
162
+ let cssInsideBracket = false;
163
+ let cssAfterColon = false;
164
+
165
+ // HTML/JSX tag state: whether we're between < and >
166
+ let htmlInsideTag = false;
167
+
168
+ // Whether a span appeared between the last text node and the current position.
169
+ // Used to detect attribute context for = wrapping (replaces backward scanning).
170
+ let hasSpanSinceLastText = false;
171
+ for (let index = 0; index < children.length; index += 1) {
172
+ const child = children[index];
173
+
174
+ // ── Text nodes: state tracking and structural splits ──
175
+ if (child.type === 'text') {
176
+ const savedSpanFlag = hasSpanSinceLastText;
177
+ hasSpanSinceLastText = false;
178
+ const {
179
+ value
180
+ } = child;
181
+
182
+ // CSS: track { } [ ] : ; state and wrap & nesting selectors
183
+ if (isCss) {
184
+ const ampIndex = value.indexOf('&');
185
+ const trackEnd = ampIndex !== -1 ? ampIndex : value.length;
186
+ for (let ci = 0; ci < trackEnd; ci += 1) {
187
+ const char = value[ci];
188
+ if (char === '{') {
189
+ cssInsideBlock = true;
190
+ cssAfterColon = false;
191
+ } else if (char === '}') {
192
+ cssInsideBlock = false;
193
+ cssAfterColon = false;
194
+ } else if (char === '[') {
195
+ cssInsideBracket = true;
196
+ } else if (char === ']') {
197
+ cssInsideBracket = false;
198
+ } else if (char === ':' && cssInsideBlock && !cssInsideBracket) {
199
+ cssAfterColon = true;
200
+ } else if (char === ';') {
201
+ cssAfterColon = false;
202
+ }
203
+ }
204
+
205
+ // Wrap bare & in a pl-ent span to match GitHub rendering of CSS nesting selector
206
+ if (ampIndex !== -1) {
207
+ const before = value.slice(0, ampIndex);
208
+ const after = value.slice(ampIndex + 1);
209
+ const ampSpan = {
210
+ type: 'element',
211
+ tagName: 'span',
212
+ properties: {
213
+ className: ['pl-ent']
214
+ },
215
+ children: [{
216
+ type: 'text',
217
+ value: '&'
218
+ }]
219
+ };
220
+ const newNodes = [];
221
+ if (before) {
222
+ newNodes.push({
223
+ type: 'text',
224
+ value: before
225
+ });
226
+ }
227
+ newNodes.push(ampSpan);
228
+ if (after) {
229
+ newNodes.push({
230
+ type: 'text',
231
+ value: after
232
+ });
233
+ }
234
+ children.splice(index, 1, ...newNodes);
235
+ // Advance past the inserted span to process remaining text for more & chars
236
+ index += newNodes.indexOf(ampSpan);
237
+ continue;
238
+ }
239
+ }
240
+
241
+ // HTML/JSX: track < > tag boundaries and wrap bare = in attribute context
242
+ if (isHtmlJsx) {
243
+ for (let ci = 0; ci < value.length; ci += 1) {
244
+ if (value[ci] === '<') {
245
+ htmlInsideTag = true;
246
+ } else if (value[ci] === '>') {
247
+ htmlInsideTag = false;
248
+ }
249
+ }
250
+ if (htmlInsideTag && savedSpanFlag) {
251
+ const equalsIndex = value.indexOf('=');
252
+ if (equalsIndex !== -1) {
253
+ // Tag the following pl-s span as attribute value
254
+ const nextChild = children[index + 1];
255
+ if (nextChild && nextChild.type === 'element' && nextChild.tagName === 'span' && getFirstClass(nextChild) === 'pl-s') {
256
+ addClass(nextChild, 'di-av');
257
+ }
258
+
259
+ // Split text around = and wrap in di-ae span
260
+ const before = value.slice(0, equalsIndex);
261
+ const after = value.slice(equalsIndex + 1);
262
+ const equalsSpan = {
263
+ type: 'element',
264
+ tagName: 'span',
265
+ properties: {
266
+ className: ['di-ae']
267
+ },
268
+ children: [{
269
+ type: 'text',
270
+ value: '='
271
+ }]
272
+ };
273
+ const newNodes = [];
274
+ if (before) {
275
+ newNodes.push({
276
+ type: 'text',
277
+ value: before
278
+ });
279
+ }
280
+ newNodes.push(equalsSpan);
281
+ if (after) {
282
+ newNodes.push({
283
+ type: 'text',
284
+ value: after
285
+ });
286
+ }
287
+ children.splice(index, 1, ...newNodes);
288
+ index += newNodes.length - 1;
289
+ hasSpanSinceLastText = newNodes[newNodes.length - 1].type === 'element';
290
+ }
291
+ }
292
+ }
293
+ continue;
294
+ }
295
+
296
+ // ── Non-element nodes: skip ──
297
+ if (child.type !== 'element') {
298
+ continue;
299
+ }
300
+
301
+ // Recurse into nested elements (frames, lines, nested spans)
302
+ if (child.children.length > 0) {
303
+ enhanceChildren(child.children, isCss, isHtmlJsx, isJs, isTs, isJsx);
304
+ }
305
+ if (child.tagName !== 'span') {
306
+ continue;
307
+ }
308
+ const hadPrecedingSpan = hasSpanSinceLastText;
309
+ hasSpanSinceLastText = true;
310
+ const firstClass = getFirstClass(child);
311
+
312
+ // ── Per-element enhancements (all grammars) ──
313
+ if (firstClass === 'pl-c1') {
314
+ enhanceConstantSpan(child, isJs, isTs);
315
+ } else if (firstClass === 'pl-s') {
316
+ enhanceStringSpan(child);
317
+ }
318
+
319
+ // ── CSS-specific enhancements ──
320
+ if (isCss) {
321
+ // CSS attribute selector name: span preceded by text ending with [
322
+ if (firstClass && CSS_ATTR_SELECTOR_CLASSES.has(firstClass) && index > 0) {
323
+ const prev = children[index - 1];
324
+ if (prev.type === 'text' && prev.value.endsWith('[')) {
325
+ addClass(child, 'di-da');
326
+ }
327
+ }
328
+
329
+ // CSS property name / value classification based on : position
330
+ if (firstClass === 'pl-c1' && cssInsideBlock && !cssInsideBracket) {
331
+ addClass(child, cssAfterColon ? 'di-cv' : 'di-cp');
332
+ }
333
+ }
334
+
335
+ // ── HTML/JSX attribute enhancements ──
336
+ if (isHtmlJsx && htmlInsideTag) {
337
+ // Attribute key: pl-e inside a tag
338
+ if (firstClass === 'pl-e') {
339
+ addClass(child, 'di-ak');
340
+ }
341
+
342
+ // Attribute equals: pl-k span containing =
343
+ if (firstClass === 'pl-k' && getShallowTextContent(child) === '=' && hadPrecedingSpan) {
344
+ addClass(child, 'di-ae');
345
+ const nextChild = children[index + 1];
346
+ if (nextChild && nextChild.type === 'element' && nextChild.tagName === 'span' && getFirstClass(nextChild) === 'pl-s') {
347
+ addClass(nextChild, 'di-av');
348
+ }
349
+ }
350
+ }
351
+
352
+ // ── JSX component name detection ──
353
+ if (isJsx && index > 0) {
354
+ const prev = children[index - 1];
355
+
356
+ // Opening/closing: text ending in < or </ followed by pl-c1
357
+ if (firstClass === 'pl-c1' && prev.type === 'text') {
358
+ if (prev.value.endsWith('<') || prev.value.endsWith('</')) {
359
+ addClass(child, 'di-jsx');
360
+ }
361
+ }
362
+
363
+ // Standalone closing: pl-k("</") followed by pl-smi or pl-c1
364
+ // Normalize the token shape to match the text-bracket pattern:
365
+ // - pl-smi JSX component (PascalCase) → pl-c1 + di-jsx
366
+ // - pl-smi HTML element (lowercase) → pl-ent
367
+ // - Remove pl-k from the adjacent bracket spans
368
+ if ((firstClass === 'pl-smi' || firstClass === 'pl-c1') && prev.type === 'element' && prev.tagName === 'span' && getFirstClass(prev) === 'pl-k' && getShallowTextContent(prev) === '</') {
369
+ // Find the closing bracket span: pl-k(">")
370
+ const closeBracket = children[index + 1];
371
+ const hasCloseBracket = closeBracket && closeBracket.type === 'element' && closeBracket.tagName === 'span' && getFirstClass(closeBracket) === 'pl-k' && getShallowTextContent(closeBracket) === '>';
372
+ if (firstClass === 'pl-c1') {
373
+ addClass(child, 'di-jsx');
374
+ } else {
375
+ const tagText = getShallowTextContent(child);
376
+ const isComponent = tagText && tagText[0] === tagText[0].toUpperCase() && tagText[0] !== tagText[0].toLowerCase();
377
+ if (isComponent) {
378
+ // JSX component: pl-smi → pl-c1 + di-jsx
379
+ replaceClass(child, 'pl-smi', 'pl-c1');
380
+ addClass(child, 'di-jsx');
381
+ } else {
382
+ // HTML element: pl-smi → pl-ent
383
+ replaceClass(child, 'pl-smi', 'pl-ent');
384
+ }
385
+ }
386
+
387
+ // Replace bracket spans with text nodes to match the text-bracket pattern.
388
+ // This allows enhanceCodeInline to handle both patterns uniformly.
389
+ const prevText = getShallowTextContent(prev) ?? '</';
390
+ children[index - 1] = {
391
+ type: 'text',
392
+ value: prevText
393
+ };
394
+ if (hasCloseBracket) {
395
+ const closeText = getShallowTextContent(closeBracket) ?? '>';
396
+ children[index + 1] = {
397
+ type: 'text',
398
+ value: closeText
399
+ };
400
+ }
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Extends a syntax-highlighted HAST tree with additional `di-*` CSS classes
408
+ * for fine-grained styling control. All extensions are **additive** — existing
409
+ * `pl-*` classes from starry-night are never removed.
410
+ *
411
+ * @param tree - The HAST root node produced by starry-night's `highlight()`
412
+ * @param grammarScope - The grammar scope used for highlighting (e.g., 'source.tsx', 'source.css')
413
+ */
414
+ export function extendSyntaxTokens(tree, grammarScope) {
415
+ const caps = getLanguageCapabilitiesFromScope(grammarScope);
416
+ const isCss = caps.semantics === 'css';
417
+ const isHtmlJsx = caps.supportsJsx || grammarScope === 'text.html.basic';
418
+ const isJs = caps.semantics === 'js';
419
+ const isTs = caps.supportsTypes;
420
+ const isJsx = caps.supportsJsx;
421
+ enhanceChildren(tree.children, isCss, isHtmlJsx, isJs, isTs, isJsx);
422
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Language capabilities derived from a grammar scope or language class.
3
+ *
4
+ * Shared by both `extendSyntaxTokens` (which receives grammar scopes like
5
+ * `'source.tsx'`) and `enhanceCodeTypes` (which reads `language-*` CSS classes).
6
+ */
7
+ export interface LanguageCapabilities {
8
+ /** Whether `type Name` and `const name: Name =` syntax is recognized. */
9
+ supportsTypes: boolean;
10
+ /** Whether JSX `<Component prop={}>` syntax is recognized. */
11
+ supportsJsx: boolean;
12
+ /**
13
+ * Which platform semantics apply: `'js'` for function calls / JS patterns,
14
+ * `'css'` for CSS patterns, or `undefined` for unknown languages.
15
+ */
16
+ semantics?: 'js' | 'css';
17
+ }
18
+ /**
19
+ * Resolves language capabilities from a starry-night grammar scope string.
20
+ *
21
+ * Note: `.jsx` files map to `source.tsx` via the extension map, so there is
22
+ * no separate `source.jsx` scope. MDX is treated as JS+TS+JSX because it
23
+ * embeds TypeScript JSX.
24
+ */
25
+ export declare function getLanguageCapabilitiesFromScope(grammarScope: string): LanguageCapabilities;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Language capabilities derived from a grammar scope or language class.
3
+ *
4
+ * Shared by both `extendSyntaxTokens` (which receives grammar scopes like
5
+ * `'source.tsx'`) and `enhanceCodeTypes` (which reads `language-*` CSS classes).
6
+ */
7
+
8
+ const BASE_CAPABILITIES = {
9
+ supportsTypes: false,
10
+ supportsJsx: false
11
+ };
12
+
13
+ /**
14
+ * Resolves language capabilities from a starry-night grammar scope string.
15
+ *
16
+ * Note: `.jsx` files map to `source.tsx` via the extension map, so there is
17
+ * no separate `source.jsx` scope. MDX is treated as JS+TS+JSX because it
18
+ * embeds TypeScript JSX.
19
+ */
20
+ export function getLanguageCapabilitiesFromScope(grammarScope) {
21
+ switch (grammarScope) {
22
+ case 'source.js':
23
+ return {
24
+ supportsTypes: false,
25
+ supportsJsx: false,
26
+ semantics: 'js'
27
+ };
28
+ case 'source.ts':
29
+ return {
30
+ supportsTypes: true,
31
+ supportsJsx: false,
32
+ semantics: 'js'
33
+ };
34
+ case 'source.tsx':
35
+ return {
36
+ supportsTypes: true,
37
+ supportsJsx: true,
38
+ semantics: 'js'
39
+ };
40
+ case 'source.mdx':
41
+ return {
42
+ supportsTypes: true,
43
+ supportsJsx: true,
44
+ semantics: 'js'
45
+ };
46
+ case 'source.css':
47
+ return {
48
+ supportsTypes: false,
49
+ supportsJsx: false,
50
+ semantics: 'css'
51
+ };
52
+ default:
53
+ return BASE_CAPABILITIES;
54
+ }
55
+ }
@@ -1,6 +1,7 @@
1
1
  import { createStarryNight } from '@wooorm/starry-night';
2
2
  import { grammars, extensionMap, getGrammarFromLanguage } from "./grammars.mjs";
3
3
  import { starryNightGutter } from "./addLineGutters.mjs";
4
+ import { extendSyntaxTokens } from "./extendSyntaxTokens.mjs";
4
5
  const STARRY_NIGHT_KEY = '__docs_infra_starry_night_instance__';
5
6
 
6
7
  /**
@@ -39,6 +40,7 @@ export const parseSource = (source, fileName, language) => {
39
40
  };
40
41
  }
41
42
  const highlighted = starryNight.highlight(source, grammarScope);
43
+ extendSyntaxTokens(highlighted, grammarScope); // mutates the tree to add di-* classes
42
44
  const sourceLines = source.split(/\r?\n|\r/);
43
45
  starryNightGutter(highlighted, sourceLines); // mutates the tree to add line gutters
44
46
 
@@ -1,6 +1,7 @@
1
1
  import { createStarryNight } from '@wooorm/starry-night';
2
2
  import { visit } from 'unist-util-visit';
3
3
  import { grammars, extensionMap } from "../parseSource/grammars.mjs";
4
+ import { extendSyntaxTokens } from "../parseSource/extendSyntaxTokens.mjs";
4
5
  import { removePrefixFromHighlightedNodes } from "./removePrefixFromHighlightedNodes.mjs";
5
6
  const STARRY_NIGHT_KEY = '__docs_infra_starry_night_instance__';
6
7
 
@@ -114,6 +115,7 @@ export default function transformHtmlCodeInline(options = {}) {
114
115
 
115
116
  // Apply syntax highlighting
116
117
  const highlighted = starryNight.highlight(sourceToHighlight, extensionMap[fileType]);
118
+ extendSyntaxTokens(highlighted, extensionMap[fileType]);
117
119
 
118
120
  // Replace the code element's children with the highlighted nodes
119
121
  if (highlighted.type === 'root' && highlighted.children) {
@@ -9,7 +9,7 @@ export type UseCopierOpts = {
9
9
  timeout?: number;
10
10
  };
11
11
  export declare function useCopier(contents: (() => string | undefined) | string, opts?: UseCopierOpts): {
12
- copy: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>;
12
+ copy: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
13
13
  recentlySuccessful: boolean;
14
14
  };
15
15
  export {};
@@ -49,31 +49,31 @@ export declare function useDemo<T extends {} = {}>(contentProps: ContentProps<T>
49
49
  variants: string[];
50
50
  selectedVariant: string;
51
51
  selectVariant: (variant: string | null) => void;
52
- files: {
52
+ files: Array<{
53
53
  name: string;
54
- slug?: string | undefined;
54
+ slug?: string;
55
55
  component: React.ReactNode;
56
- }[];
56
+ }>;
57
57
  selectedFile: React.ReactNode;
58
58
  selectedFileLines: number;
59
59
  selectedFileName: string | undefined;
60
60
  selectFileName: (fileName: string) => void;
61
- allFilesSlugs: {
61
+ allFilesSlugs: Array<{
62
62
  fileName: string;
63
63
  slug: string;
64
64
  variantName: string;
65
- }[];
65
+ }>;
66
66
  expanded: boolean;
67
67
  expand: () => void;
68
68
  setExpanded: (expanded: boolean) => void;
69
- copy: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>;
69
+ copy: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
70
70
  availableTransforms: string[];
71
71
  selectedTransform: string | null | undefined;
72
72
  selectTransform: (transformName: string | null) => void;
73
- setSource?: ((source: string) => void) | undefined;
73
+ setSource?: (source: string) => void;
74
74
  userProps: T & {
75
- name?: string | undefined;
76
- slug?: string | undefined;
75
+ name?: string;
76
+ slug?: string;
77
77
  };
78
78
  component: string | number | bigint | true | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | Iterable<React.ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | null;
79
79
  ref: React.RefObject<HTMLDivElement | null>;
@@ -3,27 +3,27 @@ import type { UseSearchOptions, UseSearchResult } from "./types.mjs";
3
3
  * Orama schema definition for our search document
4
4
  */
5
5
  declare const searchSchema: {
6
- readonly type: "string";
7
- readonly group: "string";
8
- readonly title: "string";
9
- readonly description: "string";
10
- readonly slug: "string";
11
- readonly sectionTitle: "string";
12
- readonly prefix: "string";
13
- readonly path: "string";
14
- readonly keywords: "string";
15
- readonly page: "string";
16
- readonly pageKeywords: "string";
17
- readonly sections: "string";
18
- readonly subsections: "string";
19
- readonly part: "string";
20
- readonly export: "string";
21
- readonly types: "string";
22
- readonly props: "string";
23
- readonly dataAttributes: "string";
24
- readonly cssVariables: "string";
25
- readonly section: "string";
26
- readonly subsection: "string";
6
+ readonly type: 'string';
7
+ readonly group: 'string';
8
+ readonly title: 'string';
9
+ readonly description: 'string';
10
+ readonly slug: 'string';
11
+ readonly sectionTitle: 'string';
12
+ readonly prefix: 'string';
13
+ readonly path: 'string';
14
+ readonly keywords: 'string';
15
+ readonly page: 'string';
16
+ readonly pageKeywords: 'string';
17
+ readonly sections: 'string';
18
+ readonly subsections: 'string';
19
+ readonly part: 'string';
20
+ readonly export: 'string';
21
+ readonly types: 'string';
22
+ readonly props: 'string';
23
+ readonly dataAttributes: 'string';
24
+ readonly cssVariables: 'string';
25
+ readonly section: 'string';
26
+ readonly subsection: 'string';
27
27
  };
28
28
  type SearchSchema = typeof searchSchema;
29
29
  export declare const defaultSearchBoost: {