rehype-highlight-code-lines 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ipikuka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # rehype-highlight-code-lines
2
+
3
+ [![NPM version][badge-npm-version]][npm-package-url]
4
+ [![NPM downloads][badge-npm-download]][npm-package-url]
5
+ [![Build][badge-build]][github-workflow-url]
6
+ [![codecov](https://codecov.io/gh/ipikuka/rehype-highlight-code-lines/graph/badge.svg?token=RKrZlvMHwq)](https://codecov.io/gh/ipikuka/rehype-highlight-code-lines)
7
+ [![type-coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage&prefix=%E2%89%A5&suffix=%&query=$.typeCoverage.atLeast&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fipikuka%2Frehype-highlight-code-lines%2Fmaster%2Fpackage.json)](https://github.com/ipikuka/rehype-highlight-code-lines)
8
+ [![typescript][badge-typescript]][typescript-url]
9
+ [![License][badge-license]][github-license-url]
10
+
11
+ This package is a [unified][unified] ([rehype][rehype]) plugin **to add wrapper to each line in a code block, allowing numbering of the code block and highlighting desired lines of code**.
12
+
13
+ **[unified][unified]** is a project that transforms content with abstract syntax trees (ASTs) using the new parser **[micromark][micromark]**. **[remark][remark]** adds support for markdown to unified. **[mdast][mdast]** is the Markdown Abstract Syntax Tree (AST) which is a specification for representing markdown in a syntax tree. "**[rehype][rehype]**" is a tool that transforms HTML with plugins. "**[hast][hast]**" stands for HTML Abstract Syntax Tree (HAST) that rehype uses.
14
+
15
+ **This plugin allows adding line numbers to code blocks and highlighting of desired code lines.**
16
+
17
+ ## When should I use this?
18
+
19
+ The `rehype-highlight-code-lines` is useful if you want to add line numbers to code blocks and want to highlight desired code lines.
20
+
21
+ **The `rehype-highlight-code-lines` is NOT code highlighter and does NOT provide code highlighting!** You can use a code highlighter for example **[rehype-highlight][rehype-highlight]** to highlight the code, then use the `rehype-highlight-code-lines` **after**.
22
+
23
+ > [!IMPORTANT]
24
+ > If the code highlighter already provides numbering and highlighting code lines, don't use `rehype-highlight-code-lines`!
25
+ > \
26
+ > \
27
+ > You can use `rehype-highlight-code-lines` even without a code highlighter.
28
+
29
+ ## Installation
30
+
31
+ This package is suitable for ESM only. In Node.js (version 16+), install with npm:
32
+
33
+ ```bash
34
+ npm install rehype-highlight-code-lines
35
+ ```
36
+
37
+ or
38
+
39
+ ```bash
40
+ yarn add rehype-highlight-code-lines
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ In a code fence, just after the language of the code block,
46
+ + specify a range of number in curly braces in order to highlight desired code lines,
47
+ + and/or add `showLineNumbers` in order to add numbering to code lines.
48
+
49
+ **\`\`\`[language] {2,4-6} showLineNumbers**
50
+
51
+ **\`\`\`[language] showLineNumbers {2}**
52
+
53
+ **\`\`\`[language] {1-3}**
54
+
55
+ **\`\`\`[language] showLineNumbers**
56
+
57
+ You can use the specifiers without a language.
58
+
59
+ **\`\`\`{5} showLineNumbers**
60
+
61
+ **\`\`\`showLineNumbers {5}**
62
+
63
+ **\`\`\`{2,3}**
64
+
65
+ **\`\`\`showLineNumbers**
66
+
67
+ Say we have the following markdown file, `example.md`:
68
+
69
+ ````markdown
70
+ ```javascript {2} showLineNumbers
71
+ let a1;
72
+ let a2;
73
+ let a3;
74
+ ```
75
+ ````
76
+
77
+ I assume you use `rehype-highlight` for code highlighting. Our module, `example.js`, looks as follows:
78
+
79
+ ```javascript
80
+ import { read } from "to-vfile";
81
+ import remark from "remark";
82
+ import gfm from "remark-gfm";
83
+ import remarkRehype from "remark-rehype";
84
+ import rehypeHighlight from "rehype-highlight";
85
+ import rehypeHighlightLines from "rehype-highlight-code-lines";
86
+ import rehypeStringify from "rehype-stringify";
87
+
88
+ main();
89
+
90
+ async function main() {
91
+ const file = await remark()
92
+ .use(gfm)
93
+ .use(remarkRehype)
94
+ .use(rehypeHighlight)
95
+ .use(rehypeHighlightLines)
96
+ .use(rehypeStringify)
97
+ .process(await read("example.md"));
98
+
99
+ console.log(String(file));
100
+ }
101
+ ```
102
+
103
+ Now, running `node example.js` you will see that each line of code is wrapped in a `div`, which has appropriate class names (`code-line`, `numbered-code-line`, `highlighted-code-line`) and line numbering attribute `data-line-number`.
104
+
105
+ ```html
106
+ <pre>
107
+ <code class="hljs language-javascript">
108
+ <div class="code-line numbered-code-line" data-line-number="1">
109
+ <span class="hljs-keyword">let</span> a1;
110
+ </div>
111
+ <div
112
+ class="code-line numbered-code-line highlighted-code-line" data-line-number="2">
113
+ <span class="hljs-keyword">let</span> a2;
114
+ </div>
115
+ <div class="code-line numbered-code-line" data-line-number="3">
116
+ <span class="hljs-keyword">let</span> a3;
117
+ </div>
118
+ </code>
119
+ </pre>
120
+ ```
121
+
122
+ Without `rehype-highlight-code-lines`, the lines of code wouldn't be in a `div`.
123
+
124
+ ```html
125
+ <pre>
126
+ <code class="hljs language-javascript">
127
+ <span class="hljs-keyword">let</span> a1;
128
+ <span class="hljs-keyword">let</span> a2;
129
+ <span class="hljs-keyword">let</span> a3;
130
+ </code>
131
+ </pre>
132
+ ```
133
+
134
+ ## Options
135
+
136
+ There is one **boolean** option.
137
+
138
+ ```typescript
139
+ type HighlightLinesOptions = {
140
+ showLineNumbers?: boolean; // the default is "false"
141
+ };
142
+
143
+ use(rehypeHighlightLines, HighlightLinesOptions);
144
+ ```
145
+
146
+ ### Examples:
147
+
148
+ ```typescript
149
+ use(rehypeHighlightLines);
150
+
151
+ // all code blocks will be numbered in MDX/markdown source
152
+ use(rehypeHighlightLines, {showLineNumbers: true});
153
+ ```
154
+
155
+ ## Syntax tree
156
+
157
+ This plugin modifies the `hast` (HTML abstract syntax tree).
158
+
159
+ ## Types
160
+
161
+ This package is fully typed with [TypeScript][typescript].
162
+
163
+ The plugin exports the type `HighlightLinesOptions`.
164
+
165
+ ## Compatibility
166
+
167
+ This plugin works with `rehype-parse` version 1+, `rehype-stringify` version 1+, `rehype` version 1+, and unified version `4+`.
168
+
169
+ ## Security
170
+
171
+ Use of `rehype-highlight-code-lines` involves rehype (hast), but doesn't lead to cross-site scripting (XSS) attacks.
172
+
173
+ ## My Plugins
174
+
175
+ I like to contribute the Unified / Remark / MDX ecosystem, so I recommend you to have a look my plugins.
176
+
177
+ ### My Remark Plugins
178
+
179
+ - [`remark-flexible-code-titles`](https://www.npmjs.com/package/remark-flexible-code-titles)
180
+ – Remark plugin to add titles or/and containers for the code blocks with customizable properties
181
+ - [`remark-flexible-containers`](https://www.npmjs.com/package/remark-flexible-containers)
182
+ – Remark plugin to add custom containers with customizable properties in markdown
183
+ - [`remark-ins`](https://www.npmjs.com/package/remark-ins)
184
+ – Remark plugin to add `ins` element in markdown
185
+ - [`remark-flexible-paragraphs`](https://www.npmjs.com/package/remark-flexible-paragraphs)
186
+ – Remark plugin to add custom paragraphs with customizable properties in markdown
187
+ - [`remark-flexible-markers`](https://www.npmjs.com/package/remark-flexible-markers)
188
+ – Remark plugin to add custom `mark` element with customizable properties in markdown
189
+ - [`remark-flexible-toc`](https://www.npmjs.com/package/remark-flexible-toc)
190
+ – Remark plugin to expose the table of contents via `vfile.data` or via an option reference
191
+ - [`remark-mdx-remove-esm`](https://www.npmjs.com/package/remark-mdx-remove-esm)
192
+ – Remark plugin to remove import and/or export statements (mdxjsEsm)
193
+
194
+ ### My Rehype Plugins
195
+
196
+ - [`rehype-pre-language`](https://www.npmjs.com/package/rehype-pre-language)
197
+ – Rehype plugin to add language information as a property to `pre` element
198
+ - [`rehype-highlight-code-lines`](https://www.npmjs.com/package/rehype-highlight-code-lines)
199
+ – Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines
200
+
201
+ ### My Recma Plugins
202
+
203
+ - [`recma-mdx-escape-missing-components`](https://www.npmjs.com/package/recma-mdx-escape-missing-components)
204
+ – Recma plugin to set the default value `() => null` for the Components in MDX in case of missing or not provided so as not to throw an error
205
+ - [`recma-mdx-change-props`](https://www.npmjs.com/package/recma-mdx-change-props)
206
+ – Recma plugin to change the `props` parameter into the `_props` in the `function _createMdxContent(props) {/* */}` in the compiled source in order to be able to use `{props.foo}` like expressions. It is useful for the `next-mdx-remote` or `next-mdx-remote-client` users in `nextjs` applications.
207
+
208
+ ## License
209
+
210
+ [MIT License](./LICENSE) © ipikuka
211
+
212
+ ### Keywords
213
+
214
+ 🟩 [unified][unifiednpm] 🟩 [rehype][rehypenpm] 🟩 [rehype plugin][rehypepluginnpm] 🟩 [hast][hastnpm] 🟩 [markdown][markdownnpm] 🟩 [rehype-highlight][rehypehighlightnpm]
215
+
216
+ [unifiednpm]: https://www.npmjs.com/search?q=keywords:unified
217
+ [rehypenpm]: https://www.npmjs.com/search?q=keywords:rehype
218
+ [rehypepluginnpm]: https://www.npmjs.com/search?q=keywords:rehype%20plugin
219
+ [hastnpm]: https://www.npmjs.com/search?q=keywords:hast
220
+ [markdownnpm]: https://www.npmjs.com/search?q=keywords:markdown
221
+ [rehypehighlightnpm]: https://www.npmjs.com/search?q=keywords:rehype-highlight
222
+
223
+ [unified]: https://github.com/unifiedjs/unified
224
+ [micromark]: https://github.com/micromark/micromark
225
+ [remark]: https://github.com/remarkjs/remark
226
+ [remarkplugins]: https://github.com/remarkjs/remark/blob/main/doc/plugins.md
227
+ [mdast]: https://github.com/syntax-tree/mdast
228
+ [rehype]: https://github.com/rehypejs/rehype
229
+ [rehypeplugins]: https://github.com/rehypejs/rehype/blob/main/doc/plugins.md
230
+ [hast]: https://github.com/syntax-tree/hast
231
+ [typescript]: https://www.typescriptlang.org/
232
+ [rehype-highlight]: https://github.com/rehypejs/rehype-highlight
233
+
234
+ [badge-npm-version]: https://img.shields.io/npm/v/rehype-highlight-code-lines
235
+ [badge-npm-download]:https://img.shields.io/npm/dt/rehype-highlight-code-lines
236
+ [npm-package-url]: https://www.npmjs.com/package/rehype-highlight-code-lines
237
+
238
+ [badge-license]: https://img.shields.io/github/license/ipikuka/rehype-highlight-code-lines
239
+ [github-license-url]: https://github.com/ipikuka/rehype-highlight-code-lines/blob/main/LICENSE
240
+
241
+ [badge-build]: https://github.com/ipikuka/rehype-highlight-code-lines/actions/workflows/publish.yml/badge.svg
242
+ [github-workflow-url]: https://github.com/ipikuka/rehype-highlight-code-lines/actions/workflows/publish.yml
243
+
244
+ [badge-typescript]: https://img.shields.io/npm/types/rehype-highlight-code-lines
245
+ [typescript-url]: https://www.typescriptlang.org/
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from "unified";
2
+ import type { Root } from "hast";
3
+ export type HighlightLinesOptions = {
4
+ showLineNumbers?: boolean;
5
+ };
6
+ export declare function clsx(arr: (string | false | null | undefined | 0)[]): string[];
7
+ /**
8
+ *
9
+ * adds line numbers to code blocks and allow highlighting of desired code lines
10
+ *
11
+ */
12
+ declare const plugin: Plugin<[HighlightLinesOptions?], Root>;
13
+ export default plugin;
@@ -0,0 +1,152 @@
1
+ import { visit, CONTINUE } from "unist-util-visit";
2
+ import rangeParser from "parse-numeric-range";
3
+ const DEFAULT_SETTINGS = {
4
+ showLineNumbers: false,
5
+ };
6
+ // a simple util for our use case, like clsx package
7
+ export function clsx(arr) {
8
+ return arr.filter((item) => !!item);
9
+ }
10
+ /**
11
+ *
12
+ * adds line numbers to code blocks and allow highlighting of desired code lines
13
+ *
14
+ */
15
+ const plugin = (options) => {
16
+ const settings = Object.assign({}, DEFAULT_SETTINGS, options);
17
+ /**
18
+ *
19
+ * constructs the line element
20
+ *
21
+ */
22
+ const createLine = (children, lineNumber, classNames) => {
23
+ return {
24
+ type: "element",
25
+ tagName: "div",
26
+ children,
27
+ properties: {
28
+ className: classNames,
29
+ ...(lineNumber && { "data-line-number": lineNumber }),
30
+ },
31
+ };
32
+ };
33
+ // match all common types of line breaks
34
+ const RE = /\r?\n|\r/g;
35
+ function starryNightGutter(tree, showLineNumbers, linesToBeHighlighted) {
36
+ const replacement = [];
37
+ let index = -1;
38
+ let start = 0;
39
+ let startTextRemainder = "";
40
+ let lineNumber = 0;
41
+ while (++index < tree.children.length) {
42
+ const child = tree.children[index];
43
+ if (child.type === "text") {
44
+ let textStart = 0;
45
+ let match = RE.exec(child.value);
46
+ while (match) {
47
+ // Nodes in this line.
48
+ const line = tree.children.slice(start, index);
49
+ /* v8 ignore start */
50
+ // Prepend text from a partial matched earlier text.
51
+ if (startTextRemainder) {
52
+ line.unshift({ type: "text", value: startTextRemainder });
53
+ startTextRemainder = "";
54
+ }
55
+ /* v8 ignore end */
56
+ // Append text from this text.
57
+ if (match.index > textStart) {
58
+ line.push({
59
+ type: "text",
60
+ value: child.value.slice(textStart, match.index),
61
+ });
62
+ }
63
+ // Add a line, and the eol.
64
+ lineNumber += 1;
65
+ replacement.push(createLine(line, showLineNumbers ? lineNumber : undefined, clsx([
66
+ "code-line",
67
+ showLineNumbers && "numbered-code-line",
68
+ linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
69
+ ])), {
70
+ type: "text",
71
+ value: match[0],
72
+ });
73
+ start = index + 1;
74
+ textStart = match.index + match[0].length;
75
+ match = RE.exec(child.value);
76
+ }
77
+ // If we matched, make sure to not drop the text after the last line ending.
78
+ if (start === index + 1) {
79
+ startTextRemainder = child.value.slice(textStart);
80
+ }
81
+ }
82
+ }
83
+ const line = tree.children.slice(start);
84
+ /* v8 ignore start */
85
+ // Prepend text from a partial matched earlier text.
86
+ if (startTextRemainder) {
87
+ line.unshift({ type: "text", value: startTextRemainder });
88
+ startTextRemainder = "";
89
+ }
90
+ if (line.length > 0) {
91
+ lineNumber += 1;
92
+ replacement.push(createLine(line, showLineNumbers ? lineNumber : undefined, clsx([
93
+ "code-line",
94
+ showLineNumbers && "numbered-code-line",
95
+ linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
96
+ ])));
97
+ }
98
+ /* v8 ignore end */
99
+ // Replace children with new array.
100
+ tree.children = replacement;
101
+ }
102
+ /**
103
+ * Transform.
104
+ *
105
+ * @param {Root} tree
106
+ * Tree.
107
+ * @returns {undefined}
108
+ * Nothing.
109
+ */
110
+ return (tree) => {
111
+ visit(tree, "element", function (node, index, parent) {
112
+ /* v8 ignore next */
113
+ if (!parent || typeof index === "undefined")
114
+ return;
115
+ if (node.tagName !== "pre")
116
+ return CONTINUE;
117
+ const code = node.children[0];
118
+ /* v8 ignore next */
119
+ if (!code || code.type !== "element" || code.tagName !== "code")
120
+ return;
121
+ let meta = code.data?.meta?.toLowerCase().trim();
122
+ // handle if there is no language provided in code block
123
+ if (Array.isArray(code.properties.className)) {
124
+ const testingFunction = (element) => typeof element === "string" && element.startsWith("language-");
125
+ const className = code.properties.className.find(testingFunction);
126
+ if (className) {
127
+ const language = className.slice(9).toLowerCase();
128
+ if (language.startsWith("{") || language.startsWith("showlinenumbers")) {
129
+ meta = meta ? language + meta : language;
130
+ const index = code.properties.className.findIndex(testingFunction);
131
+ if (index > -1) {
132
+ code.properties.className[index] = "language-unknown";
133
+ }
134
+ }
135
+ }
136
+ }
137
+ if (!meta)
138
+ return;
139
+ const showLineNumbers = settings.showLineNumbers || meta.includes("showlinenumbers");
140
+ // find number range string within curly braces and parse it
141
+ const RE = /{(?<lines>[\d\s,-]+)}/g;
142
+ const strLineNumbers = RE.exec(meta)?.groups?.lines?.replace(/\s/g, "");
143
+ const linesToBeHighlighted = strLineNumbers ? rangeParser(strLineNumbers) : [];
144
+ if (!showLineNumbers && linesToBeHighlighted.length === 0)
145
+ return;
146
+ // add wrapper for each line mutating the code element
147
+ starryNightGutter(code, showLineNumbers, linesToBeHighlighted);
148
+ });
149
+ };
150
+ };
151
+ export default plugin;
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "rehype-highlight-code-lines",
3
+ "version": "0.0.1",
4
+ "description": "Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines",
5
+ "type": "module",
6
+ "exports": "./dist/esm/index.js",
7
+ "main": "./dist/esm/index.js",
8
+ "types": "./dist/esm/index.d.ts",
9
+ "scripts": {
10
+ "build": "rimraf dist && tsc --build && type-coverage",
11
+ "format": "npm run prettier && npm run lint",
12
+ "prettier": "prettier --write .",
13
+ "lint": "eslint .",
14
+ "test": "vitest --watch=false",
15
+ "test:watch": "vitest",
16
+ "test:file": "vitest test.spec.ts",
17
+ "prepack": "npm run build",
18
+ "prepublishOnly": "npm run test && npm run format && npm run test-coverage",
19
+ "test-coverage": "vitest run --coverage"
20
+ },
21
+ "files": [
22
+ "dist/",
23
+ "src/",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/ipikuka/rehype-highlight-code-lines.git"
30
+ },
31
+ "keywords": [
32
+ "unified",
33
+ "hast",
34
+ "rehype",
35
+ "markdown",
36
+ "plugin",
37
+ "rehype-plugin",
38
+ "rehype-highlight",
39
+ "line numbering",
40
+ "line highlighting"
41
+ ],
42
+ "author": "ipikuka <talatkuyuk@gmail.com>",
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/ipikuka/rehype-highlight-code-lines#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/ipikuka/rehype-highlight-code-lines/issues"
47
+ },
48
+ "devDependencies": {
49
+ "@types/dedent": "^0.7.2",
50
+ "@types/node": "^20.14.2",
51
+ "@typescript-eslint/eslint-plugin": "^7.13.0",
52
+ "@typescript-eslint/parser": "^7.13.0",
53
+ "@vitest/coverage-v8": "^1.6.0",
54
+ "dedent": "^1.5.3",
55
+ "eslint": "^8.57.0",
56
+ "eslint-config-prettier": "^9.1.0",
57
+ "eslint-plugin-prettier": "^5.1.3",
58
+ "prettier": "^3.3.2",
59
+ "rehype-highlight": "^7.0.0",
60
+ "rehype-stringify": "^10.0.0",
61
+ "remark-gfm": "^4.0.0",
62
+ "remark-parse": "^11.0.0",
63
+ "remark-rehype": "^11.1.0",
64
+ "rimraf": "^5.0.7",
65
+ "type-coverage": "^2.29.0",
66
+ "typescript": "^5.4.5",
67
+ "unified": "^11.0.4",
68
+ "vfile": "^6.0.1",
69
+ "vitest": "^1.4.0"
70
+ },
71
+ "dependencies": {
72
+ "@types/hast": "^3.0.4",
73
+ "parse-numeric-range": "^1.3.0",
74
+ "unist-util-visit": "^5.0.0"
75
+ },
76
+ "typeCoverage": {
77
+ "atLeast": 100,
78
+ "detail": true,
79
+ "ignoreAsAssertion": true,
80
+ "ignoreCatch": true,
81
+ "strict": true
82
+ },
83
+ "sideEffects": false
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,217 @@
1
+ import type { Plugin } from "unified";
2
+ import type { Root, Element, ElementContent, ElementData } from "hast";
3
+ import { type VisitorResult, visit, CONTINUE } from "unist-util-visit";
4
+ import rangeParser from "parse-numeric-range";
5
+
6
+ export type HighlightLinesOptions = {
7
+ showLineNumbers?: boolean;
8
+ };
9
+
10
+ const DEFAULT_SETTINGS: HighlightLinesOptions = {
11
+ showLineNumbers: false,
12
+ };
13
+
14
+ type CodeData = ElementData & {
15
+ meta?: string;
16
+ };
17
+
18
+ // a simple util for our use case, like clsx package
19
+ export function clsx(arr: (string | false | null | undefined | 0)[]): string[] {
20
+ return arr.filter((item): item is string => !!item);
21
+ }
22
+
23
+ /**
24
+ *
25
+ * adds line numbers to code blocks and allow highlighting of desired code lines
26
+ *
27
+ */
28
+ const plugin: Plugin<[HighlightLinesOptions?], Root> = (options) => {
29
+ const settings = Object.assign({}, DEFAULT_SETTINGS, options);
30
+
31
+ /**
32
+ *
33
+ * constructs the line element
34
+ *
35
+ */
36
+ const createLine = (
37
+ children: ElementContent[],
38
+ lineNumber: number | undefined,
39
+ classNames: string[],
40
+ ): Element => {
41
+ return {
42
+ type: "element",
43
+ tagName: "div",
44
+ children,
45
+ properties: {
46
+ className: classNames,
47
+ ...(lineNumber && { "data-line-number": lineNumber }),
48
+ },
49
+ };
50
+ };
51
+
52
+ // match all common types of line breaks
53
+ const RE = /\r?\n|\r/g;
54
+
55
+ function starryNightGutter(
56
+ tree: Element,
57
+ showLineNumbers: boolean,
58
+ linesToBeHighlighted: number[],
59
+ ) {
60
+ const replacement: Array<ElementContent> = [];
61
+ let index = -1;
62
+ let start = 0;
63
+ let startTextRemainder = "";
64
+ let lineNumber = 0;
65
+
66
+ while (++index < tree.children.length) {
67
+ const child = tree.children[index];
68
+
69
+ if (child.type === "text") {
70
+ let textStart = 0;
71
+ let match = RE.exec(child.value);
72
+
73
+ while (match) {
74
+ // Nodes in this line.
75
+ const line = tree.children.slice(start, index);
76
+
77
+ /* v8 ignore start */
78
+
79
+ // Prepend text from a partial matched earlier text.
80
+ if (startTextRemainder) {
81
+ line.unshift({ type: "text", value: startTextRemainder });
82
+ startTextRemainder = "";
83
+ }
84
+
85
+ /* v8 ignore end */
86
+
87
+ // Append text from this text.
88
+ if (match.index > textStart) {
89
+ line.push({
90
+ type: "text",
91
+ value: child.value.slice(textStart, match.index),
92
+ });
93
+ }
94
+
95
+ // Add a line, and the eol.
96
+ lineNumber += 1;
97
+ replacement.push(
98
+ createLine(
99
+ line,
100
+ showLineNumbers ? lineNumber : undefined,
101
+ clsx([
102
+ "code-line",
103
+ showLineNumbers && "numbered-code-line",
104
+ linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
105
+ ]),
106
+ ),
107
+ {
108
+ type: "text",
109
+ value: match[0],
110
+ },
111
+ );
112
+
113
+ start = index + 1;
114
+ textStart = match.index + match[0].length;
115
+ match = RE.exec(child.value);
116
+ }
117
+
118
+ // If we matched, make sure to not drop the text after the last line ending.
119
+ if (start === index + 1) {
120
+ startTextRemainder = child.value.slice(textStart);
121
+ }
122
+ }
123
+ }
124
+
125
+ const line = tree.children.slice(start);
126
+
127
+ /* v8 ignore start */
128
+
129
+ // Prepend text from a partial matched earlier text.
130
+ if (startTextRemainder) {
131
+ line.unshift({ type: "text", value: startTextRemainder });
132
+ startTextRemainder = "";
133
+ }
134
+
135
+ if (line.length > 0) {
136
+ lineNumber += 1;
137
+ replacement.push(
138
+ createLine(
139
+ line,
140
+ showLineNumbers ? lineNumber : undefined,
141
+ clsx([
142
+ "code-line",
143
+ showLineNumbers && "numbered-code-line",
144
+ linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
145
+ ]),
146
+ ),
147
+ );
148
+ }
149
+
150
+ /* v8 ignore end */
151
+
152
+ // Replace children with new array.
153
+ tree.children = replacement;
154
+ }
155
+
156
+ /**
157
+ * Transform.
158
+ *
159
+ * @param {Root} tree
160
+ * Tree.
161
+ * @returns {undefined}
162
+ * Nothing.
163
+ */
164
+ return (tree: Root): undefined => {
165
+ visit(tree, "element", function (node, index, parent): VisitorResult {
166
+ /* v8 ignore next */
167
+ if (!parent || typeof index === "undefined") return;
168
+
169
+ if (node.tagName !== "pre") return CONTINUE;
170
+
171
+ const code = node.children[0];
172
+
173
+ /* v8 ignore next */
174
+ if (!code || code.type !== "element" || code.tagName !== "code") return;
175
+
176
+ let meta = (code.data as CodeData)?.meta?.toLowerCase().trim();
177
+
178
+ // handle if there is no language provided in code block
179
+ if (Array.isArray(code.properties.className)) {
180
+ const testingFunction = (element: string | number): element is string =>
181
+ typeof element === "string" && element.startsWith("language-");
182
+
183
+ const className = code.properties.className.find(testingFunction);
184
+
185
+ if (className) {
186
+ const language = className.slice(9).toLowerCase();
187
+
188
+ if (language.startsWith("{") || language.startsWith("showlinenumbers")) {
189
+ meta = meta ? language + meta : language;
190
+
191
+ const index = code.properties.className.findIndex(testingFunction);
192
+
193
+ if (index > -1) {
194
+ code.properties.className[index] = "language-unknown";
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ if (!meta) return;
201
+
202
+ const showLineNumbers = settings.showLineNumbers || meta.includes("showlinenumbers");
203
+
204
+ // find number range string within curly braces and parse it
205
+ const RE = /{(?<lines>[\d\s,-]+)}/g;
206
+ const strLineNumbers = RE.exec(meta)?.groups?.lines?.replace(/\s/g, "");
207
+ const linesToBeHighlighted = strLineNumbers ? rangeParser(strLineNumbers) : [];
208
+
209
+ if (!showLineNumbers && linesToBeHighlighted.length === 0) return;
210
+
211
+ // add wrapper for each line mutating the code element
212
+ starryNightGutter(code, showLineNumbers, linesToBeHighlighted);
213
+ });
214
+ };
215
+ };
216
+
217
+ export default plugin;