@mui/internal-docs-infra 0.8.1-canary.0 → 0.8.1-canary.1
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/CodeProvider/CodeProvider.mjs +4 -1
- package/abstractCreateDemo/abstractCreateDemo.d.mts +4 -4
- package/abstractCreateDemoClient/abstractCreateDemoClient.d.mts +1 -1
- package/package.json +2 -2
- package/pipeline/enhanceCodeInline/enhanceCodeInline.d.mts +9 -11
- package/pipeline/enhanceCodeInline/enhanceCodeInline.mjs +169 -83
- package/pipeline/enhanceCodeTypes/getLanguageCapabilities.d.mts +2 -18
- package/pipeline/enhanceCodeTypes/getLanguageCapabilities.mjs +0 -12
- package/pipeline/loaderUtils/parseImportsAndComments.mjs +14 -14
- package/pipeline/loaderUtils/resolveModulePath.d.mts +3 -3
- package/pipeline/parseSource/extendSyntaxTokens.d.mts +15 -0
- package/pipeline/parseSource/extendSyntaxTokens.mjs +422 -0
- package/pipeline/parseSource/languageCapabilities.d.mts +25 -0
- package/pipeline/parseSource/languageCapabilities.mjs +55 -0
- package/pipeline/parseSource/parseSource.mjs +2 -0
- package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +2 -0
- package/useCopier/index.d.mts +1 -1
- package/useDemo/useDemo.d.mts +9 -9
- package/useSearch/useSearch.d.mts +21 -21
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
29
|
+
export declare function createDemoClientFactory(options: AbstractCreateDemoClientOptions): (url: string, meta?: CreateDemoClientMeta) => React.ComponentType<{
|
|
30
30
|
children: React.ReactNode;
|
|
31
31
|
}>;
|
|
32
32
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.8.1-canary.
|
|
3
|
+
"version": "0.8.1-canary.1",
|
|
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": "
|
|
636
|
+
"gitSha": "4e00a23176ff08c1393829f9f3c71323e3735550"
|
|
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
|
|
6
|
-
*
|
|
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. **
|
|
12
|
-
* `
|
|
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><<span class="pl-ent">div</span>></code>`
|
|
16
20
|
*
|
|
17
21
|
* Into:
|
|
18
|
-
* `<code><span class="pl-ent">&
|
|
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"><<span class="pl-ent">div</span>></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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - pl-
|
|
7
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
33
|
+
function getTagWrapperClass(element) {
|
|
33
34
|
const className = element.properties?.className;
|
|
34
35
|
if (!Array.isArray(className)) {
|
|
35
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
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
|
-
|
|
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(
|
|
247
|
+
newChildren.push(wrapperSpan);
|
|
166
248
|
|
|
167
|
-
// Remove
|
|
168
|
-
|
|
169
|
-
queue.
|
|
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 =
|
|
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
|
-
*
|
|
231
|
-
*
|
|
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
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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
|
|
239
|
-
for (
|
|
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
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
261
|
-
*
|
|
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. **
|
|
267
|
-
* `
|
|
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><<span class="pl-ent">div</span>></code>`
|
|
271
361
|
*
|
|
272
362
|
* Into:
|
|
273
|
-
* `<code><span class="pl-ent">&
|
|
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"><<span class="pl-ent">div</span>></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
|
-
//
|
|
312
|
-
|
|
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
|
-
|
|
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
|
|
880
|
-
//
|
|
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
|
|
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 (
|
|
890
|
-
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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) {
|
package/useCopier/index.d.mts
CHANGED
|
@@ -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
|
|
12
|
+
copy: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void>;
|
|
13
13
|
recentlySuccessful: boolean;
|
|
14
14
|
};
|
|
15
15
|
export {};
|
package/useDemo/useDemo.d.mts
CHANGED
|
@@ -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
|
|
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
|
|
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?: (
|
|
73
|
+
setSource?: (source: string) => void;
|
|
74
74
|
userProps: T & {
|
|
75
|
-
name?: string
|
|
76
|
-
slug?: string
|
|
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:
|
|
7
|
-
readonly group:
|
|
8
|
-
readonly title:
|
|
9
|
-
readonly description:
|
|
10
|
-
readonly slug:
|
|
11
|
-
readonly sectionTitle:
|
|
12
|
-
readonly prefix:
|
|
13
|
-
readonly path:
|
|
14
|
-
readonly keywords:
|
|
15
|
-
readonly page:
|
|
16
|
-
readonly pageKeywords:
|
|
17
|
-
readonly sections:
|
|
18
|
-
readonly subsections:
|
|
19
|
-
readonly part:
|
|
20
|
-
readonly export:
|
|
21
|
-
readonly types:
|
|
22
|
-
readonly props:
|
|
23
|
-
readonly dataAttributes:
|
|
24
|
-
readonly cssVariables:
|
|
25
|
-
readonly section:
|
|
26
|
-
readonly subsection:
|
|
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: {
|