@shikijs/transformers 3.23.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 -745
  3. package/package.json +8 -5
package/dist/index.mjs CHANGED
@@ -1,831 +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
- info: ["highlighted", "info"]
369
- },
370
- classActivePre = "has-highlighted",
371
- classActiveCode
372
- } = options;
373
- return transformerNotationMap(
374
- {
375
- classMap,
376
- classActivePre,
377
- classActiveCode,
378
- matchAlgorithm: options.matchAlgorithm
379
- },
380
- "@shikijs/transformers:notation-error-level"
381
- );
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");
382
337
  }
383
338
 
339
+ //#endregion
340
+ //#region src/transformers/notation-focus.ts
341
+ /**
342
+ * Allow using `[!code focus]` notation in code to mark focused lines.
343
+ */
384
344
  function transformerNotationFocus(options = {}) {
385
- const {
386
- classActiveLine = "focused",
387
- classActivePre = "has-focused",
388
- classActiveCode
389
- } = options;
390
- return transformerNotationMap(
391
- {
392
- classMap: {
393
- focus: classActiveLine
394
- },
395
- classActivePre,
396
- classActiveCode,
397
- matchAlgorithm: options.matchAlgorithm
398
- },
399
- "@shikijs/transformers:notation-focus"
400
- );
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");
401
352
  }
402
353
 
354
+ //#endregion
355
+ //#region src/transformers/notation-highlight.ts
356
+ /**
357
+ * Allow using `[!code highlight]` notation in code to mark highlighted lines.
358
+ */
403
359
  function transformerNotationHighlight(options = {}) {
404
- const {
405
- classActiveLine = "highlighted",
406
- classActivePre = "has-highlighted",
407
- classActiveCode
408
- } = options;
409
- return transformerNotationMap(
410
- {
411
- classMap: {
412
- highlight: classActiveLine,
413
- hl: classActiveLine
414
- },
415
- classActivePre,
416
- classActiveCode,
417
- matchAlgorithm: options.matchAlgorithm
418
- },
419
- "@shikijs/transformers:notation-highlight"
420
- );
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");
421
370
  }
422
371
 
372
+ //#endregion
373
+ //#region src/shared/highlight-word.ts
423
374
  function highlightWordInLine(line, ignoredElement, word, className) {
424
- const content = getTextContent(line);
425
- let index = content.indexOf(word);
426
- while (index !== -1) {
427
- highlightRange.call(this, line.children, ignoredElement, index, word.length, className);
428
- index = content.indexOf(word, index + 1);
429
- }
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
+ }
430
381
  }
431
382
  function getTextContent(element) {
432
- if (element.type === "text")
433
- return element.value;
434
- if (element.type === "element" && element.tagName === "span")
435
- return element.children.map(getTextContent).join("");
436
- return "";
437
- }
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
+ */
438
394
  function highlightRange(elements, ignoredElement, index, len, className) {
439
- let currentIdx = 0;
440
- for (let i = 0; i < elements.length; i++) {
441
- const element = elements[i];
442
- if (element.type !== "element" || element.tagName !== "span" || element === ignoredElement)
443
- continue;
444
- const textNode = element.children[0];
445
- if (textNode.type !== "text")
446
- continue;
447
- if (hasOverlap([currentIdx, currentIdx + textNode.value.length - 1], [index, index + len])) {
448
- const start = Math.max(0, index - currentIdx);
449
- const length = len - Math.max(0, currentIdx - index);
450
- if (length === 0)
451
- continue;
452
- const separated = separateToken(element, textNode, start, length);
453
- this.addClassToHast(separated[1], className);
454
- const output = separated.filter(Boolean);
455
- elements.splice(i, 1, ...output);
456
- i += output.length - 1;
457
- }
458
- currentIdx += textNode.value.length;
459
- }
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
+ }
460
413
  }
461
414
  function hasOverlap(range1, range2) {
462
- return range1[0] <= range2[1] && range1[1] >= range2[0];
415
+ return range1[0] <= range2[1] && range1[1] >= range2[0];
463
416
  }
