@scalar/code-highlight 0.3.4 → 0.3.5
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/CHANGELOG.md +6 -0
- package/dist/chunks/code.js +124 -0
- package/dist/chunks/code.js.map +1 -0
- package/dist/chunks/languages.js +12720 -0
- package/dist/chunks/languages.js.map +1 -0
- package/dist/chunks/lib.js +13688 -0
- package/dist/chunks/lib.js.map +1 -0
- package/dist/chunks/lib2.js +731 -0
- package/dist/chunks/lib2.js.map +1 -0
- package/dist/chunks/markdown.js +20921 -0
- package/dist/chunks/markdown.js.map +1 -0
- package/dist/chunks/rehype-alert.js +74 -0
- package/dist/chunks/rehype-alert.js.map +1 -0
- package/dist/chunks/rehype-highlight.js +2987 -0
- package/dist/chunks/rehype-highlight.js.map +1 -0
- package/dist/code/index.d.ts +1 -1
- package/dist/code/index.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -5
- package/dist/languages/index.d.ts +3 -3
- package/dist/languages/index.js +2 -3
- package/dist/markdown/index.d.ts +1 -1
- package/dist/markdown/index.js +3 -1
- package/dist/rehype-alert/index.d.ts +1 -1
- package/dist/rehype-alert/index.js +2 -1
- package/dist/rehype-highlight/index.d.ts +1 -1
- package/dist/rehype-highlight/index.js +2 -1
- package/package.json +4 -4
- package/dist/code/highlight.js +0 -62
- package/dist/code/line-numbers.js +0 -94
- package/dist/constants.js +0 -11
- package/dist/languages/basic.js +0 -30
- package/dist/languages/curl.js +0 -86
- package/dist/languages/json-yaml.js +0 -9
- package/dist/languages/standard.js +0 -115
- package/dist/markdown/markdown.js +0 -228
- package/dist/rehype-alert/rehype-alert.js +0 -102
- package/dist/rehype-highlight/rehype-highlight.js +0 -98
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { visit } from 'unist-util-visit';
|
|
2
|
-
// ---------------------------------------------------------------------------
|
|
3
|
-
// Line Numbering plugin
|
|
4
|
-
function isText(element) {
|
|
5
|
-
return element?.type === 'text';
|
|
6
|
-
}
|
|
7
|
-
function isElement(node) {
|
|
8
|
-
return node?.type === 'element';
|
|
9
|
-
}
|
|
10
|
-
function textElement(value) {
|
|
11
|
-
return { type: 'text', value };
|
|
12
|
-
}
|
|
13
|
-
function lineBreak() {
|
|
14
|
-
return { type: 'text', value: '\n' };
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Adds lines to code blocks
|
|
18
|
-
*/
|
|
19
|
-
export function codeBlockLinesPlugin() {
|
|
20
|
-
return (tree) => {
|
|
21
|
-
visit(tree, 'element', (node, _i, parent) => {
|
|
22
|
-
if (parent?.type === 'element' && parent.tagName === 'pre' && node.tagName === 'code') {
|
|
23
|
-
let numLines = 0;
|
|
24
|
-
// Wraps each line in a span
|
|
25
|
-
node.children = addLines(node);
|
|
26
|
-
// Adds a line break to the end of each line
|
|
27
|
-
node.children.forEach((child) => {
|
|
28
|
-
if (child.type === 'element' && child.tagName === 'span') {
|
|
29
|
-
const lastChild = child.children[child.children.length - 1];
|
|
30
|
-
if (lastChild && (!isText(lastChild) || (isText(lastChild) && !hasLineBreak(lastChild)))) {
|
|
31
|
-
child.children.push(lineBreak());
|
|
32
|
-
numLines++;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
// We need to maintain a count of the total lines to allow space for the labels
|
|
37
|
-
node.properties.style = [`--line-count: ${numLines};`, `--line-digits: ${numLines.toString().length};`];
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
// console.log('NUMBER OF LINES IS: ', numLines)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Adds lines to a node recursively and returns them
|
|
45
|
-
*
|
|
46
|
-
* @param node - The node to add lines to
|
|
47
|
-
* @param lines - The current lines
|
|
48
|
-
* @param copyParent - Whether to copy the parent node to save the original node styles
|
|
49
|
-
*/
|
|
50
|
-
function addLines(node, lines = [], copyParent) {
|
|
51
|
-
const line = () => lines[lines.length - 1] ?? ((lines.push(createLine()) && lines[lines.length - 1]) || undefined);
|
|
52
|
-
node.children.forEach((child) => {
|
|
53
|
-
if (isText(child) && hasLineBreak(child)) {
|
|
54
|
-
const split = child.value.split(/\n/);
|
|
55
|
-
split.forEach((content, i) => {
|
|
56
|
-
if (copyParent) {
|
|
57
|
-
line()?.children.push({ ...node, children: [textElement(content)] });
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
line()?.children.push(textElement(content));
|
|
61
|
-
}
|
|
62
|
-
i !== split.length - 1 && lines.push(createLine());
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
else if (isElement(child) && child.children.some(hasLineBreak)) {
|
|
66
|
-
addLines(child, lines, true);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
line()?.children.push(child);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
return lines;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Creates a new line element
|
|
76
|
-
*
|
|
77
|
-
* @param children - The children the line should have initially
|
|
78
|
-
*/
|
|
79
|
-
function createLine(...children) {
|
|
80
|
-
return {
|
|
81
|
-
type: 'element',
|
|
82
|
-
tagName: 'span',
|
|
83
|
-
properties: { class: ['line'] },
|
|
84
|
-
children,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Checks if a node has a line break
|
|
89
|
-
*
|
|
90
|
-
* @param node - The node to check
|
|
91
|
-
*/
|
|
92
|
-
function hasLineBreak(node) {
|
|
93
|
-
return (isText(node) && /\r?\n/.test(node.value)) || (isElement(node) && node.children.some(hasLineBreak));
|
|
94
|
-
}
|
package/dist/constants.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** Map common markdown language shortcuts to lowlight languages */
|
|
2
|
-
export const lowlightLanguageMappings = {
|
|
3
|
-
'ts': 'typescript',
|
|
4
|
-
'js': 'javascript',
|
|
5
|
-
'py': 'python',
|
|
6
|
-
'py3': 'python',
|
|
7
|
-
'c#': 'csharp',
|
|
8
|
-
'c++': 'cpp',
|
|
9
|
-
'node': 'javascript',
|
|
10
|
-
};
|
|
11
|
-
//
|
package/dist/languages/basic.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import bash from 'highlight.js/lib/languages/bash';
|
|
2
|
-
import css from 'highlight.js/lib/languages/css';
|
|
3
|
-
import javascript from 'highlight.js/lib/languages/javascript';
|
|
4
|
-
import json from 'highlight.js/lib/languages/json';
|
|
5
|
-
import less from 'highlight.js/lib/languages/less';
|
|
6
|
-
import markdown from 'highlight.js/lib/languages/markdown';
|
|
7
|
-
import plaintext from 'highlight.js/lib/languages/plaintext';
|
|
8
|
-
import python from 'highlight.js/lib/languages/python';
|
|
9
|
-
import scss from 'highlight.js/lib/languages/scss';
|
|
10
|
-
import shell from 'highlight.js/lib/languages/shell';
|
|
11
|
-
import typescript from 'highlight.js/lib/languages/typescript';
|
|
12
|
-
import xml from 'highlight.js/lib/languages/xml';
|
|
13
|
-
import yaml from 'highlight.js/lib/languages/yaml';
|
|
14
|
-
const basicLanguages = {
|
|
15
|
-
bash,
|
|
16
|
-
css,
|
|
17
|
-
html: xml,
|
|
18
|
-
javascript,
|
|
19
|
-
json,
|
|
20
|
-
less,
|
|
21
|
-
markdown,
|
|
22
|
-
plaintext,
|
|
23
|
-
python,
|
|
24
|
-
scss,
|
|
25
|
-
shell,
|
|
26
|
-
typescript,
|
|
27
|
-
xml,
|
|
28
|
-
yaml,
|
|
29
|
-
};
|
|
30
|
-
export { basicLanguages };
|
package/dist/languages/curl.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cURL grammar adapted from highlightjs-curl to avoid a CJS dependency.
|
|
3
|
-
*/
|
|
4
|
-
const curlLanguage = (hljs) => {
|
|
5
|
-
const quoteString = {
|
|
6
|
-
className: 'string',
|
|
7
|
-
begin: /"/,
|
|
8
|
-
end: /"/,
|
|
9
|
-
contains: [
|
|
10
|
-
hljs.BACKSLASH_ESCAPE,
|
|
11
|
-
{
|
|
12
|
-
className: 'variable',
|
|
13
|
-
begin: /\$\(/,
|
|
14
|
-
end: /\)/,
|
|
15
|
-
contains: [hljs.BACKSLASH_ESCAPE],
|
|
16
|
-
},
|
|
17
|
-
],
|
|
18
|
-
relevance: 0,
|
|
19
|
-
};
|
|
20
|
-
const optionRequest = {
|
|
21
|
-
className: 'literal',
|
|
22
|
-
begin: /(--request|-X)\s/,
|
|
23
|
-
contains: [
|
|
24
|
-
{
|
|
25
|
-
className: 'symbol',
|
|
26
|
-
begin: /(get|post|delete|options|head|put|patch|trace|connect)/,
|
|
27
|
-
end: /\s/,
|
|
28
|
-
returnEnd: true,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
returnEnd: true,
|
|
32
|
-
relevance: 10,
|
|
33
|
-
};
|
|
34
|
-
const option = {
|
|
35
|
-
className: 'literal',
|
|
36
|
-
begin: /--/,
|
|
37
|
-
end: /[\s"]/,
|
|
38
|
-
returnEnd: true,
|
|
39
|
-
relevance: 0,
|
|
40
|
-
};
|
|
41
|
-
const optionSingle = {
|
|
42
|
-
className: 'literal',
|
|
43
|
-
begin: /-\w/,
|
|
44
|
-
end: /[\s"]/,
|
|
45
|
-
returnEnd: true,
|
|
46
|
-
relevance: 0,
|
|
47
|
-
};
|
|
48
|
-
const escapedQuote = {
|
|
49
|
-
className: 'string',
|
|
50
|
-
begin: /\\"/,
|
|
51
|
-
relevance: 0,
|
|
52
|
-
};
|
|
53
|
-
const aposString = {
|
|
54
|
-
className: 'string',
|
|
55
|
-
begin: /'/,
|
|
56
|
-
end: /'/,
|
|
57
|
-
relevance: 0,
|
|
58
|
-
};
|
|
59
|
-
const number = {
|
|
60
|
-
className: 'number',
|
|
61
|
-
variants: [{ begin: hljs.C_NUMBER_RE }],
|
|
62
|
-
relevance: 0,
|
|
63
|
-
};
|
|
64
|
-
const pathMode = {
|
|
65
|
-
match: /(\/[a-z._-]+)+/,
|
|
66
|
-
};
|
|
67
|
-
return {
|
|
68
|
-
name: 'curl',
|
|
69
|
-
aliases: ['curl'],
|
|
70
|
-
keywords: 'curl',
|
|
71
|
-
case_insensitive: true,
|
|
72
|
-
contains: [
|
|
73
|
-
optionRequest,
|
|
74
|
-
option,
|
|
75
|
-
optionSingle,
|
|
76
|
-
quoteString,
|
|
77
|
-
escapedQuote,
|
|
78
|
-
aposString,
|
|
79
|
-
hljs.APOS_STRING_MODE,
|
|
80
|
-
hljs.QUOTE_STRING_MODE,
|
|
81
|
-
number,
|
|
82
|
-
pathMode,
|
|
83
|
-
],
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
export default curlLanguage;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import json from 'highlight.js/lib/languages/json';
|
|
2
|
-
import plaintext from 'highlight.js/lib/languages/plaintext';
|
|
3
|
-
import yaml from 'highlight.js/lib/languages/yaml';
|
|
4
|
-
const jsonYamlLanguages = {
|
|
5
|
-
json,
|
|
6
|
-
plaintext,
|
|
7
|
-
yaml,
|
|
8
|
-
};
|
|
9
|
-
export { jsonYamlLanguages };
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import bash from 'highlight.js/lib/languages/bash';
|
|
2
|
-
import c from 'highlight.js/lib/languages/c';
|
|
3
|
-
import clojure from 'highlight.js/lib/languages/clojure';
|
|
4
|
-
import cpp from 'highlight.js/lib/languages/cpp';
|
|
5
|
-
import csharp from 'highlight.js/lib/languages/csharp';
|
|
6
|
-
import css from 'highlight.js/lib/languages/css';
|
|
7
|
-
import dart from 'highlight.js/lib/languages/dart';
|
|
8
|
-
import diff from 'highlight.js/lib/languages/diff';
|
|
9
|
-
import dockerfile from 'highlight.js/lib/languages/dockerfile';
|
|
10
|
-
import elixir from 'highlight.js/lib/languages/elixir';
|
|
11
|
-
import fsharp from 'highlight.js/lib/languages/fsharp';
|
|
12
|
-
import go from 'highlight.js/lib/languages/go';
|
|
13
|
-
import graphql from 'highlight.js/lib/languages/graphql';
|
|
14
|
-
import haskell from 'highlight.js/lib/languages/haskell';
|
|
15
|
-
import http from 'highlight.js/lib/languages/http';
|
|
16
|
-
import ini from 'highlight.js/lib/languages/ini';
|
|
17
|
-
import java from 'highlight.js/lib/languages/java';
|
|
18
|
-
import javascript from 'highlight.js/lib/languages/javascript';
|
|
19
|
-
import json from 'highlight.js/lib/languages/json';
|
|
20
|
-
import kotlin from 'highlight.js/lib/languages/kotlin';
|
|
21
|
-
import less from 'highlight.js/lib/languages/less';
|
|
22
|
-
import lua from 'highlight.js/lib/languages/lua';
|
|
23
|
-
import makefile from 'highlight.js/lib/languages/makefile';
|
|
24
|
-
import markdown from 'highlight.js/lib/languages/markdown';
|
|
25
|
-
import matlab from 'highlight.js/lib/languages/matlab';
|
|
26
|
-
import nginx from 'highlight.js/lib/languages/nginx';
|
|
27
|
-
import objectivec from 'highlight.js/lib/languages/objectivec';
|
|
28
|
-
import ocaml from 'highlight.js/lib/languages/ocaml';
|
|
29
|
-
import perl from 'highlight.js/lib/languages/perl';
|
|
30
|
-
import php from 'highlight.js/lib/languages/php';
|
|
31
|
-
import plaintext from 'highlight.js/lib/languages/plaintext';
|
|
32
|
-
import powershell from 'highlight.js/lib/languages/powershell';
|
|
33
|
-
import properties from 'highlight.js/lib/languages/properties';
|
|
34
|
-
import python from 'highlight.js/lib/languages/python';
|
|
35
|
-
import r from 'highlight.js/lib/languages/r';
|
|
36
|
-
import ruby from 'highlight.js/lib/languages/ruby';
|
|
37
|
-
import rust from 'highlight.js/lib/languages/rust';
|
|
38
|
-
import scala from 'highlight.js/lib/languages/scala';
|
|
39
|
-
import scss from 'highlight.js/lib/languages/scss';
|
|
40
|
-
import shell from 'highlight.js/lib/languages/shell';
|
|
41
|
-
import sql from 'highlight.js/lib/languages/sql';
|
|
42
|
-
import swift from 'highlight.js/lib/languages/swift';
|
|
43
|
-
import typescript from 'highlight.js/lib/languages/typescript';
|
|
44
|
-
import xml from 'highlight.js/lib/languages/xml';
|
|
45
|
-
import yaml from 'highlight.js/lib/languages/yaml';
|
|
46
|
-
import curl from './curl.js';
|
|
47
|
-
/**
|
|
48
|
-
* We group languages into three categories based on their popularity and usage.
|
|
49
|
-
* This helps in optimizing the bundle size by allowing users to include only
|
|
50
|
-
* the languages they need.
|
|
51
|
-
*
|
|
52
|
-
* 1. Standard Languages: These are the most popular languages that cover the
|
|
53
|
-
* majority of use cases.
|
|
54
|
-
* 2. Medium Languages: These languages are not as popular as the ones in
|
|
55
|
-
* `standardLanguages`, but still have a decent amount of users.
|
|
56
|
-
* 3. Specialized Languages: These languages are more specialized and have a
|
|
57
|
-
* smaller user base.
|
|
58
|
-
*
|
|
59
|
-
* Each category is represented as a separate object, making it easy to import
|
|
60
|
-
* only the languages you need.
|
|
61
|
-
*/
|
|
62
|
-
/**
|
|
63
|
-
* These are the most popular languages that cover the majority of use cases.
|
|
64
|
-
*/
|
|
65
|
-
export const standardLanguages = {
|
|
66
|
-
bash,
|
|
67
|
-
c,
|
|
68
|
-
clojure,
|
|
69
|
-
cpp,
|
|
70
|
-
csharp,
|
|
71
|
-
css,
|
|
72
|
-
curl,
|
|
73
|
-
dart,
|
|
74
|
-
diff,
|
|
75
|
-
docker: dockerfile,
|
|
76
|
-
dockerfile,
|
|
77
|
-
elixir,
|
|
78
|
-
fsharp,
|
|
79
|
-
go,
|
|
80
|
-
graphql,
|
|
81
|
-
haskell,
|
|
82
|
-
html: xml,
|
|
83
|
-
http,
|
|
84
|
-
ini,
|
|
85
|
-
java,
|
|
86
|
-
javascript,
|
|
87
|
-
json,
|
|
88
|
-
kotlin,
|
|
89
|
-
less,
|
|
90
|
-
lua,
|
|
91
|
-
makefile,
|
|
92
|
-
markdown,
|
|
93
|
-
matlab,
|
|
94
|
-
nginx,
|
|
95
|
-
objectivec,
|
|
96
|
-
ocaml,
|
|
97
|
-
perl,
|
|
98
|
-
php,
|
|
99
|
-
plaintext,
|
|
100
|
-
powershell,
|
|
101
|
-
properties,
|
|
102
|
-
python,
|
|
103
|
-
r,
|
|
104
|
-
ruby,
|
|
105
|
-
rust,
|
|
106
|
-
scala,
|
|
107
|
-
scss,
|
|
108
|
-
shell,
|
|
109
|
-
sql,
|
|
110
|
-
swift,
|
|
111
|
-
toml: ini,
|
|
112
|
-
typescript,
|
|
113
|
-
xml,
|
|
114
|
-
yaml,
|
|
115
|
-
};
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import rehypeExternalLinks from 'rehype-external-links';
|
|
2
|
-
import rehypeFormat from 'rehype-format';
|
|
3
|
-
import rehypeParse from 'rehype-parse';
|
|
4
|
-
import rehypeRaw from 'rehype-raw';
|
|
5
|
-
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
|
6
|
-
import rehypeStringify from 'rehype-stringify';
|
|
7
|
-
import remarkGfm from 'remark-gfm';
|
|
8
|
-
import remarkParse from 'remark-parse';
|
|
9
|
-
import remarkRehype from 'remark-rehype';
|
|
10
|
-
import remarkStringify from 'remark-stringify';
|
|
11
|
-
import { unified } from 'unified';
|
|
12
|
-
import { SKIP, visit } from 'unist-util-visit';
|
|
13
|
-
import { standardLanguages } from '../languages/index.js';
|
|
14
|
-
import { rehypeAlert } from '../rehype-alert/index.js';
|
|
15
|
-
import { rehypeHighlight } from '../rehype-highlight/index.js';
|
|
16
|
-
/**
|
|
17
|
-
* Type-guard to check if a node is a heading.
|
|
18
|
-
*/
|
|
19
|
-
export const isHeading = (node) => {
|
|
20
|
-
return node.type === 'heading' && 'depth' in node && 'children' in node;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Plugin to transform nodes in a Markdown AST
|
|
24
|
-
*/
|
|
25
|
-
const transformNodes = (options, ..._ignored) => (tree) => {
|
|
26
|
-
if (!options?.transform || !options?.type) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
visit(tree, options?.type, (node) => {
|
|
30
|
-
options?.transform ? options?.transform(node) : node;
|
|
31
|
-
return SKIP;
|
|
32
|
-
});
|
|
33
|
-
return;
|
|
34
|
-
};
|
|
35
|
-
const TAGS_WITH_INLINE_MARKDOWN = new Set(['dd', 'dt', 'li', 'p', 'summary', 'td', 'th']);
|
|
36
|
-
const MAY_CONTAIN_INLINE_MARKDOWN = /[`*_\[~]/;
|
|
37
|
-
/**
|
|
38
|
-
* Preserve HTML-like text in inline markdown by turning mdast `html` nodes into text nodes.
|
|
39
|
-
*/
|
|
40
|
-
const preserveHtmlLikeText = () => (tree) => {
|
|
41
|
-
visit(tree, 'html', (node, index, parent) => {
|
|
42
|
-
if (typeof index !== 'number' || !parent || !('children' in parent) || !Array.isArray(parent.children)) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
parent.children[index] = {
|
|
46
|
-
type: 'text',
|
|
47
|
-
value: node.value ?? '',
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
const inlineMarkdownProcessor = unified().use(remarkParse).use(remarkGfm).use(preserveHtmlLikeText).use(remarkRehype);
|
|
52
|
-
const htmlFragmentParser = unified().use(rehypeParse, { fragment: true });
|
|
53
|
-
const htmlFragmentStringifier = unified().use(rehypeStringify);
|
|
54
|
-
/**
|
|
55
|
-
* Parse inline markdown and return children from the generated paragraph.
|
|
56
|
-
*/
|
|
57
|
-
const extractInlineChildrenFromMarkdown = (value) => {
|
|
58
|
-
const tree = inlineMarkdownProcessor.runSync(inlineMarkdownProcessor.parse(value));
|
|
59
|
-
if (tree.children.length !== 1) {
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
const paragraph = tree.children.at(0);
|
|
63
|
-
if (!paragraph || paragraph.type !== 'element' || paragraph.tagName !== 'p') {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
return paragraph.children;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Re-parses text nodes in selected HTML tags so inline markdown works in tags like <p>.
|
|
70
|
-
*/
|
|
71
|
-
const transformInlineMarkdownInHtml = () => (tree) => {
|
|
72
|
-
visit(tree, 'element', (node) => {
|
|
73
|
-
if (!TAGS_WITH_INLINE_MARKDOWN.has(node.tagName)) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
node.children = node.children.flatMap((child) => {
|
|
77
|
-
if (child.type !== 'text' || !MAY_CONTAIN_INLINE_MARKDOWN.test(child.value)) {
|
|
78
|
-
return [child];
|
|
79
|
-
}
|
|
80
|
-
const markdownChildren = extractInlineChildrenFromMarkdown(child.value);
|
|
81
|
-
return markdownChildren.length ? markdownChildren : [child];
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* Rewrites raw HTML strings so inline markdown parsing is only applied to raw HTML input.
|
|
87
|
-
*/
|
|
88
|
-
const transformInlineMarkdownInRawHtml = () => (tree) => {
|
|
89
|
-
visit(tree, 'raw', (node) => {
|
|
90
|
-
if (typeof node.value !== 'string' || !MAY_CONTAIN_INLINE_MARKDOWN.test(node.value)) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const htmlFragmentTree = htmlFragmentParser.parse(node.value);
|
|
94
|
-
transformInlineMarkdownInHtml()(htmlFragmentTree);
|
|
95
|
-
node.value = htmlFragmentStringifier.stringify(htmlFragmentTree);
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
/**
|
|
99
|
-
* Take a Markdown string and generate HTML from it
|
|
100
|
-
*/
|
|
101
|
-
export function htmlFromMarkdown(markdown, options) {
|
|
102
|
-
// Add permitted tags and remove stripped ones
|
|
103
|
-
const removeTags = options?.removeTags ?? [];
|
|
104
|
-
const tagNames = [...(defaultSchema.tagNames ?? []), ...(options?.allowTags ?? [])].filter((t) => !removeTags.includes(t));
|
|
105
|
-
const html = unified()
|
|
106
|
-
// Parses markdown
|
|
107
|
-
.use(remarkParse)
|
|
108
|
-
// Support autolink literals, footnotes, strikethrough, tables and tasklists
|
|
109
|
-
.use(remarkGfm)
|
|
110
|
-
.use(transformNodes, {
|
|
111
|
-
transform: options?.transform,
|
|
112
|
-
type: options?.transformType,
|
|
113
|
-
})
|
|
114
|
-
// Allows any HTML tags
|
|
115
|
-
.use(remarkRehype, { allowDangerousHtml: true })
|
|
116
|
-
// Adds GitHub alerts
|
|
117
|
-
.use(rehypeAlert)
|
|
118
|
-
// Parse inline markdown only inside raw HTML fragments, not normal markdown output
|
|
119
|
-
.use(transformInlineMarkdownInRawHtml)
|
|
120
|
-
// Creates an HTML AST
|
|
121
|
-
.use(rehypeRaw)
|
|
122
|
-
// Removes disallowed tags
|
|
123
|
-
.use(rehypeSanitize, {
|
|
124
|
-
...defaultSchema,
|
|
125
|
-
// Don't prefix the heading ids
|
|
126
|
-
clobberPrefix: '',
|
|
127
|
-
// Makes it even more strict
|
|
128
|
-
tagNames,
|
|
129
|
-
attributes: {
|
|
130
|
-
...defaultSchema.attributes,
|
|
131
|
-
abbr: ['title'],
|
|
132
|
-
// Allow all class names while preserving the existing default attributes
|
|
133
|
-
'*': [...(defaultSchema.attributes?.['*'] ?? []), 'className'],
|
|
134
|
-
},
|
|
135
|
-
// Strip content of dangerous elements, not just the tags
|
|
136
|
-
strip: ['script', 'style', 'object', 'embed', 'form'],
|
|
137
|
-
})
|
|
138
|
-
// Syntax highlighting
|
|
139
|
-
.use(rehypeHighlight, {
|
|
140
|
-
languages: standardLanguages,
|
|
141
|
-
// Enable auto detection
|
|
142
|
-
detect: true,
|
|
143
|
-
})
|
|
144
|
-
// Adds target="_blank" to external links
|
|
145
|
-
.use(rehypeExternalLinks, { target: '_blank' })
|
|
146
|
-
// Formats the HTML
|
|
147
|
-
.use(rehypeFormat)
|
|
148
|
-
// Converts the HTML AST to a string
|
|
149
|
-
.use(rehypeStringify)
|
|
150
|
-
// Run the pipeline
|
|
151
|
-
.processSync(markdown);
|
|
152
|
-
return html.toString();
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Create a Markdown AST from a string.
|
|
156
|
-
*/
|
|
157
|
-
function getMarkdownAst(markdown) {
|
|
158
|
-
return unified().use(remarkParse).use(remarkGfm).parse(markdown);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Find all headings of a specific type in a Markdown AST.
|
|
162
|
-
*/
|
|
163
|
-
export function getHeadings(markdown, depth = 1) {
|
|
164
|
-
const tree = getMarkdownAst(markdown);
|
|
165
|
-
const nodes = [];
|
|
166
|
-
visit(tree, 'heading', (node) => {
|
|
167
|
-
const text = textFromNode(node);
|
|
168
|
-
if (text) {
|
|
169
|
-
nodes.push({ depth: node.depth ?? depth, value: text });
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
return nodes;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Extract plain text from a Markdown AST node (recursively).
|
|
176
|
-
*
|
|
177
|
-
* Handles headings with nested phrasing content such as links.
|
|
178
|
-
*/
|
|
179
|
-
export function textFromNode(node) {
|
|
180
|
-
if (node.type === 'text') {
|
|
181
|
-
return node.value ?? '';
|
|
182
|
-
}
|
|
183
|
-
if ('children' in node && Array.isArray(node.children)) {
|
|
184
|
-
return node.children.map((child) => textFromNode(child)).join('');
|
|
185
|
-
}
|
|
186
|
-
return '';
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Return multiple Markdown documents. Every heading should be its own document.
|
|
190
|
-
*/
|
|
191
|
-
export function splitContent(markdown) {
|
|
192
|
-
const tree = getMarkdownAst(markdown);
|
|
193
|
-
/** Sections */
|
|
194
|
-
const sections = [];
|
|
195
|
-
/** Nodes inside a section */
|
|
196
|
-
let nodes = [];
|
|
197
|
-
tree.children?.forEach((node) => {
|
|
198
|
-
// If the node is a heading, start a new section
|
|
199
|
-
if (node.type === 'heading') {
|
|
200
|
-
if (nodes.length) {
|
|
201
|
-
sections.push(nodes);
|
|
202
|
-
}
|
|
203
|
-
sections.push([node]);
|
|
204
|
-
nodes = [];
|
|
205
|
-
}
|
|
206
|
-
// Otherwise, add the node to the current section
|
|
207
|
-
else {
|
|
208
|
-
nodes.push(node);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
// Add any remaining nodes
|
|
212
|
-
if (nodes.length) {
|
|
213
|
-
sections.push(nodes);
|
|
214
|
-
}
|
|
215
|
-
return sections.map((section) => createDocument(section));
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Use remark to create a Markdown document from a list of nodes.
|
|
219
|
-
*/
|
|
220
|
-
function createDocument(nodes) {
|
|
221
|
-
// Create the Markdown string
|
|
222
|
-
const markdown = unified().use(remarkStringify).use(remarkGfm).stringify({
|
|
223
|
-
type: 'root',
|
|
224
|
-
children: nodes,
|
|
225
|
-
});
|
|
226
|
-
// Remove the whitespace
|
|
227
|
-
return markdown.trim();
|
|
228
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { visit } from 'unist-util-visit';
|
|
2
|
-
const ALERT_TYPES = ['note', 'tip', 'important', 'warning', 'caution', 'success'];
|
|
3
|
-
// Simple whitespace check function
|
|
4
|
-
function isWhitespace(node) {
|
|
5
|
-
return node.type === 'text' && typeof node.value === 'string' && /^\s*$/.test(node.value);
|
|
6
|
-
}
|
|
7
|
-
export function rehypeAlert() {
|
|
8
|
-
return (tree) => {
|
|
9
|
-
visit(tree, 'element', (node, index, parent) => {
|
|
10
|
-
if (node.tagName !== 'blockquote' || typeof index !== 'number' || !parent || parent.type !== 'root') {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
// Find the first non-whitespace child
|
|
14
|
-
const headIndex = node.children.findIndex((child) => !isWhitespace(child));
|
|
15
|
-
if (headIndex === -1) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
const head = node.children[headIndex];
|
|
19
|
-
if (!head || head.type !== 'element' || head.tagName !== 'p') {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const text = head.children[0];
|
|
23
|
-
if (!text || text.type !== 'text' || !text.value.startsWith('[!')) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const end = text.value.indexOf(']');
|
|
27
|
-
if (end === -1) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
// Extract the alert type
|
|
31
|
-
const alertType = text.value.slice(2, end).toLowerCase();
|
|
32
|
-
if (!ALERT_TYPES.includes(alertType)) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
// Remove the blockquote if it's empty
|
|
36
|
-
if (end + 1 === text.value.length) {
|
|
37
|
-
const next = head.children[1];
|
|
38
|
-
if (next) {
|
|
39
|
-
if (next.type !== 'element' || next.tagName !== 'br') {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (!head.children[2]) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
head.children = head.children.slice(2);
|
|
46
|
-
const node = head.children[0];
|
|
47
|
-
if (node && node.type === 'text' && node.value.charAt(0) === '\n') {
|
|
48
|
-
node.value = node.value.slice(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
const skipped = headIndex + 1 < node.children.length && isWhitespace(node.children[headIndex + 1]);
|
|
53
|
-
const nextIndex = skipped ? headIndex + 2 : headIndex + 1;
|
|
54
|
-
if (nextIndex >= node.children.length || node.children[nextIndex]?.type !== 'element') {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
node.children = node.children.slice(nextIndex);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else if (text.value.charAt(end + 1) === '\n' &&
|
|
61
|
-
// Check if the next character is a newline or a non-whitespace character
|
|
62
|
-
(end + 2 === text.value.length || !/^\s*$/.test(text.value.slice(end + 2)))) {
|
|
63
|
-
text.value = text.value.slice(end + 2);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// Remove the alert marker if it's not followed by a newline or a non-whitespace character
|
|
67
|
-
text.value = text.value.replace(/^\s*\[!.*?\]\s*/, '');
|
|
68
|
-
}
|
|
69
|
-
// Extract content from paragraphs to avoid wrapping in <p> tags
|
|
70
|
-
const contentChildren = [];
|
|
71
|
-
for (let i = headIndex; i < node.children.length; i++) {
|
|
72
|
-
const child = node.children[i];
|
|
73
|
-
if (child?.type === 'element' && child.tagName === 'p' && child.children) {
|
|
74
|
-
contentChildren.push(...child.children);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
contentChildren.push(child);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Replace the blockquote with a div containing the alert
|
|
81
|
-
parent.children[index] = {
|
|
82
|
-
type: 'element',
|
|
83
|
-
tagName: 'div',
|
|
84
|
-
properties: { className: ['markdown-alert', `markdown-alert-${alertType}`] },
|
|
85
|
-
children: [
|
|
86
|
-
{
|
|
87
|
-
type: 'element',
|
|
88
|
-
tagName: 'div',
|
|
89
|
-
properties: { className: ['markdown-alert-icon'] },
|
|
90
|
-
children: [],
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: 'element',
|
|
94
|
-
tagName: 'div',
|
|
95
|
-
properties: { className: ['markdown-alert-content'] },
|
|
96
|
-
children: [{ type: 'text', value: ' ' }, ...contentChildren],
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
};
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
}
|