@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.
@@ -1,114 +1,162 @@
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
- const isHeading = (node) => {
16
- return node.type === "heading" && "depth" in node && "children" in node;
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
- if (!options?.transform || !options?.type) {
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
- function htmlFromMarkdown(markdown, options) {
29
- const removeTags = options?.removeTags ?? [];
30
- const tagNames = [...defaultSchema.tagNames ?? [], ...options?.allowTags ?? []].filter(
31
- (t) => !removeTags.includes(t)
32
- );
33
- const html = unified().use(remarkParse).use(remarkGfm).use(transformNodes, {
34
- transform: options?.transform,
35
- type: options?.transformType
36
- }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeAlert).use(rehypeRaw).use(rehypeSanitize, {
37
- ...defaultSchema,
38
- // Don't prefix the heading ids
39
- clobberPrefix: "",
40
- // Makes it even more strict
41
- tagNames,
42
- attributes: {
43
- ...defaultSchema.attributes,
44
- abbr: ["title"],
45
- // Allow all class names while preserving the existing default attributes
46
- "*": [...defaultSchema.attributes?.["*"] ?? [], "className"]
47
- },
48
- // Strip content of dangerous elements, not just the tags
49
- strip: ["script", "style", "object", "embed", "form"]
50
- }).use(rehypeHighlight, {
51
- languages: standardLanguages,
52
- // Enable auto detection
53
- detect: true
54
- }).use(rehypeExternalLinks, { target: "_blank" }).use(rehypeFormat).use(rehypeStringify).processSync(markdown);
55
- return html.toString();
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
- return unified().use(remarkParse).use(remarkGfm).parse(markdown);
92
+ return unified().use(remarkParse).use(remarkGfm).parse(markdown);
59
93
  }
60
- function getHeadings(markdown, depth = 1) {
61
- const tree = getMarkdownAst(markdown);
62
- const nodes = [];
63
- visit(tree, "heading", (node) => {
64
- const text = textFromNode(node);
65
- if (text) {
66
- nodes.push({ depth: node.depth ?? depth, value: text });
67
- }
68
- });
69
- return nodes;
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
- function textFromNode(node) {
72
- if (node.type === "text") {
73
- return node.value ?? "";
74
- }
75
- if ("children" in node && Array.isArray(node.children)) {
76
- return node.children.map((child) => textFromNode(child)).join("");
77
- }
78
- return "";
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
- function splitContent(markdown) {
81
- const tree = getMarkdownAst(markdown);
82
- const sections = [];
83
- let nodes = [];
84
- tree.children?.forEach((node) => {
85
- if (node.type === "heading") {
86
- if (nodes.length) {
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
- const markdown = unified().use(remarkStringify).use(remarkGfm).stringify({
102
- type: "root",
103
- children: nodes
104
- });
105
- return markdown.trim();
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,5 +1 @@
1
- import { rehypeAlert } from "./rehype-alert.js";
2
- export {
3
- rehypeAlert
4
- };
5
- //# sourceMappingURL=index.js.map
1
+ export { rehypeAlert } from './rehype-alert.js';
@@ -1,94 +1,102 @@
1
- import { visit } from "unist-util-visit";
2
- const ALERT_TYPES = ["note", "tip", "important", "warning", "caution", "success"];
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
- return node.type === "text" && typeof node.value === "string" && /^\s*$/.test(node.value);
5
+ return node.type === 'text' && typeof node.value === 'string' && /^\s*$/.test(node.value);
5
6
  }
6
- function rehypeAlert() {
7
- return (tree) => {
8
- visit(tree, "element", (node, index, parent) => {
9
- if (node.tagName !== "blockquote" || typeof index !== "number" || !parent || parent.type !== "root") {
10
- return;
11
- }
12
- const headIndex = node.children.findIndex((child) => !isWhitespace(child));
13
- if (headIndex === -1) {
14
- return;
15
- }
16
- const head = node.children[headIndex];
17
- if (!head || head.type !== "element" || head.tagName !== "p") {
18
- return;
19
- }
20
- const text = head.children[0];
21
- if (!text || text.type !== "text" || !text.value.startsWith("[!")) {
22
- return;
23
- }
24
- const end = text.value.indexOf("]");
25
- if (end === -1) {
26
- return;
27
- }
28
- const alertType = text.value.slice(2, end).toLowerCase();
29
- if (!ALERT_TYPES.includes(alertType)) {
30
- return;
31
- }
32
- if (end + 1 === text.value.length) {
33
- const next = head.children[1];
34
- if (next) {
35
- if (next.type !== "element" || next.tagName !== "br") {
36
- return;
37
- }
38
- if (!head.children[2]) {
39
- return;
40
- }
41
- head.children = head.children.slice(2);
42
- const node2 = head.children[0];
43
- if (node2 && node2.type === "text" && node2.value.charAt(0) === "\n") {
44
- node2.value = node2.value.slice(1);
45
- }
46
- } else {
47
- const skipped = headIndex + 1 < node.children.length && isWhitespace(node.children[headIndex + 1]);
48
- const nextIndex = skipped ? headIndex + 2 : headIndex + 1;
49
- if (nextIndex >= node.children.length || node.children[nextIndex]?.type !== "element") {
50
- return;
51
- }
52
- node.children = node.children.slice(nextIndex);
53
- }
54
- } else if (text.value.charAt(end + 1) === "\n" && // Check if the next character is a newline or a non-whitespace character
55
- (end + 2 === text.value.length || !/^\s*$/.test(text.value.slice(end + 2)))) {
56
- text.value = text.value.slice(end + 2);
57
- } else {
58
- text.value = text.value.replace(/^\s*\[!.*?\]\s*/, "");
59
- }
60
- const contentChildren = [];
61
- for (let i = headIndex; i < node.children.length; i++) {
62
- const child = node.children[i];
63
- if (child?.type === "element" && child.tagName === "p" && child.children) {
64
- contentChildren.push(...child.children);
65
- } else {
66
- contentChildren.push(child);
67
- }
68
- }
69
- parent.children[index] = {
70
- type: "element",
71
- tagName: "div",
72
- properties: { className: ["markdown-alert", `markdown-alert-${alertType}`] },
73
- children: [
74
- {
75
- type: "element",
76
- tagName: "div",
77
- properties: { className: ["markdown-alert-icon"] },
78
- children: []
79
- },
80
- {
81
- type: "element",
82
- tagName: "div",
83
- properties: { className: ["markdown-alert-content"] },
84
- children: [{ type: "text", value: " " }, ...contentChildren]
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
@@ -1,5 +1 @@
1
- import { rehypeHighlight } from "./rehype-highlight.js";
2
- export {
3
- rehypeHighlight
4
- };
5
- //# sourceMappingURL=index.js.map
1
+ export { rehypeHighlight } from './rehype-highlight.js';