464
417
  function separateToken(span, textNode, index, len) {
465
- const text = textNode.value;
466
- const createNode = (value) => inheritElement(span, {
467
- children: [
468
- {
469
- type: "text",
470
- value
471
- }
472
- ]
473
- });
474
- return [
475
- index > 0 ? createNode(text.slice(0, index)) : void 0,
476
- createNode(text.slice(index, index + len)),
477
- index + len < text.length ? createNode(text.slice(index + len)) : void 0
478
- ];
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
+ ];
479
428
  }
480
429
  function inheritElement(original, overrides) {
481
- return {
482
- ...original,
483
- properties: {
484
- ...original.properties
485
- },
486
- ...overrides
487
- };
430
+ return {
431
+ ...original,
432
+ properties: { ...original.properties },
433
+ ...overrides
434
+ };
488
435
  }
489
436
 
437
+ //#endregion
438
+ //#region src/transformers/notation-highlight-word.ts
490
439
  function transformerNotationWordHighlight(options = {}) {
491
- const {
492
- classActiveWord = "highlighted-word",
493
- classActivePre = void 0
494
- } = options;
495
- return createCommentNotationTransformer(
496
- "@shikijs/transformers:notation-highlight-word",
497
- /\s*\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/,
498
- function([_, word, range], _line, comment, lines, index) {
499
- const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length;
500
- word = word.replace(/\\(.)/g, "$1");
501
- for (let i = index; i < Math.min(index + lineNum, lines.length); i++) {
502
- highlightWordInLine.call(this, lines[i], comment, word, classActiveWord);
503
- }
504
- if (classActivePre)
505
- this.addClassToHast(this.pre, classActivePre);
506
- return true;
507
- },
508
- options.matchAlgorithm
509
- );
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);
510
448
  }
511
449
 
450
+ //#endregion
451
+ //#region src/transformers/remove-comments.ts
452
+ /**
453
+ * Remove comments from the code.
454
+ */
512
455
  function transformerRemoveComments(options = {}) {
513
- const { removeEmptyLines = true } = options;
514
- return {
515
- name: "@shikijs/transformers:remove-comments",
516
- preprocess(_code, options2) {
517
- if (options2.includeExplanation !== true && options2.includeExplanation !== "scopeName")
518
- throw new Error("`transformerRemoveComments` requires `includeExplanation` to be set to `true` or `'scopeName'`");
519
- },
520
- tokens(tokens) {
521
- const result = [];
522
- for (const line of tokens) {
523
- const filteredLine = [];
524
- let hasComment = false;
525
- for (const token of line) {
526
- const isComment = token.explanation?.some(
527
- (exp) => exp.scopes.some((s) => s.scopeName.startsWith("comment"))
528
- );
529
- if (isComment) {
530
- hasComment = true;
531
- } else {
532
- filteredLine.push(token);
533
- }
534
- }
535
- if (removeEmptyLines && hasComment) {
536
- const isAllWhitespace = filteredLine.every((token) => !token.content.trim());
537
- if (isAllWhitespace)
538
- continue;
539
- }
540
- result.push(filteredLine);
541
- }
542
- return result;
543
- }
544
- };
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
+ };
545
477
  }
546
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
+ */
547
485
  function transformerRemoveLineBreak() {
548
- return {
549
- name: "@shikijs/transformers:remove-line-break",
550
- code(code) {
551
- code.children = code.children.filter((line) => !(line.type === "text" && line.value === "\n"));
552
- }
553
- };
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
+ };
554
492
  }
555
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
+ */
556
501
  function transformerRemoveNotationEscape() {
557
- return {
558
- name: "@shikijs/transformers:remove-notation-escape",
559
- code(hast) {
560
- function replace(node) {
561
- if (node.type === "text") {
562
- node.value = node.value.replace("[\\!code", "[!code");
563
- } else if ("children" in node) {
564
- for (const child of node.children) {
565
- replace(child);
566
- }
567
- }
568
- }
569
- replace(hast);
570
- return hast;
571
- }
572
- };
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
+ };
573
513
  }
