@scalar/code-highlight 0.2.4 → 0.3.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/CHANGELOG.md +12 -0
- package/dist/code/highlight.js +58 -38
- package/dist/code/index.js +1 -5
- package/dist/code/line-numbers.js +76 -51
- package/dist/constants.js +10 -12
- package/dist/index.js +5 -14
- package/dist/languages/basic.js +28 -31
- package/dist/languages/index.js +3 -9
- package/dist/languages/json-yaml.js +7 -10
- package/dist/languages/standard.js +115 -100
- package/dist/markdown/index.js +1 -2
- package/dist/markdown/markdown.js +148 -100
- package/dist/rehype-alert/index.js +1 -5
- package/dist/rehype-alert/rehype-alert.js +99 -91
- package/dist/rehype-highlight/index.js +1 -5
- package/dist/rehype-highlight/rehype-highlight.js +93 -85
- package/package.json +6 -12
- package/dist/code/highlight.js.map +0 -7
- package/dist/code/index.js.map +0 -7
- package/dist/code/line-numbers.js.map +0 -7
- package/dist/constants.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/languages/basic.js.map +0 -7
- package/dist/languages/index.js.map +0 -7
- package/dist/languages/json-yaml.js.map +0 -7
- package/dist/languages/standard.js.map +0 -7
- package/dist/markdown/index.js.map +0 -7
- package/dist/markdown/markdown.js.map +0 -7
- package/dist/rehype-alert/index.js.map +0 -7
- package/dist/rehype-alert/rehype-alert.js.map +0 -7
- package/dist/rehype-highlight/index.js.map +0 -7
- package/dist/rehype-highlight/rehype-highlight.js.map +0 -7
|
@@ -1,114 +1,162 @@
|
|
|
1
|
-
import rehypeExternalLinks from
|
|
2
|
-
import rehypeFormat from
|
|
3
|
-
import rehypeRaw from
|
|
4
|
-
import rehypeSanitize, { defaultSchema } from
|
|
5
|
-
import rehypeStringify from
|
|
6
|
-
import remarkGfm from
|
|
7
|
-
import remarkParse from
|
|
8
|
-
import remarkRehype from
|
|
9
|
-
import remarkStringify from
|
|
10
|
-
import { unified } from
|
|
11
|
-
import { SKIP, visit } from
|
|
12
|
-
import { standardLanguages } from
|
|
13
|
-
import { rehypeAlert } from
|
|
14
|
-
import { rehypeHighlight } from
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import rehypeExternalLinks from 'rehype-external-links';
|
|
2
|
+
import rehypeFormat from 'rehype-format';
|
|
3
|
+
import rehypeRaw from 'rehype-raw';
|
|
4
|
+
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
|
5
|
+
import rehypeStringify from 'rehype-stringify';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import remarkParse from 'remark-parse';
|
|
8
|
+
import remarkRehype from 'remark-rehype';
|
|
9
|
+
import remarkStringify from 'remark-stringify';
|
|
10
|
+
import { unified } from 'unified';
|
|
11
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
12
|
+
import { standardLanguages } from '../languages/index.js';
|
|
13
|
+
import { rehypeAlert } from '../rehype-alert/index.js';
|
|
14
|
+
import { rehypeHighlight } from '../rehype-highlight/index.js';
|
|
15
|
+
/**
|
|
16
|
+
* Type-guard to check if a node is a heading.
|
|
17
|
+
*/
|
|
18
|
+
export const isHeading = (node) => {
|
|
19
|
+
return node.type === 'heading' && 'depth' in node && 'children' in node;
|
|
17
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Plugin to transform nodes in a Markdown AST
|
|
23
|
+
*/
|
|
18
24
|
const transformNodes = (options, ..._ignored) => (tree) => {
|
|
19
|
-
|
|
25
|
+
if (!options?.transform || !options?.type) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
visit(tree, options?.type, (node) => {
|
|
29
|
+
options?.transform ? options?.transform(node) : node;
|
|
30
|
+
return SKIP;
|
|
31
|
+
});
|
|
20
32
|
return;
|
|
21
|
-
}
|
|
22
|
-
visit(tree, options?.type, (node) => {
|
|
23
|
-
options?.transform ? options?.transform(node) : node;
|
|
24
|
-
return SKIP;
|
|
25
|
-
});
|
|
26
|
-
return;
|
|
27
33
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Take a Markdown string and generate HTML from it
|
|
36
|
+
*/
|
|
37
|
+
export function htmlFromMarkdown(markdown, options) {
|
|
38
|
+
// Add permitted tags and remove stripped ones
|
|
39
|
+
const removeTags = options?.removeTags ?? [];
|
|
40
|
+
const tagNames = [...(defaultSchema.tagNames ?? []), ...(options?.allowTags ?? [])].filter((t) => !removeTags.includes(t));
|
|
41
|
+
const html = unified()
|
|
42
|
+
// Parses markdown
|
|
43
|
+
.use(remarkParse)
|
|
44
|
+
// Support autolink literals, footnotes, strikethrough, tables and tasklists
|
|
45
|
+
.use(remarkGfm)
|
|
46
|
+
.use(transformNodes, {
|
|
47
|
+
transform: options?.transform,
|
|
48
|
+
type: options?.transformType,
|
|
49
|
+
})
|
|
50
|
+
// Allows any HTML tags
|
|
51
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
52
|
+
// Adds GitHub alerts
|
|
53
|
+
.use(rehypeAlert)
|
|
54
|
+
// Creates a HTML AST
|
|
55
|
+
.use(rehypeRaw)
|
|
56
|
+
// Removes disallowed tags
|
|
57
|
+
.use(rehypeSanitize, {
|
|
58
|
+
...defaultSchema,
|
|
59
|
+
// Don't prefix the heading ids
|
|
60
|
+
clobberPrefix: '',
|
|
61
|
+
// Makes it even more strict
|
|
62
|
+
tagNames,
|
|
63
|
+
attributes: {
|
|
64
|
+
...defaultSchema.attributes,
|
|
65
|
+
abbr: ['title'],
|
|
66
|
+
// Allow all class names while preserving the existing default attributes
|
|
67
|
+
'*': [...(defaultSchema.attributes?.['*'] ?? []), 'className'],
|
|
68
|
+
},
|
|
69
|
+
// Strip content of dangerous elements, not just the tags
|
|
70
|
+
strip: ['script', 'style', 'object', 'embed', 'form'],
|
|
71
|
+
})
|
|
72
|
+
// Syntax highlighting
|
|
73
|
+
.use(rehypeHighlight, {
|
|
74
|
+
languages: standardLanguages,
|
|
75
|
+
// Enable auto detection
|
|
76
|
+
detect: true,
|
|
77
|
+
})
|
|
78
|
+
// Adds target="_blank" to external links
|
|
79
|
+
.use(rehypeExternalLinks, { target: '_blank' })
|
|
80
|
+
// Formats the HTML
|
|
81
|
+
.use(rehypeFormat)
|
|
82
|
+
// Converts the HTML AST to a string
|
|
83
|
+
.use(rehypeStringify)
|
|
84
|
+
// Run the pipeline
|
|
85
|
+
.processSync(markdown);
|
|
86
|
+
return html.toString();
|
|
56
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a Markdown AST from a string.
|
|
90
|
+
*/
|
|
57
91
|
function getMarkdownAst(markdown) {
|
|
58
|
-
|
|
92
|
+
return unified().use(remarkParse).use(remarkGfm).parse(markdown);
|
|
59
93
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Find all headings of a specific type in a Markdown AST.
|
|
96
|
+
*/
|
|
97
|
+
export function getHeadings(markdown, depth = 1) {
|
|
98
|
+
const tree = getMarkdownAst(markdown);
|
|
99
|
+
const nodes = [];
|
|
100
|
+
visit(tree, 'heading', (node) => {
|
|
101
|
+
const text = textFromNode(node);
|
|
102
|
+
if (text) {
|
|
103
|
+
nodes.push({ depth: node.depth ?? depth, value: text });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return nodes;
|
|
70
107
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Extract plain text from a Markdown AST node (recursively).
|
|
110
|
+
*
|
|
111
|
+
* Handles headings with nested phrasing content such as links.
|
|
112
|
+
*/
|
|
113
|
+
export function textFromNode(node) {
|
|
114
|
+
if (node.type === 'text') {
|
|
115
|
+
return node.value ?? '';
|
|
116
|
+
}
|
|
117
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
118
|
+
return node.children.map((child) => textFromNode(child)).join('');
|
|
119
|
+
}
|
|
120
|
+
return '';
|
|
79
121
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Return multiple Markdown documents. Every heading should be its own document.
|
|
124
|
+
*/
|
|
125
|
+
export function splitContent(markdown) {
|
|
126
|
+
const tree = getMarkdownAst(markdown);
|
|
127
|
+
/** Sections */
|
|
128
|
+
const sections = [];
|
|
129
|
+
/** Nodes inside a section */
|
|
130
|
+
let nodes = [];
|
|
131
|
+
tree.children?.forEach((node) => {
|
|
132
|
+
// If the node is a heading, start a new section
|
|
133
|
+
if (node.type === 'heading') {
|
|
134
|
+
if (nodes.length) {
|
|
135
|
+
sections.push(nodes);
|
|
136
|
+
}
|
|
137
|
+
sections.push([node]);
|
|
138
|
+
nodes = [];
|
|
139
|
+
}
|
|
140
|
+
// Otherwise, add the node to the current section
|
|
141
|
+
else {
|
|
142
|
+
nodes.push(node);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Add any remaining nodes
|
|
146
|
+
if (nodes.length) {
|
|
87
147
|
sections.push(nodes);
|
|
88
|
-
}
|
|
89
|
-
sections.push([node]);
|
|
90
|
-
nodes = [];
|
|
91
|
-
} else {
|
|
92
|
-
nodes.push(node);
|
|
93
148
|
}
|
|
94
|
-
|
|
95
|
-
if (nodes.length) {
|
|
96
|
-
sections.push(nodes);
|
|
97
|
-
}
|
|
98
|
-
return sections.map((section) => createDocument(section));
|
|
149
|
+
return sections.map((section) => createDocument(section));
|
|
99
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Use remark to create a Markdown document from a list of nodes.
|
|
153
|
+
*/
|
|
100
154
|
function createDocument(nodes) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
155
|
+
// Create the Markdown string
|
|
156
|
+
const markdown = unified().use(remarkStringify).use(remarkGfm).stringify({
|
|
157
|
+
type: 'root',
|
|
158
|
+
children: nodes,
|
|
159
|
+
});
|
|
160
|
+
// Remove the whitespace
|
|
161
|
+
return markdown.trim();
|
|
106
162
|
}
|
|
107
|
-
export {
|
|
108
|
-
getHeadings,
|
|
109
|
-
htmlFromMarkdown,
|
|
110
|
-
isHeading,
|
|
111
|
-
splitContent,
|
|
112
|
-
textFromNode
|
|
113
|
-
};
|
|
114
|
-
//# sourceMappingURL=markdown.js.map
|
|
@@ -1,94 +1,102 @@
|
|
|
1
|
-
import { visit } from
|
|
2
|
-
const ALERT_TYPES = [
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
const ALERT_TYPES = ['note', 'tip', 'important', 'warning', 'caution', 'success'];
|
|
3
|
+
// Simple whitespace check function
|
|
3
4
|
function isWhitespace(node) {
|
|
4
|
-
|
|
5
|
+
return node.type === 'text' && typeof node.value === 'string' && /^\s*$/.test(node.value);
|
|
5
6
|
}
|
|
6
|
-
function rehypeAlert() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
};
|
|
90
102
|
}
|
|
91
|
-
export {
|
|
92
|
-
rehypeAlert
|
|
93
|
-
};
|
|
94
|
-
//# sourceMappingURL=rehype-alert.js.map
|