@shikijs/transformers 3.22.0 → 4.0.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.
Files changed (3) hide show
  1. package/dist/index.d.mts +173 -158
  2. package/dist/index.mjs +653 -742
  3. package/package.json +8 -5
package/dist/index.mjs CHANGED
@@ -1,828 +1,739 @@
1
+ //#region src/shared/parse-comments.ts
2
+ /**
3
+ * some comment formats have to be located at the end of line
4
+ * hence we can skip matching them for other tokens
5
+ */
1
6
  const matchers = [
2
- [/^(<!--)(.+)(-->)$/, false],
3
- [/^(\/\*)(.+)(\*\/)$/, false],
4
- [/^(\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/, true],
5
- /**
6
- * for multi-line comments like this
7
- */
8
- [/^(\*)(.+)$/, true]
7
+ [/^(<!--)(.+)(-->)$/, false],
8
+ [/^(\/\*)(.+)(\*\/)$/, false],
9
+ [/^(\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/, true],
10
+ [/^(\*)(.+)$/, true]
9
11
  ];
12
+ /**
13
+ * @param lines line tokens
14
+ * @param jsx enable JSX parsing
15
+ * @param matchAlgorithm matching algorithm
16
+ */
10
17
  function parseComments(lines, jsx, matchAlgorithm) {
11
- const out = [];
12
- for (const line of lines) {
13
- if (matchAlgorithm === "v3") {
14
- const splittedElements = line.children.flatMap((element, idx) => {
15
- if (element.type !== "element")
16
- return element;
17
- const token = element.children[0];
18
- if (token.type !== "text")
19
- return element;
20
- const isLast = idx === line.children.length - 1;
21
- const isComment = matchToken(token.value, isLast);
22
- if (!isComment)
23
- return element;
24
- const rawSplits = token.value.split(/(\s+\/\/)/);
25
- if (rawSplits.length <= 1)
26
- return element;
27
- let splits = [rawSplits[0]];
28
- for (let i = 1; i < rawSplits.length; i += 2) {
29
- splits.push(rawSplits[i] + (rawSplits[i + 1] || ""));
30
- }
31
- splits = splits.filter(Boolean);
32
- if (splits.length <= 1)
33
- return element;
34
- return splits.map((split) => {
35
- return {
36
- ...element,
37
- children: [
38
- {
39
- type: "text",
40
- value: split
41
- }
42
- ]
43
- };
44
- });
45
- });
46
- if (splittedElements.length !== line.children.length)
47
- line.children = splittedElements;
48
- }
49
- const elements = line.children;
50
- let start = elements.length - 1;
51
- if (matchAlgorithm === "v1")
52
- start = 0;
53
- else if (jsx)
54
- start = elements.length - 2;
55
- for (let i = Math.max(start, 0); i < elements.length; i++) {
56
- const token = elements[i];
57
- if (token.type !== "element")
58
- continue;
59
- const head = token.children.at(0);
60
- if (head?.type !== "text")
61
- continue;
62
- const isLast = i === elements.length - 1;
63
- let match = matchToken(head.value, isLast);
64
- let additionalTokens;
65
- if (!match && i > 0 && head.value.trim().startsWith("[!code")) {
66
- const prevToken = elements[i - 1];
67
- if (prevToken?.type === "element") {
68
- const prevHead = prevToken.children.at(0);
69
- if (prevHead?.type === "text" && prevHead.value.includes("//")) {
70
- const combinedValue = prevHead.value + head.value;
71
- const combinedMatch = matchToken(combinedValue, isLast);
72
- if (combinedMatch) {
73
- match = combinedMatch;
74
- out.push({
75
- info: combinedMatch,
76
- line,
77
- token: prevToken,
78
- // Use the previous token as the main token
79
- isLineCommentOnly: elements.length === 2 && prevToken.children.length === 1 && token.children.length === 1,
80
- isJsxStyle: false,
81
- additionalTokens: [token]
82
- // Current token is the additional one
83
- });
84
- continue;
85
- }
86
- }
87
- }
88
- }
89
- if (!match)
90
- continue;
91
- if (jsx && !isLast && i !== 0) {
92
- const isJsxStyle = isValue(elements[i - 1], "{") && isValue(elements[i + 1], "}");
93
- out.push({
94
- info: match,
95
- line,
96
- token,
97
- isLineCommentOnly: elements.length === 3 && token.children.length === 1,
98
- isJsxStyle,
99
- additionalTokens
100
- });
101
- } else {
102
- out.push({
103
- info: match,
104
- line,
105
- token,
106
- isLineCommentOnly: elements.length === 1 && token.children.length === 1,
107
- isJsxStyle: false,
108
- additionalTokens
109
- });
110
- }
111
- }
112
- }
113
- return out;
18
+ const out = [];
19
+ for (const line of lines) {
20
+ if (matchAlgorithm === "v3") {
21
+ const splittedElements = line.children.flatMap((element, idx) => {
22
+ if (element.type !== "element") return element;
23
+ const token = element.children[0];
24
+ if (token.type !== "text") return element;
25
+ const isLast = idx === line.children.length - 1;
26
+ if (!matchToken(token.value, isLast)) return element;
27
+ const rawSplits = token.value.split(/(\s+\/\/)/);
28
+ if (rawSplits.length <= 1) return element;
29
+ let splits = [rawSplits[0]];
30
+ for (let i = 1; i < rawSplits.length; i += 2) splits.push(rawSplits[i] + (rawSplits[i + 1] || ""));
31
+ splits = splits.filter(Boolean);
32
+ if (splits.length <= 1) return element;
33
+ return splits.map((split) => {
34
+ return {
35
+ ...element,
36
+ children: [{
37
+ type: "text",
38
+ value: split
39
+ }]
40
+ };
41
+ });
42
+ });
43
+ if (splittedElements.length !== line.children.length) line.children = splittedElements;
44
+ }
45
+ const elements = line.children;
46
+ let start = elements.length - 1;
47
+ if (matchAlgorithm === "v1") start = 0;
48
+ else if (jsx) start = elements.length - 2;
49
+ for (let i = Math.max(start, 0); i < elements.length; i++) {
50
+ const token = elements[i];
51
+ if (token.type !== "element") continue;
52
+ const head = token.children.at(0);
53
+ if (head?.type !== "text") continue;
54
+ const isLast = i === elements.length - 1;
55
+ let match = matchToken(head.value, isLast);
56
+ let additionalTokens;
57
+ if (!match && i > 0 && head.value.trim().startsWith("[!code")) {
58
+ const prevToken = elements[i - 1];
59
+ if (prevToken?.type === "element") {
60
+ const prevHead = prevToken.children.at(0);
61
+ if (prevHead?.type === "text" && prevHead.value.includes("//")) {
62
+ const combinedMatch = matchToken(prevHead.value + head.value, isLast);
63
+ if (combinedMatch) {
64
+ match = combinedMatch;
65
+ out.push({
66
+ info: combinedMatch,
67
+ line,
68
+ token: prevToken,
69
+ isLineCommentOnly: elements.length === 2 && prevToken.children.length === 1 && token.children.length === 1,
70
+ isJsxStyle: false,
71
+ additionalTokens: [token]
72
+ });
73
+ continue;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ if (!match) continue;
79
+ if (jsx && !isLast && i !== 0) {
80
+ const isJsxStyle = isValue(elements[i - 1], "{") && isValue(elements[i + 1], "}");
81
+ out.push({
82
+ info: match,
83
+ line,
84
+ token,
85
+ isLineCommentOnly: elements.length === 3 && token.children.length === 1,
86
+ isJsxStyle,
87
+ additionalTokens
88
+ });
89
+ } else out.push({
90
+ info: match,
91
+ line,
92
+ token,
93
+ isLineCommentOnly: elements.length === 1 && token.children.length === 1,
94
+ isJsxStyle: false,
95
+ additionalTokens
96
+ });
97
+ }
98
+ }
99
+ return out;
114
100
  }
115
101
  function isValue(element, value) {
116
- if (element.type !== "element")
117
- return false;
118
- const text = element.children[0];
119
- if (text.type !== "text")
120
- return false;
121
- return text.value.trim() === value;
122
- }
102
+ if (element.type !== "element") return false;
103
+ const text = element.children[0];
104
+ if (text.type !== "text") return false;
105
+ return text.value.trim() === value;
106
+ }
107
+ /**
108
+ * @param text text value of comment node
109
+ * @param isLast whether the token is located at the end of line
110
+ */
123
111
  function matchToken(text, isLast) {
124
- let trimmed = text.trimStart();
125
- const spaceFront = text.length - trimmed.length;
126
- trimmed = trimmed.trimEnd();
127
- const spaceEnd = text.length - trimmed.length - spaceFront;
128
- for (const [matcher, endOfLine] of matchers) {
129
- if (endOfLine && !isLast)
130
- continue;
131
- const result = matcher.exec(trimmed);
132
- if (!result)
133
- continue;
134
- return [
135
- " ".repeat(spaceFront) + result[1],
136
- result[2],
137
- result[3] ? result[3] + " ".repeat(spaceEnd) : void 0
138
- ];
139
- }
140
- }
112
+ let trimmed = text.trimStart();
113
+ const spaceFront = text.length - trimmed.length;
114
+ trimmed = trimmed.trimEnd();
115
+ const spaceEnd = text.length - trimmed.length - spaceFront;
116
+ for (const [matcher, endOfLine] of matchers) {
117
+ if (endOfLine && !isLast) continue;
118
+ const result = matcher.exec(trimmed);
119
+ if (!result) continue;
120
+ return [
121
+ " ".repeat(spaceFront) + result[1],
122
+ result[2],
123
+ result[3] ? result[3] + " ".repeat(spaceEnd) : void 0
124
+ ];
125
+ }
126
+ }
127
+ /**
128
+ * Remove empty comment prefixes at line end, e.g. `// `
129
+ *
130
+ * For matchAlgorithm v1
131
+ */
141
132
  function v1ClearEndCommentPrefix(text) {
142
- const match = text.match(/(?:\/\/|["'#]|;{1,2}|%{1,2}|--)(\s*)$/);
143
- if (match && match[1].trim().length === 0) {
144
- return text.slice(0, match.index);
145
- }
146
- return text;
133
+ const match = text.match(/(?:\/\/|["'#]|;{1,2}|%{1,2}|--)(\s*)$/);
134
+ if (match && match[1].trim().length === 0) return text.slice(0, match.index);
135
+ return text;
147
136
  }
148
137
 
138
+ //#endregion
139
+ //#region src/shared/notation-transformer.ts
149
140
  function createCommentNotationTransformer(name, regex, onMatch, matchAlgorithm) {
150
- if (matchAlgorithm == null) {
151
- matchAlgorithm = "v3";
152
- }
153
- return {
154
- name,
155
- code(code) {
156
- const lines = code.children.filter((i) => i.type === "element");
157
- const linesToRemove = [];
158
- code.data ??= {};
159
- const data = code.data;
160
- data._shiki_notation ??= parseComments(lines, ["jsx", "tsx"].includes(this.options.lang), matchAlgorithm);
161
- const parsed = data._shiki_notation;
162
- for (const comment of parsed) {
163
- if (comment.info[1].length === 0)
164
- continue;
165
- let lineIdx = lines.indexOf(comment.line);
166
- if (comment.isLineCommentOnly && matchAlgorithm !== "v1")
167
- lineIdx++;
168
- let replaced = false;
169
- comment.info[1] = comment.info[1].replace(regex, (...match) => {
170
- if (onMatch.call(this, match, comment.line, comment.token, lines, lineIdx)) {
171
- replaced = true;
172
- return "";
173
- }
174
- return match[0];
175
- });
176
- if (!replaced)
177
- continue;
178
- if (matchAlgorithm === "v1")
179
- comment.info[1] = v1ClearEndCommentPrefix(comment.info[1]);
180
- const isEmpty = comment.info[1].trim().length === 0;
181
- if (isEmpty)
182
- comment.info[1] = "";
183
- if (isEmpty && comment.isLineCommentOnly) {
184
- linesToRemove.push(comment.line);
185
- } else if (isEmpty && comment.isJsxStyle) {
186
- comment.line.children.splice(comment.line.children.indexOf(comment.token) - 1, 3);
187
- } else if (isEmpty) {
188
- if (comment.additionalTokens) {
189
- for (let j = comment.additionalTokens.length - 1; j >= 0; j--) {
190
- const additionalToken = comment.additionalTokens[j];
191
- const tokenIndex = comment.line.children.indexOf(additionalToken);
192
- if (tokenIndex !== -1) {
193
- comment.line.children.splice(tokenIndex, 1);
194
- }
195
- }
196
- }
197
- comment.line.children.splice(comment.line.children.indexOf(comment.token), 1);
198
- } else {
199
- const head = comment.token.children[0];
200
- if (head.type === "text") {
201
- head.value = comment.info.join("");
202
- if (comment.additionalTokens) {
203
- for (const additionalToken of comment.additionalTokens) {
204
- const additionalHead = additionalToken.children[0];
205
- if (additionalHead?.type === "text") {
206
- additionalHead.value = "";
207
- }
208
- }
209
- }
210
- }
211
- }
212
- }
213
- for (const line of linesToRemove) {
214
- const index = code.children.indexOf(line);
215
- const nextLine = code.children[index + 1];
216
- let removeLength = 1;
217
- if (nextLine?.type === "text" && nextLine?.value === "\n")
218
- removeLength = 2;
219
- code.children.splice(index, removeLength);
220
- }
221
- }
222
- };
141
+ if (matchAlgorithm == null) matchAlgorithm = "v3";
142
+ return {
143
+ name,
144
+ code(code) {
145
+ const lines = code.children.filter((i) => i.type === "element");
146
+ const linesToRemove = [];
147
+ code.data ??= {};
148
+ const data = code.data;
149
+ data._shiki_notation ??= parseComments(lines, ["jsx", "tsx"].includes(this.options.lang), matchAlgorithm);
150
+ const parsed = data._shiki_notation;
151
+ for (const comment of parsed) {
152
+ if (comment.info[1].length === 0) continue;
153
+ let lineIdx = lines.indexOf(comment.line);
154
+ if (comment.isLineCommentOnly && matchAlgorithm !== "v1") lineIdx++;
155
+ let replaced = false;
156
+ comment.info[1] = comment.info[1].replace(regex, (...match) => {
157
+ if (onMatch.call(this, match, comment.line, comment.token, lines, lineIdx)) {
158
+ replaced = true;
159
+ return "";
160
+ }
161
+ return match[0];
162
+ });
163
+ if (!replaced) continue;
164
+ if (matchAlgorithm === "v1") comment.info[1] = v1ClearEndCommentPrefix(comment.info[1]);
165
+ const isEmpty = comment.info[1].trim().length === 0;
166
+ if (isEmpty) comment.info[1] = "";
167
+ if (isEmpty && comment.isLineCommentOnly) linesToRemove.push(comment.line);
168
+ else if (isEmpty && comment.isJsxStyle) comment.line.children.splice(comment.line.children.indexOf(comment.token) - 1, 3);
169
+ else if (isEmpty) {
170
+ if (comment.additionalTokens) for (let j = comment.additionalTokens.length - 1; j >= 0; j--) {
171
+ const additionalToken = comment.additionalTokens[j];
172
+ const tokenIndex = comment.line.children.indexOf(additionalToken);
173
+ if (tokenIndex !== -1) comment.line.children.splice(tokenIndex, 1);
174
+ }
175
+ comment.line.children.splice(comment.line.children.indexOf(comment.token), 1);
176
+ } else {
177
+ const head = comment.token.children[0];
178
+ if (head.type === "text") {
179
+ head.value = comment.info.join("");
180
+ if (comment.additionalTokens) for (const additionalToken of comment.additionalTokens) {
181
+ const additionalHead = additionalToken.children[0];
182
+ if (additionalHead?.type === "text") additionalHead.value = "";
183
+ }
184
+ }
185
+ }
186
+ }
187
+ for (const line of linesToRemove) {
188
+ const index = code.children.indexOf(line);
189
+ const nextLine = code.children[index + 1];
190
+ let removeLength = 1;
191
+ if (nextLine?.type === "text" && nextLine?.value === "\n") removeLength = 2;
192
+ code.children.splice(index, removeLength);
193
+ }
194
+ }
195
+ };
223
196
  }
224
197
 
198
+ //#endregion
199
+ //#region src/transformers/compact-line-options.ts
200
+ /**
201
+ * Transformer for `shiki`'s legacy `lineOptions`
202
+ */
225
203
  function transformerCompactLineOptions(lineOptions = []) {
226
- return {
227
- name: "@shikijs/transformers:compact-line-options",
228
- line(node, line) {
229
- const lineOption = lineOptions.find((o) => o.line === line);
230
- if (lineOption?.classes)
231
- this.addClassToHast(node, lineOption.classes);
232
- return node;
233
- }
234
- };
204
+ return {
205
+ name: "@shikijs/transformers:compact-line-options",
206
+ line(node, line) {
207
+ const lineOption = lineOptions.find((o) => o.line === line);
208
+ if (lineOption?.classes) this.addClassToHast(node, lineOption.classes);
209
+ return node;
210
+ }
211
+ };
235
212
  }
236
213
 
214
+ //#endregion
215
+ //#region src/transformers/meta-highlight.ts
237
216
  function parseMetaHighlightString(meta) {
238
- if (!meta)
239
- return null;
240
- const match = meta.match(/\{([\d,-]+)\}/);
241
- if (!match)
242
- return null;
243
- const lines = match[1].split(",").flatMap((v) => {
244
- const range = v.split("-").map((n) => Number.parseInt(n, 10));
245
- return range.length === 1 ? [range[0]] : Array.from({ length: range[1] - range[0] + 1 }, (_, i) => range[0] + i);
246
- });
247
- return lines;
248
- }
249
- const symbol = /* @__PURE__ */ Symbol("highlighted-lines");
217
+ if (!meta) return null;
218
+ const match = meta.match(/\{([\d,-]+)\}/);
219
+ if (!match) return null;
220
+ return match[1].split(",").flatMap((v) => {
221
+ const range = v.split("-").map((n) => Number.parseInt(n, 10));
222
+ return range.length === 1 ? [range[0]] : Array.from({ length: range[1] - range[0] + 1 }, (_, i) => range[0] + i);
223
+ });
224
+ }
225
+ const symbol = Symbol("highlighted-lines");
226
+ /**
227
+ * Allow using `{1,3-5}` in the code snippet meta to mark highlighted lines.
228
+ */
250
229
  function transformerMetaHighlight(options = {}) {
251
- const { className = "highlighted", zeroIndexed = false } = options;
252
- return {
253
- name: "@shikijs/transformers:meta-highlight",
254
- line(node, lineNumber) {
255
- if (!this.options.meta?.__raw)
256
- return;
257
- const meta = this.meta;
258
- meta[symbol] ??= parseMetaHighlightString(this.options.meta.__raw);
259
- const highlightedLines = meta[symbol] ?? [];
260
- const effectiveLine = zeroIndexed ? lineNumber - 1 : lineNumber;
261
- if (highlightedLines.includes(effectiveLine))
262
- this.addClassToHast(node, className);
263
- return node;
264
- }
265
- };
230
+ const { className = "highlighted", zeroIndexed = false } = options;
231
+ return {
232
+ name: "@shikijs/transformers:meta-highlight",
233
+ line(node, lineNumber) {
234
+ if (!this.options.meta?.__raw) return;
235
+ const meta = this.meta;
236
+ meta[symbol] ??= parseMetaHighlightString(this.options.meta.__raw);
237
+ const highlightedLines = meta[symbol] ?? [];
238
+ const effectiveLine = zeroIndexed ? lineNumber - 1 : lineNumber;
239
+ if (highlightedLines.includes(effectiveLine)) this.addClassToHast(node, className);
240
+ return node;
241
+ }
242
+ };
266
243
  }
267
244
 
245
+ //#endregion
246
+ //#region src/transformers/meta-highlight-word.ts
268
247
  function parseMetaHighlightWords(meta) {
269
- if (!meta)
270
- return [];
271
- const match = Array.from(meta.matchAll(/\/((?:\\.|[^/])+)\//g));
272
- return match.map((v) => v[1].replace(/\\(.)/g, "$1"));
248
+ if (!meta) return [];
249
+ return Array.from(meta.matchAll(/\/((?:\\.|[^/])+)\//g)).map((v) => v[1].replace(/\\(.)/g, "$1"));
273
250
  }
251
+ /**
252
+ * Allow using `/word/` in the code snippet meta to mark highlighted words.
253
+ */
274
254
  function transformerMetaWordHighlight(options = {}) {
275
- const {
276
- className = "highlighted-word"
277
- } = options;
278
- return {
279
- name: "@shikijs/transformers:meta-word-highlight",
280
- preprocess(code, options2) {
281
- if (!this.options.meta?.__raw)
282
- return;
283
- const words = parseMetaHighlightWords(this.options.meta.__raw);
284
- options2.decorations ||= [];
285
- for (const word of words) {
286
- const indexes = findAllSubstringIndexes(code, word);
287
- for (const index of indexes) {
288
- options2.decorations.push({
289
- start: index,
290
- end: index + word.length,
291
- properties: {
292
- class: className
293
- }
294
- });
295
- }
296
- }
297
- }
298
- };
255
+ const { className = "highlighted-word" } = options;
256
+ return {
257
+ name: "@shikijs/transformers:meta-word-highlight",
258
+ preprocess(code, options) {
259
+ if (!this.options.meta?.__raw) return;
260
+ const words = parseMetaHighlightWords(this.options.meta.__raw);
261
+ options.decorations ||= [];
262
+ for (const word of words) {
263
+ const indexes = findAllSubstringIndexes(code, word);
264
+ for (const index of indexes) options.decorations.push({
265
+ start: index,
266
+ end: index + word.length,
267
+ properties: { class: className }
268
+ });
269
+ }
270
+ }
271
+ };
299
272
  }
300
273
  function findAllSubstringIndexes(str, substr) {
301
- const indexes = [];
302
- let cursor = 0;
303
- while (true) {
304
- const index = str.indexOf(substr, cursor);
305
- if (index === -1 || index >= str.length)
306
- break;
307
- if (index < cursor)
308
- break;
309
- indexes.push(index);
310
- cursor = index + substr.length;
311
- }
312
- return indexes;
274
+ const indexes = [];
275
+ let cursor = 0;
276
+ while (true) {
277
+ const index = str.indexOf(substr, cursor);
278
+ if (index === -1 || index >= str.length) break;
279
+ if (index < cursor) break;
280
+ indexes.push(index);
281
+ cursor = index + substr.length;
282
+ }
283
+ return indexes;
313
284
  }
314
285
 
286
+ //#endregion
287
+ //#region src/transformers/notation-map.ts
315
288
  function escapeRegExp(str) {
316
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
289
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
317
290
  }
318
291
  function transformerNotationMap(options = {}, name = "@shikijs/transformers:notation-map") {
319
- const {
320
- classMap = {},
321
- classActivePre = void 0,
322
- classActiveCode = void 0
323
- } = options;
324
- return createCommentNotationTransformer(
325
- name,
326
- new RegExp(`#?\\s*\\[!code (${Object.keys(classMap).map(escapeRegExp).join("|")})(:\\d+)?\\]`, "gi"),
327
- function([_, match, range = ":1"], _line, _comment, lines, index) {
328
- const lineNum = Number.parseInt(range.slice(1), 10);
329
- for (let i = index; i < Math.min(index + lineNum, lines.length); i++) {
330
- this.addClassToHast(lines[i], classMap[match]);
331
- }
332
- if (classActivePre)
333
- this.addClassToHast(this.pre, classActivePre);
334
- if (classActiveCode)
335
- this.addClassToHast(this.code, classActiveCode);
336
- return true;
337
- },
338
- options.matchAlgorithm
339
- );
292
+ const { classMap = {}, classActivePre = void 0, classActiveCode = void 0 } = options;
293
+ return createCommentNotationTransformer(name, new RegExp(`#?\\s*\\[!code (${Object.keys(classMap).map(escapeRegExp).join("|")})(:\\d+)?\\]`, "gi"), function([_, match, range = ":1"], _line, _comment, lines, index) {
294
+ const lineNum = Number.parseInt(range.slice(1), 10);
295
+ for (let i = index; i < Math.min(index + lineNum, lines.length); i++) this.addClassToHast(lines[i], classMap[match]);
296
+ if (classActivePre) this.addClassToHast(this.pre, classActivePre);
297
+ if (classActiveCode) this.addClassToHast(this.code, classActiveCode);
298
+ return true;
299
+ }, options.matchAlgorithm);
340
300
  }
341
301
 
302
+ //#endregion
303
+ //#region src/transformers/notation-diff.ts
304
+ /**
305
+ * Use `[!code ++]` and `[!code --]` to mark added and removed lines.
306
+ */
342
307
  function transformerNotationDiff(options = {}) {
343
- const {
344
- classLineAdd = "diff add",
345
- classLineRemove = "diff remove",
346
- classActivePre = "has-diff",
347
- classActiveCode
348
- } = options;
349
- return transformerNotationMap(
350
- {
351
- classMap: {
352
- "++": classLineAdd,
353
- "--": classLineRemove
354
- },
355
- classActivePre,
356
- classActiveCode,
357
- matchAlgorithm: options.matchAlgorithm
358
- },
359
- "@shikijs/transformers:notation-diff"
360
- );
308
+ const { classLineAdd = "diff add", classLineRemove = "diff remove", classActivePre = "has-diff", classActiveCode } = options;
309
+ return transformerNotationMap({
310
+ classMap: {
311
+ "++": classLineAdd,
312
+ "--": classLineRemove
313
+ },
314
+ classActivePre,
315
+ classActiveCode,
316
+ matchAlgorithm: options.matchAlgorithm
317
+ }, "@shikijs/transformers:notation-diff");
361
318
  }
362
319
 
320
+ //#endregion
321
+ //#region src/transformers/notation-error-level.ts
322
+ /**
323
+ * Allow using `[!code error]` `[!code warning]` notation in code to mark highlighted lines.
324
+ */
363
325
  function transformerNotationErrorLevel(options = {}) {
364
- const {
365
- classMap = {
366
- error: ["highlighted", "error"],
367
- warning: ["highlighted", "warning"]
368
- },
369
- classActivePre = "has-highlighted",
370
- classActiveCode
371
- } = options;
372
- return transformerNotationMap(
373
- {
374
- classMap,
375
- classActivePre,
376
- classActiveCode,
377
- matchAlgorithm: options.matchAlgorithm
378
- },
379
- "@shikijs/transformers:notation-error-level"
380
- );
326
+ const { classMap = {
327
+ error: ["highlighted", "error"],
328
+ warning: ["highlighted", "warning"],
329
+ info: ["highlighted", "info"]
330
+ }, classActivePre = "has-highlighted", classActiveCode } = options;
331
+ return transformerNotationMap({
332
+ classMap,
333
+ classActivePre,
334
+ classActiveCode,
335
+ matchAlgorithm: options.matchAlgorithm
336
+ }, "@shikijs/transformers:notation-error-level");
381
337
  }
382
338
 
339
+ //#endregion
340
+ //#region src/transformers/notation-focus.ts
341
+ /**
342
+ * Allow using `[!code focus]` notation in code to mark focused lines.
343
+ */
383
344
  function transformerNotationFocus(options = {}) {
384
- const {
385
- classActiveLine = "focused",
386
- classActivePre = "has-focused",
387
- classActiveCode
388
- } = options;
389
- return transformerNotationMap(
390
- {
391
- classMap: {
392
- focus: classActiveLine
393
- },
394
- classActivePre,
395
- classActiveCode,
396
- matchAlgorithm: options.matchAlgorithm
397
- },
398
- "@shikijs/transformers:notation-focus"
399
- );
345
+ const { classActiveLine = "focused", classActivePre = "has-focused", classActiveCode } = options;
346
+ return transformerNotationMap({
347
+ classMap: { focus: classActiveLine },
348
+ classActivePre,
349
+ classActiveCode,
350
+ matchAlgorithm: options.matchAlgorithm
351
+ }, "@shikijs/transformers:notation-focus");
400
352
  }
401
353
 
354
+ //#endregion
355
+ //#region src/transformers/notation-highlight.ts
356
+ /**
357
+ * Allow using `[!code highlight]` notation in code to mark highlighted lines.
358
+ */
402
359
  function transformerNotationHighlight(options = {}) {
403
- const {
404
- classActiveLine = "highlighted",
405
- classActivePre = "has-highlighted",
406
- classActiveCode
407
- } = options;
408
- return transformerNotationMap(
409
- {
410
- classMap: {
411
- highlight: classActiveLine,
412
- hl: classActiveLine
413
- },
414
- classActivePre,
415
- classActiveCode,
416
- matchAlgorithm: options.matchAlgorithm
417
- },
418
- "@shikijs/transformers:notation-highlight"
419
- );
360
+ const { classActiveLine = "highlighted", classActivePre = "has-highlighted", classActiveCode } = options;
361
+ return transformerNotationMap({
362
+ classMap: {
363
+ highlight: classActiveLine,
364
+ hl: classActiveLine
365
+ },
366
+ classActivePre,
367
+ classActiveCode,
368
+ matchAlgorithm: options.matchAlgorithm
369
+ }, "@shikijs/transformers:notation-highlight");
420
370
  }
421
371
 
372
+ //#endregion
373
+ //#region src/shared/highlight-word.ts
422
374
  function highlightWordInLine(line, ignoredElement, word, className) {
423
- const content = getTextContent(line);
424
- let index = content.indexOf(word);
425
- while (index !== -1) {
426
- highlightRange.call(this, line.children, ignoredElement, index, word.length, className);
427
- index = content.indexOf(word, index + 1);
428
- }
375
+ const content = getTextContent(line);
376
+ let index = content.indexOf(word);
377
+ while (index !== -1) {
378
+ highlightRange.call(this, line.children, ignoredElement, index, word.length, className);
379
+ index = content.indexOf(word, index + 1);
380
+ }
429
381
  }
430
382
  function getTextContent(element) {
431
- if (element.type === "text")
432
- return element.value;
433
- if (element.type === "element" && element.tagName === "span")
434
- return element.children.map(getTextContent).join("");
435
- return "";
436
- }
383
+ if (element.type === "text") return element.value;
384
+ if (element.type === "element" && element.tagName === "span") return element.children.map(getTextContent).join("");
385
+ return "";
386
+ }
387
+ /**
388
+ * @param elements
389
+ * @param ignoredElement
390
+ * @param index highlight beginning index
391
+ * @param len highlight length
392
+ * @param className class name to add to highlighted nodes
393
+ */
437
394
  function highlightRange(elements, ignoredElement, index, len, className) {
438
- let currentIdx = 0;
439
- for (let i = 0; i < elements.length; i++) {
440
- const element = elements[i];
441
- if (element.type !== "element" || element.tagName !== "span" || element === ignoredElement)
442
- continue;
443
- const textNode = element.children[0];
444
- if (textNode.type !== "text")
445
- continue;
446
- if (hasOverlap([currentIdx, currentIdx + textNode.value.length - 1], [index, index + len])) {
447
- const start = Math.max(0, index - currentIdx);
448
- const length = len - Math.max(0, currentIdx - index);
449
- if (length === 0)
450
- continue;
451
- const separated = separateToken(element, textNode, start, length);
452
- this.addClassToHast(separated[1], className);
453
- const output = separated.filter(Boolean);
454
- elements.splice(i, 1, ...output);
455
- i += output.length - 1;
456
- }
457
- currentIdx += textNode.value.length;
458
- }
395
+ let currentIdx = 0;
396
+ for (let i = 0; i < elements.length; i++) {
397
+ const element = elements[i];
398
+ if (element.type !== "element" || element.tagName !== "span" || element === ignoredElement) continue;
399
+ const textNode = element.children[0];
400
+ if (textNode.type !== "text") continue;
401
+ if (hasOverlap([currentIdx, currentIdx + textNode.value.length - 1], [index, index + len])) {
402
+ const start = Math.max(0, index - currentIdx);
403
+ const length = len - Math.max(0, currentIdx - index);
404
+ if (length === 0) continue;
405
+ const separated = separateToken(element, textNode, start, length);
406
+ this.addClassToHast(separated[1], className);
407
+ const output = separated.filter(Boolean);
408
+ elements.splice(i, 1, ...output);
409
+ i += output.length - 1;
410
+ }
411
+ currentIdx += textNode.value.length;
412
+ }
459
413
  }
460
414
  function hasOverlap(range1, range2) {
461
- return range1[0] <= range2[1] && range1[1] >= range2[0];
415
+ return range1[0] <= range2[1] && range1[1] >= range2[0];
462
416
  }
463
417
  function separateToken(span, textNode, index, len) {
464
- const text = textNode.value;
465
- const createNode = (value) => inheritElement(span, {
466
- children: [
467
- {
468
- type: "text",
469
- value
470
- }
471
- ]
472
- });
473
- return [
474
- index > 0 ? createNode(text.slice(0, index)) : void 0,
475
- createNode(text.slice(index, index + len)),
476
- index + len < text.length ? createNode(text.slice(index + len)) : void 0
477
- ];
418
+ const text = textNode.value;
419
+ const createNode = (value) => inheritElement(span, { children: [{
420
+ type: "text",
421
+ value
422
+ }] });
423
+ return [
424
+ index > 0 ? createNode(text.slice(0, index)) : void 0,
425
+ createNode(text.slice(index, index + len)),
426
+ index + len < text.length ? createNode(text.slice(index + len)) : void 0
427
+ ];
478
428
  }
479
429
  function inheritElement(original, overrides) {
480
- return {
481
- ...original,
482
- properties: {
483
- ...original.properties
484
- },
485
- ...overrides
486
- };
430
+ return {
431
+ ...original,
432
+ properties: { ...original.properties },
433
+ ...overrides
434
+ };
487
435
  }
488
436
 
437
+ //#endregion
438
+ //#region src/transformers/notation-highlight-word.ts
489
439
  function transformerNotationWordHighlight(options = {}) {
490
- const {
491
- classActiveWord = "highlighted-word",
492
- classActivePre = void 0
493
- } = options;
494
- return createCommentNotationTransformer(
495
- "@shikijs/transformers:notation-highlight-word",
496
- /\s*\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/,
497
- function([_, word, range], _line, comment, lines, index) {
498
- const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length;
499
- word = word.replace(/\\(.)/g, "$1");
500
- for (let i = index; i < Math.min(index + lineNum, lines.length); i++) {
501
- highlightWordInLine.call(this, lines[i], comment, word, classActiveWord);
502
- }
503
- if (classActivePre)
504
- this.addClassToHast(this.pre, classActivePre);
505
- return true;
506
- },
507
- options.matchAlgorithm
508
- );
440
+ const { classActiveWord = "highlighted-word", classActivePre = void 0 } = options;
441
+ return createCommentNotationTransformer("@shikijs/transformers:notation-highlight-word", /\s*\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/, function([_, word, range], _line, comment, lines, index) {
442
+ const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length;
443
+ word = word.replace(/\\(.)/g, "$1");
444
+ for (let i = index; i < Math.min(index + lineNum, lines.length); i++) highlightWordInLine.call(this, lines[i], comment, word, classActiveWord);
445
+ if (classActivePre) this.addClassToHast(this.pre, classActivePre);
446
+ return true;
447
+ }, options.matchAlgorithm);
509
448
  }
510
449
 
450
+ //#endregion
451
+ //#region src/transformers/remove-comments.ts
452
+ /**
453
+ * Remove comments from the code.
454
+ */
511
455
  function transformerRemoveComments(options = {}) {
512
- const { removeEmptyLines = true } = options;
513
- return {
514
- name: "@shikijs/transformers:remove-comments",
515
- preprocess(_code, options2) {
516
- if (options2.includeExplanation !== true && options2.includeExplanation !== "scopeName")
517
- throw new Error("`transformerRemoveComments` requires `includeExplanation` to be set to `true` or `'scopeName'`");
518
- },
519
- tokens(tokens) {
520
- const result = [];
521
- for (const line of tokens) {
522
- const filteredLine = [];
523
- let hasComment = false;
524
- for (const token of line) {
525
- const isComment = token.explanation?.some(
526
- (exp) => exp.scopes.some((s) => s.scopeName.startsWith("comment"))
527
- );
528
- if (isComment) {
529
- hasComment = true;
530
- } else {
531
- filteredLine.push(token);
532
- }
533
- }
534
- if (removeEmptyLines && hasComment) {
535
- const isAllWhitespace = filteredLine.every((token) => !token.content.trim());
536
- if (isAllWhitespace)
537
- continue;
538
- }
539
- result.push(filteredLine);
540
- }
541
- return result;
542
- }
543
- };
456
+ const { removeEmptyLines = true } = options;
457
+ return {
458
+ name: "@shikijs/transformers:remove-comments",
459
+ preprocess(_code, options) {
460
+ if (options.includeExplanation !== true && options.includeExplanation !== "scopeName") throw new Error("`transformerRemoveComments` requires `includeExplanation` to be set to `true` or `'scopeName'`");
461
+ },
462
+ tokens(tokens) {
463
+ const result = [];
464
+ for (const line of tokens) {
465
+ const filteredLine = [];
466
+ let hasComment = false;
467
+ for (const token of line) if (token.explanation?.some((exp) => exp.scopes.some((s) => s.scopeName.startsWith("comment")))) hasComment = true;
468
+ else filteredLine.push(token);
469
+ if (removeEmptyLines && hasComment) {
470
+ if (filteredLine.every((token) => !token.content.trim())) continue;
471
+ }
472
+ result.push(filteredLine);
473
+ }
474
+ return result;
475
+ }
476
+ };
544
477
  }
545
478
 
479
+ //#endregion
480
+ //#region src/transformers/remove-line-breaks.ts
481
+ /**
482
+ * Remove line breaks between lines.
483
+ * Useful when you override `display: block` to `.line` in CSS.
484
+ */
546
485
  function transformerRemoveLineBreak() {
547
- return {
548
- name: "@shikijs/transformers:remove-line-break",
549
- code(code) {
550
- code.children = code.children.filter((line) => !(line.type === "text" && line.value === "\n"));
551
- }
552
- };
486
+ return {
487
+ name: "@shikijs/transformers:remove-line-break",
488
+ code(code) {
489
+ code.children = code.children.filter((line) => !(line.type === "text" && line.value === "\n"));
490
+ }
491
+ };
553
492
  }
554
493
 
494
+ //#endregion
495
+ //#region src/transformers/remove-notation-escape.ts
496
+ /**
497
+ * Remove notation escapes.
498
+ * Useful when you want to write `// [!code` in markdown.
499
+ * If you process `// [\!code ...]` expression, you can get `// [!code ...]` in the output.
500
+ */
555
501
  function transformerRemoveNotationEscape() {
556
- return {
557
- name: "@shikijs/transformers:remove-notation-escape",
558
- code(hast) {
559
- function replace(node) {
560
- if (node.type === "text") {
561
- node.value = node.value.replace("[\\!code", "[!code");
562
- } else if ("children" in node) {
563
- for (const child of node.children) {
564
- replace(child);
565
- }
566
- }
567
- }
568
- replace(hast);
569
- return hast;
570
- }
571
- };
502
+ return {
503
+ name: "@shikijs/transformers:remove-notation-escape",
504
+ code(hast) {
505
+ function replace(node) {
506
+ if (node.type === "text") node.value = node.value.replace("[\\!code", "[!code");
507
+ else if ("children" in node) for (const child of node.children) replace(child);
508
+ }
509
+ replace(hast);
510
+ return hast;
511
+ }
512
+ };
572
513
  }
573
514
 
515
+ //#endregion
516
+ //#region src/transformers/render-indent-guides.ts
517
+ /**
518
+ * Render indentations as separate tokens.
519
+ * Apply with CSS, it can be used to render indent guides visually.
520
+ */
574
521
  function transformerRenderIndentGuides(options = {}) {
575
- return {
576
- name: "@shikijs/transformers:render-indent-guides",
577
- code(hast) {
578
- const indent = Number(
579
- this.options.meta?.indent ?? this.options.meta?.__raw?.match(/\{indent:(\d+|false)\}/)?.[1] ?? options.indent ?? 2
580
- );
581
- if (Number.isNaN(indent) || indent <= 0) {
582
- return hast;
583
- }
584
- const indentRegex = new RegExp(` {${indent}}| {0,${indent - 1}} | {1,}$`, "g");
585
- const emptyLines = [];
586
- let level = 0;
587
- for (const line of hast.children) {
588
- if (line.type !== "element") {
589
- continue;
590
- }
591
- const first = line.children[0];
592
- if (first?.type !== "element" || first?.children[0]?.type !== "text") {
593
- emptyLines.push([line, level]);
594
- continue;
595
- }
596
- const text = first.children[0];
597
- const blanks = text.value.split(/[^ \t]/, 1)[0];
598
- const ranges = [];
599
- for (const match of blanks.matchAll(indentRegex)) {
600
- const start = match.index;
601
- const end = start + match[0].length;
602
- ranges.push([start, end]);
603
- }
604
- for (const [line2, level2] of emptyLines) {
605
- line2.children.unshift(...Array.from({ length: Math.min(ranges.length, level2 + 1) }, (_, i) => ({
606
- type: "element",
607
- tagName: "span",
608
- properties: {
609
- class: "indent",
610
- style: `--indent-offset: ${i * indent}ch;`
611
- },
612
- children: []
613
- })));
614
- }
615
- emptyLines.length = 0;
616
- level = ranges.length;
617
- if (ranges.length) {
618
- line.children.unshift(
619
- ...ranges.map(([start, end]) => ({
620
- type: "element",
621
- tagName: "span",
622
- properties: {
623
- class: "indent"
624
- },
625
- children: [{
626
- type: "text",
627
- value: text.value.slice(start, end)
628
- }]
629
- }))
630
- );
631
- text.value = text.value.slice(ranges.at(-1)[1]);
632
- }
633
- }
634
- return hast;
635
- }
636
- };
522
+ return {
523
+ name: "@shikijs/transformers:render-indent-guides",
524
+ code(hast) {
525
+ const indent = Number(this.options.meta?.indent ?? this.options.meta?.__raw?.match(/\{indent:(\d+|false)\}/)?.[1] ?? options.indent ?? 2);
526
+ if (Number.isNaN(indent) || indent <= 0) return hast;
527
+ const indentRegex = new RegExp(` {${indent}}| {0,${indent - 1}}\t| {1,}$`, "g");
528
+ const emptyLines = [];
529
+ let level = 0;
530
+ for (const line of hast.children) {
531
+ if (line.type !== "element") continue;
532
+ const first = line.children[0];
533
+ if (first?.type !== "element" || first?.children[0]?.type !== "text") {
534
+ emptyLines.push([line, level]);
535
+ continue;
536
+ }
537
+ const text = first.children[0];
538
+ const blanks = text.value.split(/[^ \t]/, 1)[0];
539
+ const ranges = [];
540
+ for (const match of blanks.matchAll(indentRegex)) {
541
+ const start = match.index;
542
+ const end = start + match[0].length;
543
+ ranges.push([start, end]);
544
+ }
545
+ for (const [line, level] of emptyLines) line.children.unshift(...Array.from({ length: Math.min(ranges.length, level + 1) }, (_, i) => ({
546
+ type: "element",
547
+ tagName: "span",
548
+ properties: {
549
+ class: "indent",
550
+ style: `--indent-offset: ${i * indent}ch;`
551
+ },
552
+ children: []
553
+ })));
554
+ emptyLines.length = 0;
555
+ level = ranges.length;
556
+ if (ranges.length) {
557
+ line.children.unshift(...ranges.map(([start, end]) => ({
558
+ type: "element",
559
+ tagName: "span",
560
+ properties: { class: "indent" },
561
+ children: [{
562
+ type: "text",
563
+ value: text.value.slice(start, end)
564
+ }]
565
+ })));
566
+ text.value = text.value.slice(ranges.at(-1)[1]);
567
+ }
568
+ }
569
+ return hast;
570
+ }
571
+ };
637
572
  }
638
573
 
574
+ //#endregion
575
+ //#region src/shared/utils.ts
639
576
  function isTab(part) {
640
- return part === " ";
577
+ return part === " ";
641
578
  }
642
579
  function isSpace(part) {
643
- return part === " " || part === " ";
580
+ return part === " " || part === " ";
644
581
  }
645
582
  function separateContinuousSpaces(inputs) {
646
- const result = [];
647
- let current = "";
648
- function bump() {
649
- if (current.length)
650
- result.push(current);
651
- current = "";
652
- }
653
- inputs.forEach((part, idx) => {
654
- if (isTab(part)) {
655
- bump();
656
- result.push(part);
657
- } else if (isSpace(part) && (isSpace(inputs[idx - 1]) || isSpace(inputs[idx + 1]))) {
658
- bump();
659
- result.push(part);
660
- } else {
661
- current += part;
662
- }
663
- });
664
- bump();
665
- return result;
583
+ const result = [];
584
+ let current = "";
585
+ function bump() {
586
+ if (current.length) result.push(current);
587
+ current = "";
588
+ }
589
+ inputs.forEach((part, idx) => {
590
+ if (isTab(part)) {
591
+ bump();
592
+ result.push(part);
593
+ } else if (isSpace(part) && (isSpace(inputs[idx - 1]) || isSpace(inputs[idx + 1]))) {
594
+ bump();
595
+ result.push(part);
596
+ } else current += part;
597
+ });
598
+ bump();
599
+ return result;
666
600
  }
667
601
  function splitSpaces(parts, type, renderContinuousSpaces = true) {
668
- if (type === "all")
669
- return parts;
670
- let leftCount = 0;
671
- let rightCount = 0;
672
- if (type === "boundary") {
673
- for (let i = 0; i < parts.length; i++) {
674
- if (isSpace(parts[i]))
675
- leftCount++;
676
- else
677
- break;
678
- }
679
- }
680
- if (type === "boundary" || type === "trailing") {
681
- for (let i = parts.length - 1; i >= 0; i--) {
682
- if (isSpace(parts[i]))
683
- rightCount++;
684
- else
685
- break;
686
- }
687
- }
688
- const middle = parts.slice(leftCount, parts.length - rightCount);
689
- return [
690
- ...parts.slice(0, leftCount),
691
- ...renderContinuousSpaces ? separateContinuousSpaces(middle) : [middle.join("")],
692
- ...parts.slice(parts.length - rightCount)
693
- ];
602
+ if (type === "all") return parts;
603
+ let leftCount = 0;
604
+ let rightCount = 0;
605
+ if (type === "boundary" || type === "leading") for (let i = 0; i < parts.length; i++) if (isSpace(parts[i])) leftCount++;
606
+ else break;
607
+ if (type === "boundary" || type === "trailing") for (let i = parts.length - 1; i >= 0; i--) if (isSpace(parts[i])) rightCount++;
608
+ else break;
609
+ const middle = parts.slice(leftCount, parts.length - rightCount);
610
+ return [
611
+ ...parts.slice(0, leftCount),
612
+ ...renderContinuousSpaces ? separateContinuousSpaces(middle) : [middle.join("")],
613
+ ...parts.slice(parts.length - rightCount)
614
+ ];
694
615
  }
695
616
 
617
+ //#endregion
618
+ //#region src/transformers/render-whitespace.ts
619
+ /**
620
+ * Render whitespaces as separate tokens.
621
+ * Apply with CSS, it can be used to render tabs and spaces visually.
622
+ */
696
623
  function transformerRenderWhitespace(options = {}) {
697
- const classMap = {
698
- " ": options.classSpace ?? "space",
699
- " ": options.classTab ?? "tab"
700
- };
701
- const position = options.position ?? "all";
702
- const keys = Object.keys(classMap);
703
- return {
704
- name: "@shikijs/transformers:render-whitespace",
705
- // We use `root` hook here to ensure it runs after all other transformers
706
- root(root) {
707
- const pre = root.children[0];
708
- const code = pre.tagName === "pre" ? pre.children[0] : { children: [root] };
709
- code.children.forEach(
710
- (line) => {
711
- if (line.type !== "element" && line.type !== "root")
712
- return;
713
- const elements = line.children.filter((token) => token.type === "element");
714
- const last = elements.length - 1;
715
- line.children = line.children.flatMap((token) => {
716
- if (token.type !== "element")
717
- return token;
718
- const index = elements.indexOf(token);
719
- if (position === "boundary" && index !== 0 && index !== last)
720
- return token;
721
- if (position === "trailing" && index !== last)
722
- return token;
723
- const node = token.children[0];
724
- if (node.type !== "text" || !node.value)
725
- return token;
726
- const parts = splitSpaces(
727
- node.value.split(/([ \t])/).filter((i) => i.length),
728
- position === "boundary" && index === last && last !== 0 ? "trailing" : position,
729
- position !== "trailing"
730
- );
731
- if (parts.length <= 1)
732
- return token;
733
- return parts.map((part) => {
734
- const clone = {
735
- ...token,
736
- properties: { ...token.properties }
737
- };
738
- clone.children = [{ type: "text", value: part }];
739
- if (keys.includes(part)) {
740
- this.addClassToHast(clone, classMap[part]);
741
- delete clone.properties.style;
742
- }
743
- return clone;
744
- });
745
- });
746
- }
747
- );
748
- }
749
- };
624
+ const classMap = {
625
+ " ": options.classSpace ?? "space",
626
+ " ": options.classTab ?? "tab"
627
+ };
628
+ const position = options.position ?? "all";
629
+ const keys = Object.keys(classMap);
630
+ return {
631
+ name: "@shikijs/transformers:render-whitespace",
632
+ root(root) {
633
+ const pre = root.children[0];
634
+ (pre.tagName === "pre" ? pre.children[0] : { children: [root] }).children.forEach((line) => {
635
+ if (line.type !== "element" && line.type !== "root") return;
636
+ const elements = line.children.filter((token) => token.type === "element");
637
+ const last = elements.length - 1;
638
+ line.children = line.children.flatMap((token) => {
639
+ if (token.type !== "element") return token;
640
+ const index = elements.indexOf(token);
641
+ if (position === "boundary" && index !== 0 && index !== last) return token;
642
+ if (position === "trailing" && index !== last) return token;
643
+ if (position === "leading" && index !== 0) return token;
644
+ const node = token.children[0];
645
+ if (node.type !== "text" || !node.value) return token;
646
+ const parts = splitSpaces(node.value.split(/([ \t])/).filter((i) => i.length), position === "boundary" && index === last && last !== 0 ? "trailing" : position, position !== "trailing" && position !== "leading");
647
+ if (parts.length <= 1) return token;
648
+ return parts.map((part) => {
649
+ const clone = {
650
+ ...token,
651
+ properties: { ...token.properties }
652
+ };
653
+ clone.children = [{
654
+ type: "text",
655
+ value: part
656
+ }];
657
+ if (keys.includes(part)) {
658
+ this.addClassToHast(clone, classMap[part]);
659
+ delete clone.properties.style;
660
+ }
661
+ return clone;
662
+ });
663
+ });
664
+ });
665
+ }
666
+ };
750
667
  }
751
668
 
669
+ //#endregion
670
+ //#region src/transformers/style-to-class.ts
671
+ /**
672
+ * Remove line breaks between lines.
673
+ * Useful when you override `display: block` to `.line` in CSS.
674
+ */
752
675
  function transformerStyleToClass(options = {}) {
753
- const {
754
- classPrefix = "__shiki_",
755
- classSuffix = "",
756
- classReplacer = (className) => className
757
- } = options;
758
- const classToStyle = /* @__PURE__ */ new Map();
759
- function stringifyStyle(style) {
760
- return Object.entries(style).map(([key, value]) => `${key}:${value}`).join(";");
761
- }
762
- function registerStyle(style) {
763
- const str = typeof style === "string" ? style : stringifyStyle(style);
764
- let className = classPrefix + cyrb53(str) + classSuffix;
765
- className = classReplacer(className);
766
- if (!classToStyle.has(className)) {
767
- classToStyle.set(
768
- className,
769
- typeof style === "string" ? style : { ...style }
770
- );
771
- }
772
- return className;
773
- }
774
- return {
775
- name: "@shikijs/transformers:style-to-class",
776
- pre(t) {
777
- if (!t.properties.style)
778
- return;
779
- const className = registerStyle(t.properties.style);
780
- delete t.properties.style;
781
- this.addClassToHast(t, className);
782
- },
783
- tokens(lines) {
784
- for (const line of lines) {
785
- for (const token of line) {
786
- if (!token.htmlStyle)
787
- continue;
788
- const className = registerStyle(token.htmlStyle);
789
- token.htmlStyle = {};
790
- token.htmlAttrs ||= {};
791
- if (!token.htmlAttrs.class)
792
- token.htmlAttrs.class = className;
793
- else
794
- token.htmlAttrs.class += ` ${className}`;
795
- }
796
- }
797
- },
798
- getClassRegistry() {
799
- return classToStyle;
800
- },
801
- getCSS() {
802
- let css = "";
803
- for (const [className, style] of classToStyle.entries()) {
804
- css += `.${className}{${typeof style === "string" ? style : stringifyStyle(style)}}`;
805
- }
806
- return css;
807
- },
808
- clearRegistry() {
809
- classToStyle.clear();
810
- }
811
- };
812
- }
676
+ const { classPrefix = "__shiki_", classSuffix = "", classReplacer = (className) => className } = options;
677
+ const classToStyle = /* @__PURE__ */ new Map();
678
+ function stringifyStyle(style) {
679
+ return Object.entries(style).map(([key, value]) => `${key}:${value}`).join(";");
680
+ }
681
+ function registerStyle(style) {
682
+ let className = classPrefix + cyrb53(typeof style === "string" ? style : stringifyStyle(style)) + classSuffix;
683
+ className = classReplacer(className);
684
+ if (!classToStyle.has(className)) classToStyle.set(className, typeof style === "string" ? style : { ...style });
685
+ return className;
686
+ }
687
+ return {
688
+ name: "@shikijs/transformers:style-to-class",
689
+ pre(t) {
690
+ if (!t.properties.style) return;
691
+ const className = registerStyle(t.properties.style);
692
+ delete t.properties.style;
693
+ this.addClassToHast(t, className);
694
+ },
695
+ tokens(lines) {
696
+ for (const line of lines) for (const token of line) {
697
+ if (!token.htmlStyle) continue;
698
+ const className = registerStyle(token.htmlStyle);
699
+ token.htmlStyle = {};
700
+ token.htmlAttrs ||= {};
701
+ if (!token.htmlAttrs.class) token.htmlAttrs.class = className;
702
+ else token.htmlAttrs.class += ` ${className}`;
703
+ }
704
+ },
705
+ getClassRegistry() {
706
+ return classToStyle;
707
+ },
708
+ getCSS() {
709
+ let css = "";
710
+ for (const [className, style] of classToStyle.entries()) css += `.${className}{${typeof style === "string" ? style : stringifyStyle(style)}}`;
711
+ return css;
712
+ },
713
+ clearRegistry() {
714
+ classToStyle.clear();
715
+ }
716
+ };
717
+ }
718
+ /**
719
+ * A simple hash function.
720
+ *
721
+ * @see https://stackoverflow.com/a/52171480
722
+ */
813
723
  function cyrb53(str, seed = 0) {
814
- let h1 = 3735928559 ^ seed;
815
- let h2 = 1103547991 ^ seed;
816
- for (let i = 0, ch; i < str.length; i++) {
817
- ch = str.charCodeAt(i);
818
- h1 = Math.imul(h1 ^ ch, 2654435761);
819
- h2 = Math.imul(h2 ^ ch, 1597334677);
820
- }
821
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
822
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
823
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
824
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
825
- return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).slice(0, 6);
724
+ let h1 = 3735928559 ^ seed;
725
+ let h2 = 1103547991 ^ seed;
726
+ for (let i = 0, ch; i < str.length; i++) {
727
+ ch = str.charCodeAt(i);
728
+ h1 = Math.imul(h1 ^ ch, 2654435761);
729
+ h2 = Math.imul(h2 ^ ch, 1597334677);
730
+ }
731
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
732
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
733
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
734
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
735
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36).slice(0, 6);
826
736
  }
827
737
 
828
- export { createCommentNotationTransformer, findAllSubstringIndexes, parseMetaHighlightString, parseMetaHighlightWords, transformerCompactLineOptions, transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationMap, transformerNotationWordHighlight, transformerRemoveComments, transformerRemoveLineBreak, transformerRemoveNotationEscape, transformerRenderIndentGuides, transformerRenderWhitespace, transformerStyleToClass };
738
+ //#endregion
739
+ export { createCommentNotationTransformer, findAllSubstringIndexes, parseMetaHighlightString, parseMetaHighlightWords, transformerCompactLineOptions, transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationMap, transformerNotationWordHighlight, transformerRemoveComments, transformerRemoveLineBreak, transformerRemoveNotationEscape, transformerRenderIndentGuides, transformerRenderWhitespace, transformerStyleToClass };