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 +21 -0
- package/README.md +245 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.js +152 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +84 -0
- package/src/index.ts +217 -0
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
|
+
[](https://codecov.io/gh/ipikuka/rehype-highlight-code-lines)
|
|
7
|
+
[](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;
|