574
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
+ */
575
521
  function transformerRenderIndentGuides(options = {}) {
576
- return {
577
- name: "@shikijs/transformers:render-indent-guides",
578
- code(hast) {
579
- const indent = Number(
580
- this.options.meta?.indent ?? this.options.meta?.__raw?.match(/\{indent:(\d+|false)\}/)?.[1] ?? options.indent ?? 2
581
- );
582
- if (Number.isNaN(indent) || indent <= 0) {
583
- return hast;
584
- }
585
- const indentRegex = new RegExp(` {${indent}}| {0,${indent - 1}} | {1,}$`, "g");
586
- const emptyLines = [];
587
- let level = 0;
588
- for (const line of hast.children) {
589
- if (line.type !== "element") {
590
- continue;
591
- }
592
- const first = line.children[0];
593
- if (first?.type !== "element" || first?.children[0]?.type !== "text") {
594
- emptyLines.push([line, level]);
595
- continue;
596
- }
597
- const text = first.children[0];
598
- const blanks = text.value.split(/[^ \t]/, 1)[0];
599
- const ranges = [];
600
- for (const match of blanks.matchAll(indentRegex)) {
601
- const start = match.index;
602
- const end = start + match[0].length;
603
- ranges.push([start, end]);
604
- }
605
- for (const [line2, level2] of emptyLines) {
606
- line2.children.unshift(...Array.from({ length: Math.min(ranges.length, level2 + 1) }, (_, i) => ({
607
- type: "element",
608
- tagName: "span",
609
- properties: {
610
- class: "indent",
611
- style: `--indent-offset: ${i * indent}ch;`
612
- },
613
- children: []
614
- })));
615
- }
616
- emptyLines.length = 0;
617
- level = ranges.length;
618
- if (ranges.length) {
619
- line.children.unshift(
620
- ...ranges.map(([start, end]) => ({
621
- type: "element",
622
- tagName: "span",
623
- properties: {
624
- class: "indent"
625
- },
626
- children: [{
627
- type: "text",
628
- value: text.value.slice(start, end)
629
- }]
630
- }))
631
- );
632
- text.value = text.value.slice(ranges.at(-1)[1]);
633
- }
634
- }
635
- return hast;
636
- }
637
- };
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
+ };
638
572
  }
639
573
 
574
+ //#endregion
575
+ //#region src/shared/utils.ts
640
576
  function isTab(part) {
641
- return part === " ";
577
+ return part === " ";
642
578
  }
643
579
  function isSpace(part) {
644
- return part === " " || part === " ";
580
+ return part === " " || part === " ";
645
581
  }
646
582
  function separateContinuousSpaces(inputs) {
647
- const result = [];
648
- let current = "";
649
- function bump() {
650
- if (current.length)
651
- result.push(current);
652
- current = "";
653
- }
654
- inputs.forEach((part, idx) => {
655
- if (isTab(part)) {
656
- bump();
657
- result.push(part);
658
- } else if (isSpace(part) && (isSpace(inputs[idx - 1]) || isSpace(inputs[idx + 1]))) {
659
- bump();
660
- result.push(part);
661
- } else {
662
- current += part;
663
- }
664
- });
665
- bump();
666
- 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;
667
600
  }
668
601
  function splitSpaces(parts, type, renderContinuousSpaces = true) {
669
- if (type === "all")
670
- return parts;
671
- let leftCount = 0;
672
- let rightCount = 0;
673
- if (type === "boundary" || type === "leading") {
674
- for (let i = 0; i < parts.length; i++) {
675
- if (isSpace(parts[i]))
676
- leftCount++;
677
- else
678
- break;
679
- }
680
- }
681
- if (type === "boundary" || type === "trailing") {
682
- for (let i = parts.length - 1; i >= 0; i--) {
683
- if (isSpace(parts[i]))
684
- rightCount++;
685
- else
686
- break;
687
- }
688
- }
689
- const middle = parts.slice(leftCount, parts.length - rightCount);
690
- return [
691
- ...parts.slice(0, leftCount),
692
- ...renderContinuousSpaces ? separateContinuousSpaces(middle) : [middle.join("")],
693
- ...parts.slice(parts.length - rightCount)
694
- ];
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
+ ];
695
615
  }
