@luckyfishes/markdown-core 0.1.0
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/README.md +21 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +1148 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# `@luckyfishes/markdown-core`
|
|
2
|
+
|
|
3
|
+
Reusable Markdown/MDX parsing and AST transformation utilities extracted from `noname_blog`.
|
|
4
|
+
|
|
5
|
+
This package only handles syntax parsing and AST transforms. It does not ship React components, styles, or framework-specific rendering glue.
|
|
6
|
+
|
|
7
|
+
## Included
|
|
8
|
+
|
|
9
|
+
- Custom block syntax like `::tabs`, `::details`, and `::ComponentName`
|
|
10
|
+
- Inline component syntax like `:badge[...]()` and `:tip[...]()`
|
|
11
|
+
- Superscript and subscript transforms
|
|
12
|
+
- Code fence to MDX component transforms
|
|
13
|
+
- Footnotes heading normalization
|
|
14
|
+
- Heading extraction helpers
|
|
15
|
+
|
|
16
|
+
## Not Included
|
|
17
|
+
|
|
18
|
+
- React component implementations
|
|
19
|
+
- MDX runtime rendering
|
|
20
|
+
- Next.js integration
|
|
21
|
+
- CSS styles
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { PluggableList } from 'unified';
|
|
2
|
+
import { RootContent, Text, Root } from 'mdast';
|
|
3
|
+
|
|
4
|
+
declare const FOOTNOTES_HEADING_TEXT = "Footnotes";
|
|
5
|
+
declare const FOOTNOTES_HEADING_ID = "footnote-label";
|
|
6
|
+
type FootnotesOptions = {
|
|
7
|
+
headingId?: string;
|
|
8
|
+
headingText?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type HastNode$1 = {
|
|
12
|
+
type: string;
|
|
13
|
+
tagName?: string;
|
|
14
|
+
properties?: Record<string, unknown>;
|
|
15
|
+
children?: HastNode$1[];
|
|
16
|
+
value?: string;
|
|
17
|
+
};
|
|
18
|
+
type CodeFenceComponentMapping = {
|
|
19
|
+
componentName: string;
|
|
20
|
+
propName: string;
|
|
21
|
+
};
|
|
22
|
+
type CodeFenceComponentMappings = Record<string, CodeFenceComponentMapping>;
|
|
23
|
+
declare const DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS: CodeFenceComponentMappings;
|
|
24
|
+
declare function createCodeFenceComponentPlugin(mappings?: CodeFenceComponentMappings): (tree: HastNode$1) => void;
|
|
25
|
+
|
|
26
|
+
type NodeWithPosition = {
|
|
27
|
+
position?: {
|
|
28
|
+
start?: {
|
|
29
|
+
line?: number;
|
|
30
|
+
offset?: number;
|
|
31
|
+
};
|
|
32
|
+
end?: {
|
|
33
|
+
line?: number;
|
|
34
|
+
offset?: number;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type FlowNode = RootContent & NodeWithPosition;
|
|
39
|
+
type CustomSyntaxComponentNames = {
|
|
40
|
+
badge: string;
|
|
41
|
+
tip: string;
|
|
42
|
+
tabs: string;
|
|
43
|
+
tabsList: string;
|
|
44
|
+
tabsTrigger: string;
|
|
45
|
+
tabsContent: string;
|
|
46
|
+
};
|
|
47
|
+
type CustomSyntaxOptions = {
|
|
48
|
+
componentNames?: Partial<CustomSyntaxComponentNames>;
|
|
49
|
+
};
|
|
50
|
+
declare const DEFAULT_CUSTOM_SYNTAX_COMPONENT_NAMES: CustomSyntaxComponentNames;
|
|
51
|
+
declare function resolveCustomSyntaxComponentNames(options?: CustomSyntaxOptions): CustomSyntaxComponentNames;
|
|
52
|
+
type BlockHandlerKind = "block" | "container";
|
|
53
|
+
type BlockHandlerMatchContext = {
|
|
54
|
+
children: FlowNode[];
|
|
55
|
+
componentNames: CustomSyntaxComponentNames;
|
|
56
|
+
getNodeSource: (node: NodeWithPosition) => string;
|
|
57
|
+
index: number;
|
|
58
|
+
source: string;
|
|
59
|
+
lines: string[];
|
|
60
|
+
};
|
|
61
|
+
type BlockHandlerTransformContext = BlockHandlerMatchContext & {
|
|
62
|
+
consumeThroughLine: (endLine: number) => number;
|
|
63
|
+
createFlowElement: (name: string, props?: Record<string, unknown>, children?: RootContent[]) => RootContent;
|
|
64
|
+
createInlineTextElement: (name: string, props?: Record<string, unknown>, text?: string) => RootContent;
|
|
65
|
+
createText: (value: string) => Text;
|
|
66
|
+
getLine: (lineNumber: number) => string;
|
|
67
|
+
getNodeSource: (node: NodeWithPosition) => string;
|
|
68
|
+
parseYamlProps: (yamlSource: string) => Record<string, unknown>;
|
|
69
|
+
transformFragment: (markdown: string) => RootContent[];
|
|
70
|
+
};
|
|
71
|
+
type BlockHandlerResult = {
|
|
72
|
+
consumed: number;
|
|
73
|
+
nodes: RootContent[];
|
|
74
|
+
};
|
|
75
|
+
type CustomSyntaxBlockHandler = {
|
|
76
|
+
kind: BlockHandlerKind;
|
|
77
|
+
match: (context: BlockHandlerMatchContext) => boolean;
|
|
78
|
+
name: string;
|
|
79
|
+
priority?: number;
|
|
80
|
+
transform: (context: BlockHandlerTransformContext) => BlockHandlerResult;
|
|
81
|
+
};
|
|
82
|
+
type InlineHandlerContext = {
|
|
83
|
+
componentNames: CustomSyntaxComponentNames;
|
|
84
|
+
createInlineTextElement: (name: string, props?: Record<string, unknown>, text?: string) => RootContent;
|
|
85
|
+
createText: (value: string) => Text;
|
|
86
|
+
getNodeSource: (node: NodeWithPosition) => string;
|
|
87
|
+
source: string;
|
|
88
|
+
};
|
|
89
|
+
type CustomSyntaxInlineHandler = {
|
|
90
|
+
kind: "inline";
|
|
91
|
+
match: (nodes: RootContent[], index: number, context: InlineHandlerContext) => boolean;
|
|
92
|
+
name: string;
|
|
93
|
+
priority?: number;
|
|
94
|
+
transform: (nodes: RootContent[], index: number, context: InlineHandlerContext) => {
|
|
95
|
+
consumed: number;
|
|
96
|
+
nodes: RootContent[];
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type MarkdownPresetOptions = {
|
|
101
|
+
codeFenceMappings?: CodeFenceComponentMappings;
|
|
102
|
+
customSyntax?: CustomSyntaxOptions;
|
|
103
|
+
footnotes?: FootnotesOptions;
|
|
104
|
+
};
|
|
105
|
+
type MarkdownPreset = {
|
|
106
|
+
remarkPlugins: PluggableList;
|
|
107
|
+
rehypePlugins: PluggableList;
|
|
108
|
+
};
|
|
109
|
+
declare function createMarkdownPreset(options?: MarkdownPresetOptions): MarkdownPreset;
|
|
110
|
+
|
|
111
|
+
type HastNode = {
|
|
112
|
+
type: string;
|
|
113
|
+
tagName?: string;
|
|
114
|
+
properties?: Record<string, unknown>;
|
|
115
|
+
children?: HastNode[];
|
|
116
|
+
value?: string;
|
|
117
|
+
};
|
|
118
|
+
declare function rehypeFootnotesHeading(options?: FootnotesOptions): (tree: HastNode) => void;
|
|
119
|
+
|
|
120
|
+
declare function createCustomSyntaxRemarkPlugin(options?: CustomSyntaxOptions): (tree: Root, file?: {
|
|
121
|
+
value?: unknown;
|
|
122
|
+
}) => void;
|
|
123
|
+
|
|
124
|
+
declare const YAML_PROP_PREFIX = "__YAML__";
|
|
125
|
+
|
|
126
|
+
type MdastNode = {
|
|
127
|
+
type: string;
|
|
128
|
+
value?: string;
|
|
129
|
+
children?: MdastNode[];
|
|
130
|
+
};
|
|
131
|
+
declare function remarkSuperSub(): (tree: MdastNode) => void;
|
|
132
|
+
|
|
133
|
+
type MarkdownHeading = {
|
|
134
|
+
depth: 1 | 2 | 3;
|
|
135
|
+
text: string;
|
|
136
|
+
id: string;
|
|
137
|
+
};
|
|
138
|
+
declare function extractHeadings(markdown: string, options?: FootnotesOptions): MarkdownHeading[];
|
|
139
|
+
|
|
140
|
+
export { type CodeFenceComponentMapping, type CodeFenceComponentMappings, type CustomSyntaxBlockHandler, type CustomSyntaxComponentNames, type CustomSyntaxInlineHandler, type CustomSyntaxOptions, DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS, DEFAULT_CUSTOM_SYNTAX_COMPONENT_NAMES, FOOTNOTES_HEADING_ID, FOOTNOTES_HEADING_TEXT, type FlowNode, type FootnotesOptions, type MarkdownHeading, type MarkdownPreset, type MarkdownPresetOptions, type NodeWithPosition, YAML_PROP_PREFIX, createCodeFenceComponentPlugin, createCustomSyntaxRemarkPlugin, createMarkdownPreset, extractHeadings, rehypeFootnotesHeading, remarkSuperSub, resolveCustomSyntaxComponentNames };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
// src/footnotes.ts
|
|
2
|
+
var FOOTNOTES_HEADING_TEXT = "Footnotes";
|
|
3
|
+
var FOOTNOTES_HEADING_ID = "footnote-label";
|
|
4
|
+
function resolveFootnotesHeadingDepth(depths) {
|
|
5
|
+
return depths.includes(1) ? 1 : 2;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/preset.ts
|
|
9
|
+
import remarkGfm2 from "remark-gfm";
|
|
10
|
+
|
|
11
|
+
// src/rehype/code-fence-components.ts
|
|
12
|
+
import { visit } from "unist-util-visit";
|
|
13
|
+
var DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS = {
|
|
14
|
+
chart: { componentName: "ChartBlock", propName: "spec" },
|
|
15
|
+
mermaid: { componentName: "MermaidDiagram", propName: "chart" },
|
|
16
|
+
music: { componentName: "MusicScore", propName: "score" }
|
|
17
|
+
};
|
|
18
|
+
function hasLanguage(node, language) {
|
|
19
|
+
const className = node.properties?.className;
|
|
20
|
+
const targetClass = `language-${language}`;
|
|
21
|
+
if (typeof className === "string") {
|
|
22
|
+
return className.split(/\s+/).includes(targetClass);
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(className)) {
|
|
25
|
+
return className.some((name) => `${name}` === targetClass);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function extractText(node) {
|
|
30
|
+
if (node.type === "text") {
|
|
31
|
+
return node.value ?? "";
|
|
32
|
+
}
|
|
33
|
+
if (!node.children || node.children.length === 0) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
return node.children.map((child) => extractText(child)).join("");
|
|
37
|
+
}
|
|
38
|
+
function createCodeFenceComponentPlugin(mappings = DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS) {
|
|
39
|
+
const resolvedMappings = {
|
|
40
|
+
...DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS,
|
|
41
|
+
...mappings
|
|
42
|
+
};
|
|
43
|
+
return (tree) => {
|
|
44
|
+
visit(
|
|
45
|
+
tree,
|
|
46
|
+
"element",
|
|
47
|
+
(node, index, parent) => {
|
|
48
|
+
if (!parent || typeof index !== "number") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (node.tagName !== "pre") {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const [codeNode] = node.children ?? [];
|
|
55
|
+
if (!codeNode || codeNode.type !== "element" || codeNode.tagName !== "code") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const match = Object.entries(resolvedMappings).find(
|
|
59
|
+
([language2]) => hasLanguage(codeNode, language2)
|
|
60
|
+
);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const [language, mapping] = match;
|
|
65
|
+
const value = extractText(codeNode).trim();
|
|
66
|
+
parent.children = parent.children ?? [];
|
|
67
|
+
parent.children[index] = {
|
|
68
|
+
type: "element",
|
|
69
|
+
tagName: mapping.componentName,
|
|
70
|
+
properties: {
|
|
71
|
+
[mapping.propName]: value,
|
|
72
|
+
"data-language": language
|
|
73
|
+
},
|
|
74
|
+
children: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/rehype/footnotes-heading.ts
|
|
82
|
+
import { visit as visit2 } from "unist-util-visit";
|
|
83
|
+
function hasFootnotesSection(node) {
|
|
84
|
+
return node.tagName === "section" && Object.prototype.hasOwnProperty.call(node.properties ?? {}, "dataFootnotes");
|
|
85
|
+
}
|
|
86
|
+
function removeSrOnlyClass(className) {
|
|
87
|
+
if (typeof className === "string") {
|
|
88
|
+
return className.split(/\s+/).filter((name) => name && name !== "sr-only");
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(className)) {
|
|
91
|
+
return className.filter((name) => `${name}` !== "sr-only");
|
|
92
|
+
}
|
|
93
|
+
return className;
|
|
94
|
+
}
|
|
95
|
+
function isHeadingTag(tagName) {
|
|
96
|
+
return tagName === "h1" || tagName === "h2" || tagName === "h3";
|
|
97
|
+
}
|
|
98
|
+
function resolveHeadingTagName(tree) {
|
|
99
|
+
const depths = [];
|
|
100
|
+
visit2(tree, "element", (node) => {
|
|
101
|
+
if (!isHeadingTag(node.tagName)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const depth = Number(node.tagName.slice(1));
|
|
105
|
+
if (!Number.isNaN(depth)) {
|
|
106
|
+
depths.push(depth);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return `h${resolveFootnotesHeadingDepth(depths)}`;
|
|
110
|
+
}
|
|
111
|
+
function createHeadingNode(tagName, headingId, headingText) {
|
|
112
|
+
return {
|
|
113
|
+
type: "element",
|
|
114
|
+
tagName,
|
|
115
|
+
properties: {
|
|
116
|
+
id: headingId
|
|
117
|
+
},
|
|
118
|
+
children: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
value: headingText
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function rehypeFootnotesHeading(options) {
|
|
127
|
+
const headingId = options?.headingId ?? FOOTNOTES_HEADING_ID;
|
|
128
|
+
const headingText = options?.headingText ?? FOOTNOTES_HEADING_TEXT;
|
|
129
|
+
return (tree) => {
|
|
130
|
+
const headingTagName = resolveHeadingTagName(tree);
|
|
131
|
+
visit2(tree, "element", (node) => {
|
|
132
|
+
if (!hasFootnotesSection(node)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
node.children = node.children ?? [];
|
|
136
|
+
const existingHeading = node.children.find(
|
|
137
|
+
(child) => child.type === "element" && (child.tagName === "h1" || child.tagName === "h2") && child.properties?.id === headingId
|
|
138
|
+
);
|
|
139
|
+
if (existingHeading) {
|
|
140
|
+
existingHeading.tagName = headingTagName;
|
|
141
|
+
existingHeading.properties = {
|
|
142
|
+
...existingHeading.properties,
|
|
143
|
+
className: removeSrOnlyClass(existingHeading.properties?.className),
|
|
144
|
+
id: headingId
|
|
145
|
+
};
|
|
146
|
+
existingHeading.children = [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
value: headingText
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
node.children.unshift(
|
|
155
|
+
createHeadingNode(headingTagName, headingId, headingText)
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/remark/custom-syntax/utils.ts
|
|
162
|
+
import matter from "gray-matter";
|
|
163
|
+
import remarkGfm from "remark-gfm";
|
|
164
|
+
import remarkParse from "remark-parse";
|
|
165
|
+
import { unified } from "unified";
|
|
166
|
+
var YAML_PROP_PREFIX = "__YAML__";
|
|
167
|
+
var YAML_BODY_SPLIT_RE = /^---\s*$/;
|
|
168
|
+
var COMPONENT_START_RE = /^::([A-Za-z][A-Za-z0-9_]*)\s*$/;
|
|
169
|
+
var DETAILS_START_RE = /^::\s*details(?:\s+(\[open\]))?(?:\s+(.*\S))?\s*$/i;
|
|
170
|
+
var DOUBLE_BLOCK_START_RE = /^::(?:[A-Za-z][A-Za-z0-9_]*|\s+details(?:\s+.*)?)\s*$/i;
|
|
171
|
+
var BLOCK_END_RE = /^::\s*$/;
|
|
172
|
+
var LEGACY_COMPONENT_RE = /<(AreaChart|ChartTooltip|XAxis|GitHubCalendarCard|MermaidDiagram|Card)\b/;
|
|
173
|
+
var LEGACY_CHART_BLOCK_COMPONENTS = /* @__PURE__ */ new Set([
|
|
174
|
+
"AreaChart",
|
|
175
|
+
"Area",
|
|
176
|
+
"Grid",
|
|
177
|
+
"ChartTooltip",
|
|
178
|
+
"XAxis"
|
|
179
|
+
]);
|
|
180
|
+
function createSourceLineResult(source) {
|
|
181
|
+
return {
|
|
182
|
+
lines: source.split(/\r?\n/)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function getLine(lines, lineNumber) {
|
|
186
|
+
return lines[lineNumber - 1] ?? "";
|
|
187
|
+
}
|
|
188
|
+
function getNodeSource(source, node) {
|
|
189
|
+
const start = node.position?.start?.offset;
|
|
190
|
+
const end = node.position?.end?.offset;
|
|
191
|
+
if (typeof start !== "number" || typeof end !== "number") {
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
return source.slice(start, end);
|
|
195
|
+
}
|
|
196
|
+
function parseYamlProps(yamlSource) {
|
|
197
|
+
const trimmed = yamlSource.trim();
|
|
198
|
+
if (!trimmed) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
const { data } = matter(`---
|
|
202
|
+
${yamlSource}
|
|
203
|
+
---
|
|
204
|
+
`);
|
|
205
|
+
return data ?? {};
|
|
206
|
+
}
|
|
207
|
+
function escapeHtmlAttr(raw) {
|
|
208
|
+
return raw.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
209
|
+
}
|
|
210
|
+
function serializeAttrValue(value) {
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
return escapeHtmlAttr(value);
|
|
213
|
+
}
|
|
214
|
+
return `${YAML_PROP_PREFIX}${encodeURIComponent(JSON.stringify(value))}`;
|
|
215
|
+
}
|
|
216
|
+
function createAttribute(name, value) {
|
|
217
|
+
return {
|
|
218
|
+
type: "mdxJsxAttribute",
|
|
219
|
+
name,
|
|
220
|
+
value: value == null ? null : serializeAttrValue(value)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function createText(value) {
|
|
224
|
+
return {
|
|
225
|
+
type: "text",
|
|
226
|
+
value
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function createFlowElement(name, props = {}, children = []) {
|
|
230
|
+
const node = {
|
|
231
|
+
type: "mdxJsxFlowElement",
|
|
232
|
+
name,
|
|
233
|
+
attributes: Object.entries(props).map(
|
|
234
|
+
([key, value]) => createAttribute(key, value)
|
|
235
|
+
),
|
|
236
|
+
children
|
|
237
|
+
};
|
|
238
|
+
return node;
|
|
239
|
+
}
|
|
240
|
+
function createInlineTextElement(name, props = {}, text = "") {
|
|
241
|
+
const node = {
|
|
242
|
+
type: "mdxJsxTextElement",
|
|
243
|
+
name,
|
|
244
|
+
attributes: Object.entries(props).map(
|
|
245
|
+
([key, value]) => createAttribute(key, value)
|
|
246
|
+
),
|
|
247
|
+
children: text ? [createText(text)] : []
|
|
248
|
+
};
|
|
249
|
+
return node;
|
|
250
|
+
}
|
|
251
|
+
function parseMarkdownFragment(markdown) {
|
|
252
|
+
return unified().use(remarkParse).use(remarkGfm).parse(markdown);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/remark/custom-syntax/handlers/component-block.ts
|
|
256
|
+
function findBlockEnd(lines, startLine, isNestedStart) {
|
|
257
|
+
let cursor = startLine + 1;
|
|
258
|
+
let nestedDepth = 0;
|
|
259
|
+
let innerFenceMarker = null;
|
|
260
|
+
while (cursor <= lines.length) {
|
|
261
|
+
const current = lines[cursor - 1] ?? "";
|
|
262
|
+
if (innerFenceMarker) {
|
|
263
|
+
if (current.trimStart().startsWith(innerFenceMarker)) {
|
|
264
|
+
innerFenceMarker = null;
|
|
265
|
+
}
|
|
266
|
+
cursor += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const trimmedCurrent = current.trimStart();
|
|
270
|
+
if (trimmedCurrent.startsWith("```")) {
|
|
271
|
+
innerFenceMarker = "```";
|
|
272
|
+
cursor += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (trimmedCurrent.startsWith("~~~")) {
|
|
276
|
+
innerFenceMarker = "~~~";
|
|
277
|
+
cursor += 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (isNestedStart(current) && !BLOCK_END_RE.test(current)) {
|
|
281
|
+
nestedDepth += 1;
|
|
282
|
+
cursor += 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (BLOCK_END_RE.test(current)) {
|
|
286
|
+
if (nestedDepth === 0) {
|
|
287
|
+
return cursor;
|
|
288
|
+
}
|
|
289
|
+
nestedDepth -= 1;
|
|
290
|
+
}
|
|
291
|
+
cursor += 1;
|
|
292
|
+
}
|
|
293
|
+
return -1;
|
|
294
|
+
}
|
|
295
|
+
var componentBlockHandler = {
|
|
296
|
+
kind: "block",
|
|
297
|
+
match: ({ children, index, getNodeSource: getNodeSource2 }) => {
|
|
298
|
+
const node = children[index];
|
|
299
|
+
if (!node) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const snippet = getNodeSource2(node);
|
|
303
|
+
const firstLine = snippet.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
304
|
+
return COMPONENT_START_RE.test(firstLine);
|
|
305
|
+
},
|
|
306
|
+
name: "component-block",
|
|
307
|
+
transform: (context) => {
|
|
308
|
+
const node = context.children[context.index];
|
|
309
|
+
const startLine = node.position?.start?.line;
|
|
310
|
+
if (!startLine) {
|
|
311
|
+
return { consumed: 1, nodes: [node] };
|
|
312
|
+
}
|
|
313
|
+
const openingLine = context.getLine(startLine).trim();
|
|
314
|
+
const componentMatch = openingLine.match(COMPONENT_START_RE);
|
|
315
|
+
if (!componentMatch) {
|
|
316
|
+
return { consumed: 1, nodes: [node] };
|
|
317
|
+
}
|
|
318
|
+
const componentName = componentMatch[1];
|
|
319
|
+
if (LEGACY_CHART_BLOCK_COMPONENTS.has(componentName)) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Legacy chart block syntax "::${componentName}" is not supported. Use \`\`\`chart JSON code blocks instead. (line ${startLine})`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const endLine = findBlockEnd(
|
|
325
|
+
context.lines,
|
|
326
|
+
startLine,
|
|
327
|
+
(line) => COMPONENT_START_RE.test(line)
|
|
328
|
+
);
|
|
329
|
+
if (endLine === -1) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Unclosed "::${componentName}" block at line ${startLine}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const blockLines = context.lines.slice(startLine, endLine - 1);
|
|
335
|
+
const splitAt = blockLines.findIndex(
|
|
336
|
+
(line) => YAML_BODY_SPLIT_RE.test(line)
|
|
337
|
+
);
|
|
338
|
+
const yamlLines = splitAt === -1 ? blockLines : blockLines.slice(0, splitAt);
|
|
339
|
+
const bodyLines = splitAt === -1 ? [] : blockLines.slice(splitAt + 1);
|
|
340
|
+
const props = context.parseYamlProps(yamlLines.join("\n"));
|
|
341
|
+
const bodyMarkdown = bodyLines.join("\n");
|
|
342
|
+
const children = bodyMarkdown ? context.transformFragment(bodyMarkdown) : [];
|
|
343
|
+
return {
|
|
344
|
+
consumed: context.consumeThroughLine(endLine),
|
|
345
|
+
nodes: [context.createFlowElement(componentName, props, children)]
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// src/remark/custom-syntax/handlers/details-block.ts
|
|
351
|
+
function findDetailsEnd(lines, startLine) {
|
|
352
|
+
let cursor = startLine + 1;
|
|
353
|
+
let nestedDepth = 0;
|
|
354
|
+
let innerFenceMarker = null;
|
|
355
|
+
while (cursor <= lines.length) {
|
|
356
|
+
const current = lines[cursor - 1] ?? "";
|
|
357
|
+
if (innerFenceMarker) {
|
|
358
|
+
if (current.trimStart().startsWith(innerFenceMarker)) {
|
|
359
|
+
innerFenceMarker = null;
|
|
360
|
+
}
|
|
361
|
+
cursor += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const trimmedCurrent = current.trimStart();
|
|
365
|
+
if (trimmedCurrent.startsWith("```")) {
|
|
366
|
+
innerFenceMarker = "```";
|
|
367
|
+
cursor += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (trimmedCurrent.startsWith("~~~")) {
|
|
371
|
+
innerFenceMarker = "~~~";
|
|
372
|
+
cursor += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (DOUBLE_BLOCK_START_RE.test(current) && !BLOCK_END_RE.test(current)) {
|
|
376
|
+
nestedDepth += 1;
|
|
377
|
+
cursor += 1;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (BLOCK_END_RE.test(current)) {
|
|
381
|
+
if (nestedDepth === 0) {
|
|
382
|
+
return cursor;
|
|
383
|
+
}
|
|
384
|
+
nestedDepth -= 1;
|
|
385
|
+
}
|
|
386
|
+
cursor += 1;
|
|
387
|
+
}
|
|
388
|
+
return -1;
|
|
389
|
+
}
|
|
390
|
+
var detailsBlockHandler = {
|
|
391
|
+
kind: "container",
|
|
392
|
+
match: ({ children, index, getNodeSource: getNodeSource2 }) => {
|
|
393
|
+
const node = children[index];
|
|
394
|
+
if (!node) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const snippet = getNodeSource2(node);
|
|
398
|
+
const firstLine = snippet.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
399
|
+
return DETAILS_START_RE.test(firstLine);
|
|
400
|
+
},
|
|
401
|
+
name: "details-block",
|
|
402
|
+
priority: 10,
|
|
403
|
+
transform: (context) => {
|
|
404
|
+
const node = context.children[context.index];
|
|
405
|
+
const startLine = node.position?.start?.line;
|
|
406
|
+
if (!startLine) {
|
|
407
|
+
return { consumed: 1, nodes: [node] };
|
|
408
|
+
}
|
|
409
|
+
const openingLine = context.getLine(startLine).trim();
|
|
410
|
+
const detailsMatch = openingLine.match(DETAILS_START_RE);
|
|
411
|
+
if (!detailsMatch) {
|
|
412
|
+
return { consumed: 1, nodes: [node] };
|
|
413
|
+
}
|
|
414
|
+
const endLine = findDetailsEnd(context.lines, startLine);
|
|
415
|
+
if (endLine === -1) {
|
|
416
|
+
throw new Error(`Unclosed ":: details" block at line ${startLine}`);
|
|
417
|
+
}
|
|
418
|
+
const bodyMarkdown = context.lines.slice(startLine, endLine - 1).join("\n");
|
|
419
|
+
const summaryText = detailsMatch[2]?.trim() ?? "";
|
|
420
|
+
const children = [
|
|
421
|
+
context.createFlowElement("summary", {}, [
|
|
422
|
+
context.createText(summaryText || "Details")
|
|
423
|
+
]),
|
|
424
|
+
...context.transformFragment(bodyMarkdown)
|
|
425
|
+
];
|
|
426
|
+
const props = detailsMatch[1] ? { open: true } : {};
|
|
427
|
+
return {
|
|
428
|
+
consumed: context.consumeThroughLine(endLine),
|
|
429
|
+
nodes: [context.createFlowElement("details", props, children)]
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/remark/custom-syntax/handlers/inline-component.ts
|
|
435
|
+
var INLINE_COMPONENT_DEFINITIONS = {
|
|
436
|
+
badge: {
|
|
437
|
+
buildProps: ({ option, params }) => {
|
|
438
|
+
const shape = params.shape || params.variant || option || "default";
|
|
439
|
+
return { shape };
|
|
440
|
+
},
|
|
441
|
+
resolveComponentName: (context) => context.componentNames.badge
|
|
442
|
+
},
|
|
443
|
+
tip: {
|
|
444
|
+
buildProps: ({ body, option, params }) => ({
|
|
445
|
+
text: body,
|
|
446
|
+
tip: params.tip || params.tooltip || params.message || option || "",
|
|
447
|
+
copy: params.copy === "false" ? false : params.copy ? true : option === "copy" ? true : params.value ? true : false,
|
|
448
|
+
value: params.value || ""
|
|
449
|
+
}),
|
|
450
|
+
resolveComponentName: (context) => context.componentNames.tip
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
function parseInlineComponentParams(option) {
|
|
454
|
+
const trimmed = option.trim();
|
|
455
|
+
if (!trimmed) {
|
|
456
|
+
return {
|
|
457
|
+
option: trimmed,
|
|
458
|
+
params: {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (!trimmed.includes("=") && !trimmed.includes(",")) {
|
|
462
|
+
return {
|
|
463
|
+
option: trimmed,
|
|
464
|
+
params: {}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const params = {};
|
|
468
|
+
let hasInvalidSegment = false;
|
|
469
|
+
for (const segment of trimmed.split(",")) {
|
|
470
|
+
const entry = segment.trim();
|
|
471
|
+
if (!entry) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const separatorIndex = entry.indexOf("=");
|
|
475
|
+
if (separatorIndex === -1) {
|
|
476
|
+
params[entry] = "true";
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const key = entry.slice(0, separatorIndex).trim();
|
|
480
|
+
const value = entry.slice(separatorIndex + 1).trim();
|
|
481
|
+
if (!key) {
|
|
482
|
+
hasInvalidSegment = true;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
params[key] = value;
|
|
486
|
+
}
|
|
487
|
+
if (hasInvalidSegment) {
|
|
488
|
+
return {
|
|
489
|
+
option: trimmed,
|
|
490
|
+
params: {}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
option: "",
|
|
495
|
+
params
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
var INLINE_COMPONENT_RE = /(^|[^\\]):([A-Za-z][A-Za-z0-9_-]*)\[([^\]]*)\]\(([^)]*)\)/g;
|
|
499
|
+
function extractInlineText(children) {
|
|
500
|
+
return children.map(
|
|
501
|
+
(child) => child.type === "text" ? typeof child.value === "string" ? child.value : "" : "children" in child ? extractInlineText(child.children) : ""
|
|
502
|
+
).join("");
|
|
503
|
+
}
|
|
504
|
+
function buildInlineComponentNode(context, name, body, rawOption) {
|
|
505
|
+
const definition = INLINE_COMPONENT_DEFINITIONS[name];
|
|
506
|
+
if (!definition) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const parsedOption = parseInlineComponentParams(rawOption);
|
|
510
|
+
return context.createInlineTextElement(
|
|
511
|
+
definition.resolveComponentName(context),
|
|
512
|
+
definition.buildProps({
|
|
513
|
+
body,
|
|
514
|
+
name,
|
|
515
|
+
option: parsedOption.option,
|
|
516
|
+
params: parsedOption.params
|
|
517
|
+
}),
|
|
518
|
+
body
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
function isInlineLinkCandidate(node) {
|
|
522
|
+
return node?.type === "link";
|
|
523
|
+
}
|
|
524
|
+
var inlineComponentHandler = {
|
|
525
|
+
kind: "inline",
|
|
526
|
+
match: (nodes, index) => {
|
|
527
|
+
const current = nodes[index];
|
|
528
|
+
const next = nodes[index + 1];
|
|
529
|
+
if (current?.type === "text" && typeof current.value === "string" && /(^|[^\\]):([A-Za-z][A-Za-z0-9_-]*)$/.test(current.value) && isInlineLinkCandidate(next)) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (current?.type !== "text" || typeof current.value !== "string") {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
INLINE_COMPONENT_RE.lastIndex = 0;
|
|
536
|
+
return INLINE_COMPONENT_RE.test(current.value);
|
|
537
|
+
},
|
|
538
|
+
name: "inline-component",
|
|
539
|
+
transform: (nodes, index, context) => {
|
|
540
|
+
const current = nodes[index];
|
|
541
|
+
const next = nodes[index + 1];
|
|
542
|
+
if (current?.type !== "text" || typeof current.value !== "string") {
|
|
543
|
+
return { consumed: 1, nodes: current ? [current] : [] };
|
|
544
|
+
}
|
|
545
|
+
const markerMatch = current.value.match(
|
|
546
|
+
/^(.*?)(?<!\\):([A-Za-z][A-Za-z0-9_-]*)$/
|
|
547
|
+
);
|
|
548
|
+
if (markerMatch && isInlineLinkCandidate(next)) {
|
|
549
|
+
const leadingText = markerMatch[1] ?? "";
|
|
550
|
+
const name = markerMatch[2];
|
|
551
|
+
const body = extractInlineText(next.children);
|
|
552
|
+
const componentNode = buildInlineComponentNode(
|
|
553
|
+
context,
|
|
554
|
+
name,
|
|
555
|
+
body,
|
|
556
|
+
next.url
|
|
557
|
+
);
|
|
558
|
+
if (!componentNode) {
|
|
559
|
+
return { consumed: 1, nodes: [current] };
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
consumed: 2,
|
|
563
|
+
nodes: [
|
|
564
|
+
...leadingText ? [context.createText(leadingText)] : [],
|
|
565
|
+
componentNode
|
|
566
|
+
]
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const output = [];
|
|
570
|
+
const source = current.value;
|
|
571
|
+
let lastIndex = 0;
|
|
572
|
+
INLINE_COMPONENT_RE.lastIndex = 0;
|
|
573
|
+
for (const match of source.matchAll(INLINE_COMPONENT_RE)) {
|
|
574
|
+
const fullMatch = match[0];
|
|
575
|
+
const prefix = match[1] ?? "";
|
|
576
|
+
const name = match[2];
|
|
577
|
+
const body = match[3] ?? "";
|
|
578
|
+
const rawOption = match[4] ?? "";
|
|
579
|
+
const start = match.index ?? 0;
|
|
580
|
+
const matchStart = start + prefix.length;
|
|
581
|
+
const before = source.slice(lastIndex, matchStart);
|
|
582
|
+
if (before) {
|
|
583
|
+
output.push(context.createText(before));
|
|
584
|
+
}
|
|
585
|
+
const componentNode = buildInlineComponentNode(
|
|
586
|
+
context,
|
|
587
|
+
name,
|
|
588
|
+
body,
|
|
589
|
+
rawOption
|
|
590
|
+
);
|
|
591
|
+
if (componentNode) {
|
|
592
|
+
output.push(componentNode);
|
|
593
|
+
} else {
|
|
594
|
+
output.push(context.createText(fullMatch.slice(prefix.length)));
|
|
595
|
+
}
|
|
596
|
+
lastIndex = start + fullMatch.length;
|
|
597
|
+
}
|
|
598
|
+
if (lastIndex < source.length) {
|
|
599
|
+
output.push(context.createText(source.slice(lastIndex)));
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
consumed: 1,
|
|
603
|
+
nodes: output.length > 0 ? output : [current]
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/remark/custom-syntax/handlers/legacy-guard.ts
|
|
609
|
+
var legacyGuardBlockHandler = {
|
|
610
|
+
kind: "block",
|
|
611
|
+
match: ({ children, index, getNodeSource: getNodeSource2 }) => {
|
|
612
|
+
const node = children[index];
|
|
613
|
+
if (!node) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
if (node.type === "html" && typeof node.value === "string" && LEGACY_COMPONENT_RE.test(node.value)) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
const snippet = getNodeSource2(node);
|
|
620
|
+
const firstLine = snippet.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
621
|
+
const match = firstLine.match(COMPONENT_START_RE);
|
|
622
|
+
return Boolean(match && LEGACY_CHART_BLOCK_COMPONENTS.has(match[1]));
|
|
623
|
+
},
|
|
624
|
+
name: "legacy-guard-block",
|
|
625
|
+
priority: 100,
|
|
626
|
+
transform: (context) => {
|
|
627
|
+
const node = context.children[context.index];
|
|
628
|
+
const startLine = node?.position?.start?.line ?? context.index + 1;
|
|
629
|
+
if (node?.type === "html" && typeof node.value === "string" && LEGACY_COMPONENT_RE.test(node.value)) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
`Legacy MDX component syntax is not supported. Use "::ComponentName" with YAML props instead. (line ${startLine})`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
const openingLine = context.getLine(startLine).trim();
|
|
635
|
+
const match = openingLine.match(COMPONENT_START_RE);
|
|
636
|
+
if (match && LEGACY_CHART_BLOCK_COMPONENTS.has(match[1])) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Legacy chart block syntax "::${match[1]}" is not supported. Use \`\`\`chart JSON code blocks instead. (line ${startLine})`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
return { consumed: 1, nodes: node ? [node] : [] };
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// src/remark/custom-syntax/handlers/tabs-block.ts
|
|
646
|
+
var TABS_START_RE = /^::tabs\s*$/i;
|
|
647
|
+
function findTabsEnd(lines, startLine) {
|
|
648
|
+
let cursor = startLine + 1;
|
|
649
|
+
let nestedDepth = 0;
|
|
650
|
+
let innerFenceMarker = null;
|
|
651
|
+
while (cursor <= lines.length) {
|
|
652
|
+
const current = lines[cursor - 1] ?? "";
|
|
653
|
+
if (innerFenceMarker) {
|
|
654
|
+
if (current.trimStart().startsWith(innerFenceMarker)) {
|
|
655
|
+
innerFenceMarker = null;
|
|
656
|
+
}
|
|
657
|
+
cursor += 1;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const trimmedCurrent = current.trimStart();
|
|
661
|
+
if (trimmedCurrent.startsWith("```")) {
|
|
662
|
+
innerFenceMarker = "```";
|
|
663
|
+
cursor += 1;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (trimmedCurrent.startsWith("~~~")) {
|
|
667
|
+
innerFenceMarker = "~~~";
|
|
668
|
+
cursor += 1;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (TABS_START_RE.test(current)) {
|
|
672
|
+
nestedDepth += 1;
|
|
673
|
+
cursor += 1;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (BLOCK_END_RE.test(current)) {
|
|
677
|
+
if (nestedDepth === 0) {
|
|
678
|
+
return cursor;
|
|
679
|
+
}
|
|
680
|
+
nestedDepth -= 1;
|
|
681
|
+
}
|
|
682
|
+
cursor += 1;
|
|
683
|
+
}
|
|
684
|
+
return -1;
|
|
685
|
+
}
|
|
686
|
+
function splitTabSections(value) {
|
|
687
|
+
const lines = value.split(/\r?\n/);
|
|
688
|
+
const sections = [];
|
|
689
|
+
let currentSection = [];
|
|
690
|
+
let yamlSource = "";
|
|
691
|
+
let yamlCaptured = false;
|
|
692
|
+
let inInnerFence = null;
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
const trimmed = line.trim();
|
|
695
|
+
if (inInnerFence) {
|
|
696
|
+
if (trimmed.startsWith(inInnerFence)) {
|
|
697
|
+
inInnerFence = null;
|
|
698
|
+
}
|
|
699
|
+
currentSection.push(line);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (trimmed.startsWith("```")) {
|
|
703
|
+
inInnerFence = "```";
|
|
704
|
+
currentSection.push(line);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (trimmed.startsWith("~~~")) {
|
|
708
|
+
inInnerFence = "~~~";
|
|
709
|
+
currentSection.push(line);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (YAML_BODY_SPLIT_RE.test(line)) {
|
|
713
|
+
if (!yamlCaptured) {
|
|
714
|
+
yamlSource = currentSection.join("\n");
|
|
715
|
+
yamlCaptured = true;
|
|
716
|
+
} else {
|
|
717
|
+
sections.push(currentSection.join("\n"));
|
|
718
|
+
}
|
|
719
|
+
currentSection = [];
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
currentSection.push(line);
|
|
723
|
+
}
|
|
724
|
+
sections.push(currentSection.join("\n"));
|
|
725
|
+
return {
|
|
726
|
+
sections,
|
|
727
|
+
yamlSource
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
var tabsBlockHandler = {
|
|
731
|
+
kind: "container",
|
|
732
|
+
match: ({ children, index, getNodeSource: getNodeSource2 }) => {
|
|
733
|
+
const node = children[index];
|
|
734
|
+
if (!node) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const snippet = getNodeSource2(node);
|
|
738
|
+
const firstLine = snippet.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
739
|
+
return TABS_START_RE.test(firstLine);
|
|
740
|
+
},
|
|
741
|
+
name: "tabs-block",
|
|
742
|
+
priority: 20,
|
|
743
|
+
transform: (context) => {
|
|
744
|
+
const node = context.children[context.index];
|
|
745
|
+
const startLine = node.position?.start?.line;
|
|
746
|
+
if (!startLine) {
|
|
747
|
+
return { consumed: 1, nodes: [node] };
|
|
748
|
+
}
|
|
749
|
+
const openingLine = context.getLine(startLine).trim();
|
|
750
|
+
if (!TABS_START_RE.test(openingLine)) {
|
|
751
|
+
return { consumed: 1, nodes: [node] };
|
|
752
|
+
}
|
|
753
|
+
const endLine = findTabsEnd(context.lines, startLine);
|
|
754
|
+
if (endLine === -1) {
|
|
755
|
+
throw new Error(`Unclosed "::tabs" block at line ${startLine}`);
|
|
756
|
+
}
|
|
757
|
+
const blockLines = context.lines.slice(startLine, endLine - 1);
|
|
758
|
+
const { sections, yamlSource } = splitTabSections(blockLines.join("\n"));
|
|
759
|
+
const props = context.parseYamlProps(yamlSource);
|
|
760
|
+
const labels = Array.isArray(props.tabs) ? props.tabs : [];
|
|
761
|
+
delete props.tabs;
|
|
762
|
+
const tabsData = sections.map((section) => section.trim()).filter((section, index) => section.length > 0 || index < labels.length).map((section, index) => ({
|
|
763
|
+
content: section,
|
|
764
|
+
label: typeof labels[index] === "string" ? labels[index] : `Tab ${index + 1}`,
|
|
765
|
+
value: `tab-${index}`
|
|
766
|
+
}));
|
|
767
|
+
if (tabsData.length === 0) {
|
|
768
|
+
return {
|
|
769
|
+
consumed: context.consumeThroughLine(endLine),
|
|
770
|
+
nodes: []
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const defaultValue = typeof props.defaultValue === "string" ? props.defaultValue : tabsData[0]?.value ?? "";
|
|
774
|
+
const tabsProps = { ...props, defaultValue };
|
|
775
|
+
const tabsListChildren = tabsData.map(
|
|
776
|
+
(tab) => context.createFlowElement(
|
|
777
|
+
context.componentNames.tabsTrigger,
|
|
778
|
+
{ value: tab.value },
|
|
779
|
+
[context.createText(tab.label)]
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
const tabsContentChildren = tabsData.map(
|
|
783
|
+
(tab) => context.createFlowElement(
|
|
784
|
+
context.componentNames.tabsContent,
|
|
785
|
+
{ value: tab.value },
|
|
786
|
+
[...context.transformFragment(tab.content)]
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
return {
|
|
790
|
+
consumed: context.consumeThroughLine(endLine),
|
|
791
|
+
nodes: [
|
|
792
|
+
context.createFlowElement(context.componentNames.tabs, tabsProps, [
|
|
793
|
+
context.createFlowElement(
|
|
794
|
+
context.componentNames.tabsList,
|
|
795
|
+
{},
|
|
796
|
+
tabsListChildren
|
|
797
|
+
),
|
|
798
|
+
...tabsContentChildren
|
|
799
|
+
])
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/remark/custom-syntax/types.ts
|
|
806
|
+
var DEFAULT_CUSTOM_SYNTAX_COMPONENT_NAMES = {
|
|
807
|
+
badge: "Badge",
|
|
808
|
+
tip: "Tip",
|
|
809
|
+
tabs: "Tabs",
|
|
810
|
+
tabsList: "TabsList",
|
|
811
|
+
tabsTrigger: "TabsTrigger",
|
|
812
|
+
tabsContent: "TabsContent"
|
|
813
|
+
};
|
|
814
|
+
function resolveCustomSyntaxComponentNames(options) {
|
|
815
|
+
return {
|
|
816
|
+
...DEFAULT_CUSTOM_SYNTAX_COMPONENT_NAMES,
|
|
817
|
+
...options?.componentNames
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/remark/custom-syntax/plugin.ts
|
|
822
|
+
var BLOCK_HANDLERS = [
|
|
823
|
+
legacyGuardBlockHandler,
|
|
824
|
+
detailsBlockHandler,
|
|
825
|
+
tabsBlockHandler,
|
|
826
|
+
componentBlockHandler
|
|
827
|
+
].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
828
|
+
var INLINE_HANDLERS = [
|
|
829
|
+
inlineComponentHandler
|
|
830
|
+
].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
831
|
+
function isParent(value) {
|
|
832
|
+
return Boolean(value && typeof value === "object" && "children" in value);
|
|
833
|
+
}
|
|
834
|
+
function isGeneratedMdxNode(value) {
|
|
835
|
+
const nodeType = value.type;
|
|
836
|
+
return nodeType === "mdxJsxFlowElement" || nodeType === "mdxJsxTextElement";
|
|
837
|
+
}
|
|
838
|
+
function transformInlineChildren(parent, source, options) {
|
|
839
|
+
const nextChildren = [];
|
|
840
|
+
let index = 0;
|
|
841
|
+
const componentNames = resolveCustomSyntaxComponentNames(options);
|
|
842
|
+
while (index < parent.children.length) {
|
|
843
|
+
const handler = INLINE_HANDLERS.find(
|
|
844
|
+
(candidate) => candidate.match(parent.children, index, {
|
|
845
|
+
componentNames,
|
|
846
|
+
createInlineTextElement,
|
|
847
|
+
createText,
|
|
848
|
+
getNodeSource: (node) => getNodeSource(source, node),
|
|
849
|
+
source
|
|
850
|
+
})
|
|
851
|
+
);
|
|
852
|
+
if (!handler) {
|
|
853
|
+
nextChildren.push(parent.children[index]);
|
|
854
|
+
index += 1;
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const result = handler.transform(parent.children, index, {
|
|
858
|
+
componentNames,
|
|
859
|
+
createInlineTextElement,
|
|
860
|
+
createText,
|
|
861
|
+
getNodeSource: (node) => getNodeSource(source, node),
|
|
862
|
+
source
|
|
863
|
+
});
|
|
864
|
+
nextChildren.push(...result.nodes);
|
|
865
|
+
index += result.consumed;
|
|
866
|
+
}
|
|
867
|
+
parent.children = nextChildren;
|
|
868
|
+
}
|
|
869
|
+
function transformBlockChildren(parent, source, transformTree2, options) {
|
|
870
|
+
const { lines } = createSourceLineResult(source);
|
|
871
|
+
const originalChildren = parent.children;
|
|
872
|
+
const nextChildren = [];
|
|
873
|
+
let index = 0;
|
|
874
|
+
const componentNames = resolveCustomSyntaxComponentNames(options);
|
|
875
|
+
while (index < originalChildren.length) {
|
|
876
|
+
const handler = BLOCK_HANDLERS.find(
|
|
877
|
+
(candidate) => candidate.match({
|
|
878
|
+
children: originalChildren,
|
|
879
|
+
componentNames,
|
|
880
|
+
getNodeSource: (node) => getNodeSource(source, node),
|
|
881
|
+
index,
|
|
882
|
+
lines,
|
|
883
|
+
source
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
if (!handler) {
|
|
887
|
+
nextChildren.push(originalChildren[index]);
|
|
888
|
+
index += 1;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const result = handler.transform({
|
|
892
|
+
children: originalChildren,
|
|
893
|
+
componentNames,
|
|
894
|
+
index,
|
|
895
|
+
lines,
|
|
896
|
+
source,
|
|
897
|
+
consumeThroughLine: (endLine) => {
|
|
898
|
+
let cursor = index;
|
|
899
|
+
while (cursor < originalChildren.length) {
|
|
900
|
+
const childEndLine = originalChildren[cursor]?.position?.end?.line;
|
|
901
|
+
if (typeof childEndLine === "number" && childEndLine >= endLine) {
|
|
902
|
+
return cursor - index + 1;
|
|
903
|
+
}
|
|
904
|
+
cursor += 1;
|
|
905
|
+
}
|
|
906
|
+
return originalChildren.length - index;
|
|
907
|
+
},
|
|
908
|
+
createFlowElement,
|
|
909
|
+
createInlineTextElement,
|
|
910
|
+
createText,
|
|
911
|
+
getLine: (lineNumber) => getLine(lines, lineNumber),
|
|
912
|
+
getNodeSource: (node) => getNodeSource(source, node),
|
|
913
|
+
parseYamlProps,
|
|
914
|
+
transformFragment: (markdown) => {
|
|
915
|
+
const fragmentTree = parseMarkdownFragment(markdown);
|
|
916
|
+
return transformTree2(fragmentTree, markdown, options).children;
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
nextChildren.push(...result.nodes);
|
|
920
|
+
index += result.consumed;
|
|
921
|
+
}
|
|
922
|
+
parent.children = nextChildren;
|
|
923
|
+
}
|
|
924
|
+
function transformTree(tree, sourceOverride = "", options) {
|
|
925
|
+
const source = sourceOverride;
|
|
926
|
+
const walk = (node) => {
|
|
927
|
+
if (!isParent(node)) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
transformBlockChildren(node, source, transformTree, options);
|
|
931
|
+
transformInlineChildren(node, source, options);
|
|
932
|
+
for (const child of node.children) {
|
|
933
|
+
if (isGeneratedMdxNode(child)) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
walk(child);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
walk(tree);
|
|
940
|
+
return tree;
|
|
941
|
+
}
|
|
942
|
+
function createCustomSyntaxRemarkPlugin(options) {
|
|
943
|
+
return (tree, file) => {
|
|
944
|
+
const source = typeof file?.value === "string" ? file.value : "";
|
|
945
|
+
transformTree(tree, source, options);
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/remark/supersub.ts
|
|
950
|
+
function isParent2(node) {
|
|
951
|
+
return Array.isArray(node.children);
|
|
952
|
+
}
|
|
953
|
+
function isWhitespace(char) {
|
|
954
|
+
return char !== void 0 && /\s/.test(char);
|
|
955
|
+
}
|
|
956
|
+
function canOpenMarker(value, index, marker) {
|
|
957
|
+
const previousChar = value[index - 1];
|
|
958
|
+
const nextChar = value[index + 1];
|
|
959
|
+
if (!nextChar || isWhitespace(nextChar)) {
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
if (marker === "~") {
|
|
963
|
+
return previousChar !== "~" && nextChar !== "~";
|
|
964
|
+
}
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
function isValidWrappedContent(content) {
|
|
968
|
+
return Boolean(content.trim()) && content.trim() === content;
|
|
969
|
+
}
|
|
970
|
+
function findClosingMarker(value, startIndex, marker) {
|
|
971
|
+
for (let index = startIndex; index < value.length; index += 1) {
|
|
972
|
+
if (value[index] !== marker) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const previousChar = value[index - 1];
|
|
976
|
+
const nextChar = value[index + 1];
|
|
977
|
+
if (previousChar === "\\") {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (marker === "~" && (previousChar === "~" || nextChar === "~")) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
const content = value.slice(startIndex, index);
|
|
984
|
+
if (isValidWrappedContent(content)) {
|
|
985
|
+
return index;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return -1;
|
|
989
|
+
}
|
|
990
|
+
function createTextNode(value) {
|
|
991
|
+
return { type: "text", value };
|
|
992
|
+
}
|
|
993
|
+
function createWrappedNode(marker, value) {
|
|
994
|
+
return {
|
|
995
|
+
type: "mdxJsxTextElement",
|
|
996
|
+
name: marker === "^" ? "sup" : "sub",
|
|
997
|
+
attributes: [],
|
|
998
|
+
children: [{ type: "text", value }]
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function transformTextNode(value) {
|
|
1002
|
+
const output = [];
|
|
1003
|
+
let buffer = "";
|
|
1004
|
+
let changed = false;
|
|
1005
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1006
|
+
const char = value[index];
|
|
1007
|
+
if ((char === "^" || char === "~") && canOpenMarker(value, index, char)) {
|
|
1008
|
+
const closingIndex = findClosingMarker(value, index + 1, char);
|
|
1009
|
+
if (closingIndex !== -1) {
|
|
1010
|
+
if (buffer) {
|
|
1011
|
+
output.push(createTextNode(buffer));
|
|
1012
|
+
buffer = "";
|
|
1013
|
+
}
|
|
1014
|
+
output.push(
|
|
1015
|
+
createWrappedNode(char, value.slice(index + 1, closingIndex))
|
|
1016
|
+
);
|
|
1017
|
+
index = closingIndex;
|
|
1018
|
+
changed = true;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
buffer += char;
|
|
1023
|
+
}
|
|
1024
|
+
if (!changed) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
if (buffer) {
|
|
1028
|
+
output.push(createTextNode(buffer));
|
|
1029
|
+
}
|
|
1030
|
+
return output;
|
|
1031
|
+
}
|
|
1032
|
+
function transformNode(node) {
|
|
1033
|
+
if (!isParent2(node)) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const nextChildren = [];
|
|
1037
|
+
for (const child of node.children) {
|
|
1038
|
+
if (child.type === "text" && typeof child.value === "string") {
|
|
1039
|
+
const transformed = transformTextNode(child.value);
|
|
1040
|
+
if (transformed) {
|
|
1041
|
+
nextChildren.push(...transformed);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
transformNode(child);
|
|
1046
|
+
nextChildren.push(child);
|
|
1047
|
+
}
|
|
1048
|
+
node.children = nextChildren;
|
|
1049
|
+
}
|
|
1050
|
+
function remarkSuperSub() {
|
|
1051
|
+
return (tree) => {
|
|
1052
|
+
transformNode(tree);
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/preset.ts
|
|
1057
|
+
function createMarkdownPreset(options) {
|
|
1058
|
+
const customSyntaxPlugin = options?.customSyntax ? function customSyntaxPresetPlugin() {
|
|
1059
|
+
return createCustomSyntaxRemarkPlugin(options.customSyntax);
|
|
1060
|
+
} : createCustomSyntaxRemarkPlugin;
|
|
1061
|
+
const codeFencePlugin = options?.codeFenceMappings ? function codeFencePresetPlugin() {
|
|
1062
|
+
return createCodeFenceComponentPlugin(options.codeFenceMappings);
|
|
1063
|
+
} : createCodeFenceComponentPlugin;
|
|
1064
|
+
const footnotesPlugin = options?.footnotes ? function footnotesPresetPlugin() {
|
|
1065
|
+
return rehypeFootnotesHeading(options.footnotes);
|
|
1066
|
+
} : rehypeFootnotesHeading;
|
|
1067
|
+
return {
|
|
1068
|
+
remarkPlugins: [
|
|
1069
|
+
customSyntaxPlugin,
|
|
1070
|
+
[remarkGfm2, { singleTilde: false }],
|
|
1071
|
+
remarkSuperSub
|
|
1072
|
+
],
|
|
1073
|
+
rehypePlugins: [
|
|
1074
|
+
codeFencePlugin,
|
|
1075
|
+
footnotesPlugin
|
|
1076
|
+
]
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/toc/extract-headings.ts
|
|
1081
|
+
import GithubSlugger from "github-slugger";
|
|
1082
|
+
import remarkGfm3 from "remark-gfm";
|
|
1083
|
+
import remarkParse2 from "remark-parse";
|
|
1084
|
+
import { unified as unified2 } from "unified";
|
|
1085
|
+
import { visit as visit3 } from "unist-util-visit";
|
|
1086
|
+
function extractText2(children) {
|
|
1087
|
+
return children.map((child) => {
|
|
1088
|
+
if ("value" in child && typeof child.value === "string") {
|
|
1089
|
+
return child.value;
|
|
1090
|
+
}
|
|
1091
|
+
if ("alt" in child && typeof child.alt === "string") {
|
|
1092
|
+
return child.alt;
|
|
1093
|
+
}
|
|
1094
|
+
if ("children" in child && Array.isArray(child.children)) {
|
|
1095
|
+
return extractText2(child.children);
|
|
1096
|
+
}
|
|
1097
|
+
return "";
|
|
1098
|
+
}).join("");
|
|
1099
|
+
}
|
|
1100
|
+
function extractHeadings(markdown, options) {
|
|
1101
|
+
const tree = unified2().use(remarkParse2).use(remarkGfm3).parse(markdown);
|
|
1102
|
+
const slugger = new GithubSlugger();
|
|
1103
|
+
const headings = [];
|
|
1104
|
+
const headingId = options?.headingId ?? FOOTNOTES_HEADING_ID;
|
|
1105
|
+
const headingText = options?.headingText ?? FOOTNOTES_HEADING_TEXT;
|
|
1106
|
+
let hasFootnotes = false;
|
|
1107
|
+
visit3(tree, "heading", (node) => {
|
|
1108
|
+
if (node.depth !== 1 && node.depth !== 2 && node.depth !== 3) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const text = extractText2(node.children).trim();
|
|
1112
|
+
if (!text) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
headings.push({
|
|
1116
|
+
depth: node.depth,
|
|
1117
|
+
text,
|
|
1118
|
+
id: slugger.slug(text)
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
visit3(tree, "footnoteDefinition", () => {
|
|
1122
|
+
hasFootnotes = true;
|
|
1123
|
+
});
|
|
1124
|
+
if (hasFootnotes) {
|
|
1125
|
+
headings.push({
|
|
1126
|
+
depth: resolveFootnotesHeadingDepth(
|
|
1127
|
+
headings.map((heading) => heading.depth)
|
|
1128
|
+
),
|
|
1129
|
+
text: headingText,
|
|
1130
|
+
id: headingId
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
return headings;
|
|
1134
|
+
}
|
|
1135
|
+
export {
|
|
1136
|
+
DEFAULT_CODE_FENCE_COMPONENT_MAPPINGS,
|
|
1137
|
+
DEFAULT_CUSTOM_SYNTAX_COMPONENT_NAMES,
|
|
1138
|
+
FOOTNOTES_HEADING_ID,
|
|
1139
|
+
FOOTNOTES_HEADING_TEXT,
|
|
1140
|
+
YAML_PROP_PREFIX,
|
|
1141
|
+
createCodeFenceComponentPlugin,
|
|
1142
|
+
createCustomSyntaxRemarkPlugin,
|
|
1143
|
+
createMarkdownPreset,
|
|
1144
|
+
extractHeadings,
|
|
1145
|
+
rehypeFootnotesHeading,
|
|
1146
|
+
remarkSuperSub,
|
|
1147
|
+
resolveCustomSyntaxComponentNames
|
|
1148
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luckyfishes/markdown-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core Markdown/MDX parsing and AST transformation utilities extracted from noname_blog.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"homepage": "https://github.com/lijiajunply/nonameblog-markdown-core#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/lijiajunply/nonameblog-markdown-core/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/lijiajunply/nonameblog-markdown-core.git"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
31
|
+
"prepare": "npm run build",
|
|
32
|
+
"test": "vitest run"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"github-slugger": "^2.0.0",
|
|
36
|
+
"gray-matter": "^4.0.3",
|
|
37
|
+
"remark-gfm": "^4.0.1",
|
|
38
|
+
"remark-parse": "^11.0.0",
|
|
39
|
+
"remark-rehype": "^11.1.2",
|
|
40
|
+
"unified": "^11.0.5",
|
|
41
|
+
"unist-util-visit": "^5.1.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/mdast": "^4.0.4",
|
|
45
|
+
"tsup": "^8.5.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^3.2.4"
|
|
48
|
+
}
|
|
49
|
+
}
|