73 |
74 | function
75 | fancyAlert(arg)
77 | {
78 |
79 |
80 | if
81 | (arg)
82 | {
83 |
84 |
85 | $.facebox({ div:
87 | '#foo'
88 | })
89 |
90 |
91 | }
92 |
93 |
94 | }
95 |
97 | ```
98 |
99 | ## Generating
100 |
101 | To customise the languages for your own prism plugin:
102 |
103 | ```js
104 | import { refractor } from 'refractor/lib/core.js'
105 | import markdown from 'refractor/lang/markdown.js'
106 | import rehypePrismGenerator from 'rehype-prism-plus/generator'
107 |
108 | refractor.register(markdown)
109 | const myPrismPlugin = rehypePrismGenerator(refractor)
110 | ```
111 |
112 | ## Styling
113 |
114 | To style the language tokens, you can just copy them from any prismjs compatible ones. Here's a list of [themes](https://github.com/PrismJS/prism-themes).
115 |
116 | In addition, the following styles should be added for line highlighting and line numbers to work correctly:
117 |
118 | ```css
119 | pre {
120 | overflow-x: auto;
121 | }
122 |
123 | /**
124 | * Inspired by gatsby remark prism - https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/
125 | * 1. Make the element just wide enough to fit its content.
126 | * 2. Always fill the visible space in .code-highlight.
127 | */
128 | .code-highlight {
129 | float: left; /* 1 */
130 | min-width: 100%; /* 2 */
131 | }
132 |
133 | .code-line {
134 | display: block;
135 | padding-left: 16px;
136 | padding-right: 16px;
137 | margin-left: -16px;
138 | margin-right: -16px;
139 | border-left: 4px solid rgba(0, 0, 0, 0); /* Set placeholder for highlight accent border color to transparent */
140 | }
141 |
142 | .code-line.inserted {
143 | background-color: rgba(16, 185, 129, 0.2); /* Set inserted line (+) color */
144 | }
145 |
146 | .code-line.deleted {
147 | background-color: rgba(239, 68, 68, 0.2); /* Set deleted line (-) color */
148 | }
149 |
150 | .highlight-line {
151 | margin-left: -16px;
152 | margin-right: -16px;
153 | background-color: rgba(55, 65, 81, 0.5); /* Set highlight bg color */
154 | border-left: 4px solid rgb(59, 130, 246); /* Set highlight accent border color */
155 | }
156 |
157 | .line-number::before {
158 | display: inline-block;
159 | width: 1rem;
160 | text-align: right;
161 | margin-right: 16px;
162 | margin-left: -8px;
163 | color: rgb(156, 163, 175); /* Line number color */
164 | content: attr(line);
165 | }
166 | ```
167 |
168 | Here's the styled output using the prism-night-owl theme:
169 |
170 | 
171 |
172 | For more information on styling of language tokens, consult [refractor] and [Prism].
173 |
174 | ## API
175 |
176 | `rehype().use(rehypePrism, [options])`
177 |
178 | Syntax highlights `pre > code`.
179 | Under the hood, it uses [refractor], which is a virtual version of [Prism].
180 |
181 | The code language is configured by setting a `language-{name}` class on the `` element.
182 | You can use any [language supported by refractor].
183 |
184 | If no `language-{name}` class is found on a `` element, it will be skipped.
185 |
186 | ### options
187 |
188 | #### options.ignoreMissing
189 |
190 | Type: `boolean`.
191 | Default: `false`.
192 |
193 | By default, if `{name}` does not correspond to a [language supported by refractor] an error will be thrown.
194 |
195 | If you would like to silently skip `` elements with invalid languages or support line numbers and line highlighting for code blocks without a specified language, set this option to `true`.
196 |
197 | #### options.defaultLanguage
198 |
199 | Type: `string`.
200 | Default: ``.
201 |
202 | Uses the specified language as the default if none is specified. Takes precedence over `ignoreMissing`.
203 |
204 | Note: The language must be first registered with [refractor].
205 |
206 | #### options.showLineNumbers
207 |
208 | Type: `boolean | string[]`
209 | Default: `false`
210 |
211 | By default, line numbers will only be displayed for code block cells with a meta property that includes 'showLineNumbers'. To control the starting line number, use `showLineNumbers=X`, where `X` is the starting line number as a meta property for the code block.
212 |
213 | If you would like to show line numbers for all code blocks without specifying the meta property, set this to `true`.
214 |
215 | Alternatively, you can specify an array of languages for which the line numbers should be shown. For example, setting the option as `showLineNumbers: ['typescript']` will display line numbers only for code blocks with the language `typescript`, while other languages (e.g., `text`) will not display line numbers.
216 |
217 | **Note**: This will wrongly assign a language class and the class might appear as `language-{1,3}` or `language-showLineNumbers`, but allow the language highlighting and line number function to work. An possible approach would be to add a placeholder like `unknown` so the `div` will have `class="language-unknown"`
218 |
219 | [rehype]: https://github.com/wooorm/rehype
220 | [prism]: http://prismjs.com/
221 | [refractor]: https://github.com/wooorm/refractor
222 | [rehype plugin]: https://github.com/rehypejs/rehype/blob/master/doc/plugins.md#using-plugins
223 | [xdm]: https://github.com/wooorm/xdm
224 | [mdx-bundler]: https://github.com/kentcdodds/mdx-bundler
225 | [next-mdx-remote]: https://github.com/hashicorp/next-mdx-remote
226 | [language supported by refractor]: https://github.com/wooorm/refractor#syntaxes
227 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rehype-prism-plus",
3 | "version": "2.0.1",
4 | "description": "rehype plugin to highlight code blocks in HTML with Prism (via refractor) with line highlighting and line numbers",
5 | "source": "index.js",
6 | "files": [
7 | "dist"
8 | ],
9 | "main": "./dist/index.es.js",
10 | "module": "./dist/index.es.js",
11 | "types": "./dist/index.d.ts",
12 | "type": "module",
13 | "exports": {
14 | ".": {
15 | "types": "./dist/index.d.ts",
16 | "default": "./dist/index.es.js"
17 | },
18 | "./common": {
19 | "types": "./dist/common.d.ts",
20 | "default": "./dist/common.es.js"
21 | },
22 | "./all": {
23 | "types": "./dist/all.d.ts",
24 | "default": "./dist/all.es.js"
25 | },
26 | "./generator": {
27 | "types": "./dist/generator.d.ts",
28 | "default": "./dist/generator.es.js"
29 | }
30 | },
31 | "typesVersions": {
32 | "*": {
33 | ".": [
34 | "./dist/index"
35 | ],
36 | "common": [
37 | "./dist/common"
38 | ],
39 | "all": [
40 | "./dist/all"
41 | ],
42 | "generator": [
43 | "./dist/generator"
44 | ]
45 | }
46 | },
47 | "scripts": {
48 | "build": "tsc -b && microbundle src/index.js src/common.js src/all.js src/generator.js --format esm",
49 | "tsc": "tsc --watch",
50 | "lint": "eslint .",
51 | "prettier": "prettier --write '*.js'",
52 | "test": "uvu"
53 | },
54 | "repository": {
55 | "type": "git",
56 | "url": "git+https://github.com/timlrx/rehype-prism-plus.git"
57 | },
58 | "keywords": [
59 | "rehype",
60 | "rehype-plugin",
61 | "syntax-highlighting",
62 | "prism",
63 | "mdx",
64 | "jsx"
65 | ],
66 | "author": "Timothy Lin (https://timlrx.com)",
67 | "license": "MIT",
68 | "bugs": {
69 | "url": "https://github.com/timlrx/rehype-prism-plus/issues"
70 | },
71 | "homepage": "https://github.com/timlrx/rehype-prism-plus#readme",
72 | "dependencies": {
73 | "hast-util-to-string": "^3.0.0",
74 | "parse-numeric-range": "^1.3.0",
75 | "refractor": "^4.8.0",
76 | "rehype-parse": "^9.0.0",
77 | "unist-util-filter": "^5.0.0",
78 | "unist-util-visit": "^5.0.0"
79 | },
80 | "devDependencies": {
81 | "dedent": "^0.7.0",
82 | "eslint": "^8.43.0",
83 | "eslint-config-prettier": "^8.3.0",
84 | "eslint-plugin-node": "^11.1.0",
85 | "husky": "^8.0.0",
86 | "lint-staged": "^11.1.2",
87 | "microbundle": "^0.15.1",
88 | "prettier": "^2.8.8",
89 | "rehype": "^13.0.1",
90 | "remark": "^15.0.1",
91 | "remark-rehype": "^11.0.0",
92 | "typescript": "5.1.3",
93 | "unified": "^11.0.4",
94 | "uvu": "^0.5.1"
95 | },
96 | "prettier": {
97 | "printWidth": 100,
98 | "tabWidth": 2,
99 | "useTabs": false,
100 | "singleQuote": true,
101 | "bracketSpacing": true,
102 | "semi": false,
103 | "trailingComma": "es5"
104 | },
105 | "lint-staged": {
106 | "*.+(js|jsx|ts|tsx)": [
107 | "eslint --fix"
108 | ],
109 | "*.+(js|jsx|ts|tsx|json|css|md|mdx)": [
110 | "prettier --write"
111 | ]
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/sample-code-block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timlrx/rehype-prism-plus/d4715b1eac2c8ed52d0fd793b38d4aafa26ea76c/sample-code-block.png
--------------------------------------------------------------------------------
/src/all.js:
--------------------------------------------------------------------------------
1 | import { refractor as refractorAll } from 'refractor/lib/all.js'
2 | import rehypePrismGenerator from './generator.js'
3 |
4 | /**
5 | * Rehype prism plugin that highlights code blocks with refractor (prismjs)
6 | * This supports all the languages and should be used on the server side.
7 | *
8 | * Consider using rehypePrismCommon or rehypePrismGenerator to generate a plugin
9 | * that supports your required languages.
10 | */
11 | const rehypePrismAll = rehypePrismGenerator(refractorAll)
12 |
13 | export default rehypePrismAll
14 |
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | import { refractor as refractorCommon } from 'refractor/lib/common.js'
2 | import rehypePrismGenerator from './generator.js'
3 |
4 | /**
5 | * Rehype prism plugin that highlights code blocks with refractor (prismjs)
6 | * Supported languages: https://github.com/wooorm/refractor#data
7 | *
8 | * Consider using rehypePrismGenerator to generate a plugin
9 | * that supports your required languages.
10 | */
11 | const rehypePrismCommon = rehypePrismGenerator(refractorCommon)
12 |
13 | export default rehypePrismCommon
14 |
--------------------------------------------------------------------------------
/src/generator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import('hast').Element} Element
3 | * @typedef {import('hast').Root} Root
4 | * @typedef Options options
5 | * Configuration.
6 | * @property {boolean|string[]} [showLineNumbers]
7 | * Set `showLineNumbers` to `true` to always display line number
8 | * @property {boolean} [ignoreMissing]
9 | * Set `ignoreMissing` to `true` to ignore unsupported languages and line highlighting when no language is specified
10 | * @property {string} [defaultLanguage]
11 | * Uses the specified language as the default if none is specified. Takes precedence over `ignoreMissing`.
12 | * Note: The language must be registered with refractor.
13 | */
14 |
15 | import { visit } from 'unist-util-visit'
16 | import { toString } from 'hast-util-to-string'
17 | import { filter } from 'unist-util-filter'
18 | import rangeParser from 'parse-numeric-range'
19 |
20 | const getLanguage = (node) => {
21 | const className = node.properties.className
22 | //@ts-ignore
23 | for (const classListItem of className) {
24 | if (classListItem.slice(0, 9) === 'language-') {
25 | return classListItem.slice(9).toLowerCase()
26 | }
27 | }
28 | return null
29 | }
30 |
31 | /**
32 | * @param {import('refractor/lib/core').Refractor} refractor
33 | * @param {string} defaultLanguage
34 | * @return {void}
35 | */
36 | const checkIfLanguageIsRegistered = (refractor, defaultLanguage) => {
37 | if (defaultLanguage && !refractor.registered(defaultLanguage)) {
38 | throw new Error(`The default language "${defaultLanguage}" is not registered with refractor.`)
39 | }
40 | }
41 |
42 | /**
43 | * Create a closure that determines if we have to highlight the given index
44 | *
45 | * @param {string} meta
46 | * @return { (index:number) => boolean }
47 | */
48 | const calculateLinesToHighlight = (meta) => {
49 | const RE = /{([\d,-]+)}/
50 | // Remove space between {} e.g. {1, 3}
51 | const parsedMeta = meta
52 | .split(',')
53 | .map((str) => str.trim())
54 | .join()
55 | if (RE.test(parsedMeta)) {
56 | const strlineNumbers = RE.exec(parsedMeta)[1]
57 | const lineNumbers = rangeParser(strlineNumbers)
58 | return (index) => lineNumbers.includes(index + 1)
59 | } else {
60 | return () => false
61 | }
62 | }
63 |
64 | /**
65 | * Check if we want to start the line numbering from a given number or 1
66 | * showLineNumbers=5, will start the numbering from 5
67 | * @param {string} meta
68 | * @returns {number}
69 | */
70 | const calculateStartingLine = (meta) => {
71 | const RE = /showLineNumbers=(?\d+)/i
72 | // pick the line number after = using a named capturing group
73 | if (RE.test(meta)) {
74 | const {
75 | groups: { lines },
76 | } = RE.exec(meta)
77 | return Number(lines)
78 | }
79 | return 1
80 | }
81 |
82 | /**
83 | * Create container AST for node lines
84 | *
85 | * @param {number} number
86 | * @return {Element[]}
87 | */
88 | const createLineNodes = (number) => {
89 | const a = new Array(number)
90 | for (let i = 0; i < number; i++) {
91 | a[i] = {
92 | type: 'element',
93 | tagName: 'span',
94 | properties: { className: [] },
95 | children: [],
96 | }
97 | }
98 | return a
99 | }
100 |
101 | /**
102 | * Split multiline text nodes into individual nodes with positioning
103 | * Add a node start and end line position information for each text node
104 | *
105 | * @return { (ast:Element['children']) => Element['children'] }
106 | *
107 | */
108 | const addNodePositionClosure = () => {
109 | let startLineNum = 1
110 | /**
111 | * @param {Element['children']} ast
112 | * @return {Element['children']}
113 | */
114 | const addNodePosition = (ast) => {
115 | return ast.reduce((result, node) => {
116 | if (node.type === 'text') {
117 | const value = /** @type {string} */ (node.value)
118 | const numLines = (value.match(/\n/g) || '').length
119 | if (numLines === 0) {
120 | node.position = {
121 | // column: 1 is needed to avoid error with @next/mdx
122 | // https://github.com/timlrx/rehype-prism-plus/issues/44
123 | start: { line: startLineNum, column: 1 },
124 | end: { line: startLineNum, column: 1 },
125 | }
126 | result.push(node)
127 | } else {
128 | const lines = value.split('\n')
129 | for (const [i, line] of lines.entries()) {
130 | result.push({
131 | type: 'text',
132 | value: i === lines.length - 1 ? line : line + '\n',
133 | position: {
134 | start: { line: startLineNum + i, column: 1 },
135 | end: { line: startLineNum + i, column: 1 },
136 | },
137 | })
138 | }
139 | }
140 | startLineNum = startLineNum + numLines
141 |
142 | return result
143 | }
144 |
145 | if (Object.prototype.hasOwnProperty.call(node, 'children')) {
146 | const initialLineNum = startLineNum
147 | // @ts-ignore
148 | node.children = addNodePosition(node.children, startLineNum)
149 | result.push(node)
150 | node.position = {
151 | start: { line: initialLineNum, column: 1 },
152 | end: { line: startLineNum, column: 1 },
153 | }
154 | return result
155 | }
156 |
157 | result.push(node)
158 | return result
159 | }, [])
160 | }
161 | return addNodePosition
162 | }
163 |
164 | /**
165 | * Rehype prism plugin generator that highlights code blocks with refractor (prismjs)
166 | *
167 | * Pass in your own refractor object with the required languages registered:
168 | * https://github.com/wooorm/refractor#refractorregistersyntax
169 | *
170 | * @param {import('refractor/lib/core').Refractor} refractor
171 | * @return {import('unified').Plugin<[Options?], Root>}
172 | */
173 | const rehypePrismGenerator = (refractor) => {
174 | return (options = {}) => {
175 | checkIfLanguageIsRegistered(refractor, options.defaultLanguage)
176 | return (tree) => {
177 | visit(tree, 'element', visitor)
178 | }
179 |
180 | /**
181 | * @param {Element} node
182 | * @param {number} index
183 | * @param {Element} parent
184 | */
185 | function visitor(node, index, parent) {
186 | if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
187 | return
188 | }
189 |
190 | // @ts-ignore meta is a custom code block property
191 | let meta = /** @type {string} */ (node?.data?.meta || node?.properties?.metastring || '')
192 | // Coerce className to array
193 | if (node.properties.className) {
194 | if (typeof node.properties.className === 'boolean') {
195 | node.properties.className = []
196 | } else if (!Array.isArray(node.properties.className)) {
197 | node.properties.className = [node.properties.className]
198 | }
199 | } else {
200 | node.properties.className = []
201 | }
202 |
203 | let lang = getLanguage(node)
204 | // If no language is set on the code block, use defaultLanguage if specified
205 | if (!lang && options.defaultLanguage) {
206 | lang = options.defaultLanguage
207 | node.properties.className.push(`language-${lang}`)
208 | }
209 | node.properties.className.push('code-highlight')
210 |
211 | /** @type {Element} */
212 | let refractorRoot
213 |
214 | // Syntax highlight
215 | if (lang) {
216 | try {
217 | let rootLang
218 | if (lang?.includes('diff-')) {
219 | rootLang = lang.split('-')[1]
220 | } else {
221 | rootLang = lang
222 | }
223 | // @ts-ignore
224 | refractorRoot = refractor.highlight(toString(node), rootLang)
225 | // @ts-ignore className is already an array
226 | parent.properties.className = (parent.properties.className || []).concat(
227 | 'language-' + rootLang
228 | )
229 | } catch (err) {
230 | if (options.ignoreMissing && /Unknown language/.test(err.message)) {
231 | refractorRoot = node
232 | } else {
233 | throw err
234 | }
235 | }
236 | } else {
237 | refractorRoot = node
238 | }
239 |
240 | refractorRoot.children = addNodePositionClosure()(refractorRoot.children)
241 |
242 | // Add position info to root
243 | if (refractorRoot.children.length > 0) {
244 | refractorRoot.position = {
245 | start: { line: refractorRoot.children[0].position.start.line, column: 0 },
246 | end: {
247 | line: refractorRoot.children[refractorRoot.children.length - 1].position.end.line,
248 | column: 0,
249 | },
250 | }
251 | } else {
252 | refractorRoot.position = {
253 | start: { line: 0, column: 0 },
254 | end: { line: 0, column: 0 },
255 | }
256 | }
257 |
258 | const shouldHighlightLine = calculateLinesToHighlight(meta)
259 | const startingLineNumber = calculateStartingLine(meta)
260 | const codeLineArray = createLineNodes(refractorRoot.position.end.line)
261 |
262 | const falseShowLineNumbersStr = [
263 | 'showlinenumbers=false',
264 | 'showlinenumbers="false"',
265 | 'showlinenumbers={false}',
266 | ]
267 | for (const [i, line] of codeLineArray.entries()) {
268 | // Default class name for each line
269 | line.properties.className = ['code-line']
270 |
271 | // Syntax highlight
272 | const treeExtract = filter(
273 | refractorRoot,
274 | (node) => node.position.start.line <= i + 1 && node.position.end.line >= i + 1
275 | )
276 | line.children = treeExtract.children
277 |
278 | // Line number
279 | const isShowNumbers =
280 | (meta.toLowerCase().includes('showLineNumbers'.toLowerCase()) ||
281 | options.showLineNumbers === true ||
282 | (typeof options.showLineNumbers === 'object' &&
283 | options.showLineNumbers.includes(lang))) &&
284 | !falseShowLineNumbersStr.some((str) => meta.toLowerCase().includes(str))
285 |
286 | if (isShowNumbers) {
287 | line.properties.line = [(i + startingLineNumber).toString()]
288 | line.properties.className.push('line-number')
289 | }
290 |
291 | // Line highlight
292 | if (shouldHighlightLine(i)) {
293 | line.properties.className.push('highlight-line')
294 | }
295 |
296 | // Diff classes
297 | if (
298 | (lang === 'diff' || lang?.includes('diff-')) &&
299 | toString(line).substring(0, 1) === '-'
300 | ) {
301 | line.properties.className.push('deleted')
302 | } else if (
303 | (lang === 'diff' || lang?.includes('diff-')) &&
304 | toString(line).substring(0, 1) === '+'
305 | ) {
306 | line.properties.className.push('inserted')
307 | }
308 | }
309 |
310 | // Remove possible trailing line when splitting by \n which results in empty array
311 | if (
312 | codeLineArray.length > 0 &&
313 | toString(codeLineArray[codeLineArray.length - 1]).trim() === ''
314 | ) {
315 | codeLineArray.pop()
316 | }
317 |
318 | node.children = codeLineArray
319 | }
320 | }
321 | }
322 |
323 | export default rehypePrismGenerator
324 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import rehypePrismGenerator from './generator.js'
2 | import rehypePrismCommon from './common.js'
3 | import rehypePrism from './all.js'
4 |
5 | export { rehypePrismGenerator, rehypePrismCommon }
6 | export default rehypePrism
7 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'uvu'
2 | import * as assert from 'uvu/assert'
3 | import { visit } from 'unist-util-visit'
4 | import { rehype } from 'rehype'
5 | import { unified } from 'unified'
6 | import remarkParse from 'remark-parse'
7 | import remarkRehype from 'remark-rehype'
8 | import rehypeStringify from 'rehype-stringify'
9 | import dedent from 'dedent'
10 | import rehypePrism from './src/index.js'
11 |
12 | /**
13 | * Mock meta in code block
14 | */
15 | const addMeta = (metastring) => {
16 | if (!metastring) return
17 | return (tree) => {
18 | visit(tree, 'element', (node, index, parent) => {
19 | if (node.tagName === 'code') {
20 | node.data = { meta: metastring }
21 | }
22 | })
23 | }
24 | }
25 |
26 | const processHtml = (html, options, metastring) => {
27 | return rehype()
28 | .data('settings', { fragment: true })
29 | .use(addMeta, metastring)
30 | .use(rehypePrism, options)
31 | .processSync(html)
32 | .toString()
33 | }
34 |
35 | const processHtmlUnified = (html, options, metastring) => {
36 | return unified()
37 | .use(remarkParse)
38 | .use(remarkRehype, {})
39 | .use(addMeta, metastring)
40 | .use(rehypePrism, options)
41 | .use(rehypeStringify)
42 | .processSync(html)
43 | .toString()
44 | }
45 |
46 | test('adds a code-highlight class to the code and pre tag', () => {
47 | const result = processHtml(dedent`
48 |
49 | `)
50 | const expected = dedent`
`
51 | assert.is(result, expected)
52 | })
53 |
54 | test('add span with class code line for each line', () => {
55 | const result = processHtml(
56 | dedent`
57 | x = 6
58 | `
59 | )
60 | const expected = dedent`x = 6
`
61 | assert.is(result, expected)
62 | })
63 |
64 | test('finds code and highlights', () => {
65 | const result = processHtml(dedent`
66 |
67 | foo
68 | x = 6
69 |
70 | `).trim()
71 | const expected = dedent`
72 |
73 | foo
74 | x = 6
75 |
76 | `
77 | assert.is(result, expected)
78 | })
79 |
80 | test('respects line spacing', () => {
81 | const result = processHtml(dedent`
82 |
83 | x
84 |
85 | y
86 |
87 |
88 | `).trim()
89 | const expected = dedent`
90 |
91 | x
92 |
93 | y
94 |
95 |
96 | `
97 | assert.is(result, expected)
98 | })
99 |
100 | test('handles uppercase correctly', () => {
101 | const result = processHtml(dedent`
102 |
103 | foo
104 | x = 6
105 |
106 | `).trim()
107 | const expected = dedent`
108 |
109 | foo
110 | x = 6
111 |
112 | `
113 | assert.is(result, expected)
114 | })
115 |
116 | test('each line of code should be a separate div', async () => {
117 | const result = processHtml(dedent`
118 |
119 | foo
120 |
121 | x = 6
122 | y = 7
123 |
124 |
125 |
126 | `).trim()
127 | const codeLineCount = (result.match(//g) || []).length
128 | assert.is(codeLineCount, 2)
129 | })
130 |
131 | test('should highlight line', async () => {
132 | const meta = '{1}'
133 | const result = processHtml(
134 | dedent`
135 |
136 |
137 | x = 6
138 | y = 7
139 |
140 |
141 |
142 | `,
143 | {},
144 | meta
145 | ).trim()
146 | const codeHighlightCount = (result.match(//g) || []).length
147 | assert.is(codeHighlightCount, 1)
148 | })
149 |
150 | test('should highlight comma separated lines', async () => {
151 | const meta = '{1,3}'
152 | const result = processHtml(
153 | dedent`
154 |
155 |
156 | x = 6
157 | y = 7
158 | z = 10
159 |
160 |
161 |
162 | `,
163 | {},
164 | meta
165 | ).trim()
166 | const codeHighlightCount = (result.match(//g) || []).length
167 | assert.is(codeHighlightCount, 2)
168 | })
169 |
170 | test('should should parse ranges with a space in between', async () => {
171 | const meta = '{1, 3}'
172 | const result = processHtml(
173 | dedent`
174 |
175 |
176 | x = 6
177 | y = 7
178 | z = 10
179 |
180 |
181 |
182 | `,
183 | {},
184 | meta
185 | ).trim()
186 | const codeHighlightCount = (result.match(//g) || []).length
187 | assert.is(codeHighlightCount, 2)
188 | })
189 |
190 | test('should highlight range separated lines', async () => {
191 | const meta = '{1-3}'
192 | const result = processHtml(
193 | dedent`
194 |
195 |
196 | x = 6
197 | y = 7
198 | z = 10
199 |
200 |
201 |
202 | `,
203 | {},
204 | meta
205 | ).trim()
206 | const codeHighlightCount = (result.match(//g) || []).length
207 | assert.is(codeHighlightCount, 3)
208 | })
209 |
210 | test('showLineNumbers option add line numbers', async () => {
211 | const result = processHtml(
212 | dedent`
213 |
214 |
215 | x = 6
216 | y = 7
217 |
218 |
219 |
220 | `,
221 | { showLineNumbers: true }
222 | ).trim()
223 | assert.ok(result.match(/line="1"/g))
224 | assert.ok(result.match(/line="2"/g))
225 | assert.not(result.match(/line="3"/g))
226 | })
227 |
228 | test('not show line number when showLineNumbers=false', async () => {
229 | const meta = 'showLineNumbers=false'
230 | const result = processHtml(
231 | dedent`
232 |
233 |
234 | x = 6
235 | y = 7
236 |
237 |
238 |
239 | `,
240 | { showLineNumbers: true },
241 | meta
242 | ).trim()
243 | assert.not(result.match(/line="1"/g))
244 | assert.not(result.match(/line="2"/g))
245 | })
246 |
247 | test('show line numbers when showLineNumbers=string[] includes target language', async () => {
248 | const result = processHtml(
249 | dedent`
250 |
251 |
252 | x = 6
253 | y = 7
254 |
255 |
256 |
257 | `,
258 | { showLineNumbers: ['typescript', 'py'] }
259 | ).trim()
260 | assert.ok(result.match(/line="1"/g))
261 | assert.ok(result.match(/line="2"/g))
262 | })
263 |
264 | test('not show line numbers when showLineNumbers=string[] does not include target language', async () => {
265 | const result = processHtml(
266 | dedent`
267 |
268 |
269 | x = 6
270 | y = 7
271 |
272 |
273 |
274 | `,
275 | { showLineNumbers: ['typescript', 'py'] }
276 | ).trim()
277 | assert.not(result.match(/line="1"/g))
278 | assert.not(result.match(/line="2"/g))
279 | })
280 |
281 | test('not show line number when showLineNumbers={false}', async () => {
282 | const meta = 'showLineNumbers={false}'
283 | const result = processHtml(
284 | dedent`
285 |
286 |
287 | x = 6
288 | y = 7
289 |
290 |
291 |
292 | `,
293 | { showLineNumbers: true },
294 | meta
295 | ).trim()
296 | assert.not(result.match(/line="1"/g))
297 | assert.not(result.match(/line="2"/g))
298 | })
299 |
300 | test('showLineNumbers property works in meta field', async () => {
301 | const meta = 'showLineNumbers'
302 | const result = processHtml(
303 | dedent`
304 |
305 |
306 | x = 6
307 | y = 7
308 |
309 |
310 |
311 | `,
312 | {},
313 | meta
314 | ).trim()
315 | assert.ok(result.match(/line="1"/g))
316 | assert.ok(result.match(/line="2"/g))
317 | assert.not(result.match(/line="3"/g))
318 | })
319 |
320 | test('showLineNumbers property with custom index works in meta field', async () => {
321 | const meta = 'showLineNumbers=5'
322 | const result = processHtml(
323 | dedent`
324 |
325 |
326 | x = 6
327 | y = 7
328 |
329 |
330 |
331 | `,
332 | {},
333 | meta
334 | ).trim()
335 | assert.ok(result.match(/line="5"/g))
336 | assert.ok(result.match(/line="6"/g))
337 | assert.not(result.match(/line="7"/g))
338 | })
339 |
340 | test('should support both highlighting and add line number', async () => {
341 | const meta = '{1} showLineNumbers'
342 | const result = processHtml(
343 | dedent`
344 |
345 |
346 | x = 6
347 | y = 7
348 | z = 10
349 |
350 |
351 |
352 | `,
353 | {},
354 | meta
355 | ).trim()
356 | const codeHighlightCount = (result.match(/highlight-line/g) || []).length
357 | assert.is(codeHighlightCount, 1)
358 | assert.ok(result.match(/line="1"/g))
359 | assert.ok(result.match(/line="2"/g))
360 | })
361 |
362 | test('throw error with fake language- class', () => {
363 | assert.throws(
364 | () =>
365 | processHtml(dedent`
366 | x = 6
367 | `),
368 | /Unknown language/
369 | )
370 | })
371 |
372 | test('with options.ignoreMissing, does nothing to code block with fake language- class', () => {
373 | const result = processHtml(
374 | dedent`
375 | x = 6
376 | `,
377 | { ignoreMissing: true }
378 | )
379 | const expected = dedent`x = 6
`
380 | assert.is(result, expected)
381 | })
382 |
383 | test('with options.defaultLanguage, it adds the correct language class tag', () => {
384 | const result = processHtml(
385 | dedent`
386 | x = 6
387 | `,
388 | { defaultLanguage: 'py' }
389 | )
390 | const expected = dedent`x = 6
`
391 | assert.is(result, expected)
392 | })
393 |
394 | test('defaultLanguage should produce the same syntax tree as if manually specified', () => {
395 | const resultDefaultLanguage = processHtml(
396 | dedent`
397 | x = 6
398 | `,
399 | { defaultLanguage: 'py' }
400 | )
401 | const resultManuallySpecified = processHtml(
402 | dedent`
403 | x = 6
404 | `
405 | )
406 | assert.is(resultDefaultLanguage, resultManuallySpecified)
407 | })
408 |
409 | test('throws error if options.defaultLanguage is not registered with refractor', () => {
410 | assert.throws(
411 | () =>
412 | processHtml(
413 | dedent`
414 | x = 6
415 | `,
416 | { defaultLanguage: 'pyzqt' }
417 | ),
418 | /"pyzqt" is not registered with refractor/
419 | )
420 | })
421 |
422 | test('should work with multiline code / comments', () => {
423 | const result = processHtml(
424 | dedent`
425 |
426 | /**
427 | * My comment
428 | */
429 |
430 | `,
431 | { ignoreMissing: true }
432 | )
433 | const expected = dedent`
434 | /**
435 | * My comment
436 | */
437 |
`
438 | assert.is(result, expected)
439 | })
440 |
441 | test('adds inserted or deleted to code-line if lang=diff', async () => {
442 | const result = processHtml(
443 | dedent`
444 |
445 |
446 | + x = 6
447 | - y = 7
448 | z = 10
449 |
450 |
451 |
452 | `
453 | ).trim()
454 | assert.ok(result.includes(``))
455 | assert.ok(result.includes(``))
456 | assert.ok(result.includes(``))
457 | })
458 |
459 | test('works as a remarkjs / unifiedjs plugin', () => {
460 | const result = processHtmlUnified(
461 | dedent`
462 | ~~~jsx
463 |
464 | ~~~
465 | `,
466 | { ignoreMissing: true }
467 | )
468 | const expected = dedent`<Component/>
469 |
`
470 | assert.is(result, expected)
471 | })
472 |
473 | test('diff and code highlighting should work together', () => {
474 | const result = processHtml(
475 | dedent`
476 |
477 | .hello{
478 | - background:url('./urel.png');
479 | + background-image:url('./urel.png');
480 | }
481 |
482 | `,
483 | { ignoreMissing: true }
484 | )
485 | assert.ok(result.includes(``))
486 | assert.ok(result.includes(``))
487 | assert.ok(result.includes(``))
488 | assert.ok(result.includes(``))
489 | })
490 |
491 | test.run()
492 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/*"],
3 | "compilerOptions": {
4 | "target": "ES2020",
5 | "lib": ["ES2020"],
6 | "module": "ES2020",
7 | "moduleResolution": "node",
8 | "outDir": "dist",
9 | "allowJs": true,
10 | "checkJs": true,
11 | "declaration": true,
12 | "emitDeclarationOnly": true,
13 | "allowSyntheticDefaultImports": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------