696
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
+ */
697
623
  function transformerRenderWhitespace(options = {}) {
698
- const classMap = {
699
- " ": options.classSpace ?? "space",
700
- " ": options.classTab ?? "tab"
701
- };
702
- const position = options.position ?? "all";
703
- const keys = Object.keys(classMap);
704
- return {
705
- name: "@shikijs/transformers:render-whitespace",
706
- // We use `root` hook here to ensure it runs after all other transformers
707
- root(root) {
708
- const pre = root.children[0];
709
- const code = pre.tagName === "pre" ? pre.children[0] : { children: [root] };
710
- code.children.forEach(
711
- (line) => {
712
- if (line.type !== "element" && line.type !== "root")
713
- return;
714
- const elements = line.children.filter((token) => token.type === "element");
715
- const last = elements.length - 1;
716
- line.children = line.children.flatMap((token) => {
717
- if (token.type !== "element")
718
- return token;
719
- const index = elements.indexOf(token);
720
- if (position === "boundary" && index !== 0 && index !== last)
721
- return token;
722
- if (position === "trailing" && index !== last)
723
- return token;
724
- if (position === "leading" && index !== 0)
725
- return token;
726
- const node = token.children[0];
727
- if (node.type !== "text" || !node.value)
728
- return token;
729
- const parts = splitSpaces(
730
- node.value.split(/([ \t])/).filter((i) => i.length),
731
- position === "boundary" && index === last && last !== 0 ? "trailing" : position,
732
- position !== "trailing" && position !== "leading"
733
- );
734
- if (parts.length <= 1)
735
- return token;
736
- return parts.map((part) => {
737
- const clone = {
738
- ...token,
739
- properties: { ...token.properties }
740
- };
741
- clone.children = [{ type: "text", value: part }];
742
- if (keys.includes(part)) {
743
- this.addClassToHast(clone, classMap[part]);
744
- delete clone.properties.style;
745
- }
746
- return clone;
747
- });
748
- });
749
- }
750
- );
751
- }
752
- };
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
+ };
753
667
  }
754
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
+ */
755
675
  function transformerStyleToClass(options = {}) {
756
- const {
757
- classPrefix = "__shiki_",
758
- classSuffix = "",
759
- classReplacer = (className) => className
760
- } = options;
761
- const classToStyle = /* @__PURE__ */ new Map();
762
- function stringifyStyle(style) {
763
- return Object.entries(style).map(([key, value]) => `${key}:${value}`).join(";");
764
- }
765
- function registerStyle(style) {
766
- const str = typeof style === "string" ? style : stringifyStyle(style);
767
- let className = classPrefix + cyrb53(str) + classSuffix;
768
- className = classReplacer(className);
769
- if (!classToStyle.has(className)) {
770
- classToStyle.set(
771
- className,
772
- typeof style === "string" ? style : { ...style }
773
- );
774
- }
775
- return className;
776
- }
777
- return {
778
- name: "@shikijs/transformers:style-to-class",
779
- pre(t) {
780
- if (!t.properties.style)
781
- return;
782
- const className = registerStyle(t.properties.style);
783
- delete t.properties.style;
784
- this.addClassToHast(t, className);
785
- },
786
- tokens(lines) {
787
- for (const line of lines) {
788
- for (const token of line) {
789
- if (!token.htmlStyle)
790
- continue;
791
- const className = registerStyle(token.htmlStyle);
792
- token.htmlStyle = {};
793
- token.htmlAttrs ||= {};
794
- if (!token.htmlAttrs.class)
795
- token.htmlAttrs.class = className;
796
- else
797
- token.htmlAttrs.class += ` ${className}`;
798
- }
799
- }
800
- },
801
- getClassRegistry() {
802
- return classToStyle;
803
- },
804
- getCSS() {
805
- let css = "";
806
- for (const [className, style] of classToStyle.entries()) {
807
- css += `.${className}{${typeof style === "string" ? style : stringifyStyle(style)}}`;
808
- }
809
- return css;
810
- },
811
- clearRegistry() {
812
- classToStyle.clear();
813
- }
814
- };
815
- }
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
+ */
816
723
  function cyrb53(str, seed = 0) {
817
- let h1 = 3735928559 ^ seed;
818
- let h2 = 1103547991 ^ seed;
819
- for (let i = 0, ch; i < str.length; i++) {
820
- ch = str.charCodeAt(i);
821
- h1 = Math.imul(h1 ^ ch, 2654435761);
822
- h2 = Math.imul(h2 ^ ch, 1597334677);
823
- }
824
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
825
- h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
826
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
827
- h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
828
- 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);
829
736
  }
830
737
 
831
- 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 };