mardora 1.2.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chunk-3OCUX4OO.js +7690 -0
  4. package/dist/chunk-3OCUX4OO.js.map +1 -0
  5. package/dist/chunk-3ZOCCFDL.cjs +74 -0
  6. package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
  7. package/dist/chunk-7JOEPNEV.cjs +7740 -0
  8. package/dist/chunk-7JOEPNEV.cjs.map +1 -0
  9. package/dist/chunk-BIKZQZ6W.js +33 -0
  10. package/dist/chunk-BIKZQZ6W.js.map +1 -0
  11. package/dist/chunk-EQJESPP2.js +234 -0
  12. package/dist/chunk-EQJESPP2.js.map +1 -0
  13. package/dist/chunk-G4SE26YY.js +70 -0
  14. package/dist/chunk-G4SE26YY.js.map +1 -0
  15. package/dist/chunk-KNDWF2DP.cjs +35 -0
  16. package/dist/chunk-KNDWF2DP.cjs.map +1 -0
  17. package/dist/chunk-MLBEBFHB.cjs +2971 -0
  18. package/dist/chunk-MLBEBFHB.cjs.map +1 -0
  19. package/dist/chunk-P7JFCYU3.js +905 -0
  20. package/dist/chunk-P7JFCYU3.js.map +1 -0
  21. package/dist/chunk-SWFUKJDO.cjs +243 -0
  22. package/dist/chunk-SWFUKJDO.cjs.map +1 -0
  23. package/dist/chunk-WFVCG4LD.cjs +926 -0
  24. package/dist/chunk-WFVCG4LD.cjs.map +1 -0
  25. package/dist/chunk-XL6WFGJT.js +2901 -0
  26. package/dist/chunk-XL6WFGJT.js.map +1 -0
  27. package/dist/editor/index.cjs +277 -0
  28. package/dist/editor/index.cjs.map +1 -0
  29. package/dist/editor/index.d.cts +186 -0
  30. package/dist/editor/index.d.ts +186 -0
  31. package/dist/editor/index.js +4 -0
  32. package/dist/editor/index.js.map +1 -0
  33. package/dist/index.cjs +405 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.cts +13 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lib/index.cjs +12 -0
  40. package/dist/lib/index.cjs.map +1 -0
  41. package/dist/lib/index.d.cts +16 -0
  42. package/dist/lib/index.d.ts +16 -0
  43. package/dist/lib/index.js +3 -0
  44. package/dist/lib/index.js.map +1 -0
  45. package/dist/mardora-DCwjomil.d.cts +640 -0
  46. package/dist/mardora-DCwjomil.d.ts +640 -0
  47. package/dist/plugins/index.cjs +104 -0
  48. package/dist/plugins/index.cjs.map +1 -0
  49. package/dist/plugins/index.d.cts +740 -0
  50. package/dist/plugins/index.d.ts +740 -0
  51. package/dist/plugins/index.js +7 -0
  52. package/dist/plugins/index.js.map +1 -0
  53. package/dist/preview/index.cjs +38 -0
  54. package/dist/preview/index.cjs.map +1 -0
  55. package/dist/preview/index.d.cts +101 -0
  56. package/dist/preview/index.d.ts +101 -0
  57. package/dist/preview/index.js +5 -0
  58. package/dist/preview/index.js.map +1 -0
  59. package/dist/types-NBsaxl4d.d.cts +71 -0
  60. package/dist/types-Pw2SWWAR.d.ts +71 -0
  61. package/package.json +92 -0
  62. package/src/editor/attachments/extension.ts +181 -0
  63. package/src/editor/attachments/format.ts +63 -0
  64. package/src/editor/attachments/index.ts +3 -0
  65. package/src/editor/attachments/types.ts +37 -0
  66. package/src/editor/heading-fold/config.ts +25 -0
  67. package/src/editor/heading-fold/extension.ts +268 -0
  68. package/src/editor/heading-fold/extract.ts +88 -0
  69. package/src/editor/heading-fold/index.ts +5 -0
  70. package/src/editor/heading-fold/theme.ts +85 -0
  71. package/src/editor/heading-fold/types.ts +24 -0
  72. package/src/editor/i18n.ts +13 -0
  73. package/src/editor/icons/index.ts +367 -0
  74. package/src/editor/index.ts +16 -0
  75. package/src/editor/mardora.ts +257 -0
  76. package/src/editor/media-lightbox-theme.ts +146 -0
  77. package/src/editor/media-lightbox.ts +125 -0
  78. package/src/editor/plugin.ts +294 -0
  79. package/src/editor/selection-toolbar/activation.ts +123 -0
  80. package/src/editor/selection-toolbar/commands.ts +279 -0
  81. package/src/editor/selection-toolbar/extension.ts +564 -0
  82. package/src/editor/selection-toolbar/i18n.ts +164 -0
  83. package/src/editor/selection-toolbar/index.ts +7 -0
  84. package/src/editor/selection-toolbar/menu.ts +252 -0
  85. package/src/editor/selection-toolbar/position.ts +43 -0
  86. package/src/editor/selection-toolbar/theme.ts +195 -0
  87. package/src/editor/selection-toolbar/types.ts +155 -0
  88. package/src/editor/slash/default-commands.ts +190 -0
  89. package/src/editor/slash/extension.ts +319 -0
  90. package/src/editor/slash/index.ts +7 -0
  91. package/src/editor/slash/insertions.ts +26 -0
  92. package/src/editor/slash/menu.ts +123 -0
  93. package/src/editor/slash/position.ts +61 -0
  94. package/src/editor/slash/query.ts +33 -0
  95. package/src/editor/slash/theme.ts +113 -0
  96. package/src/editor/slash/types.ts +40 -0
  97. package/src/editor/table-of-contents/extension.ts +202 -0
  98. package/src/editor/table-of-contents/extract.ts +53 -0
  99. package/src/editor/table-of-contents/index.ts +7 -0
  100. package/src/editor/table-of-contents/panel.ts +83 -0
  101. package/src/editor/table-of-contents/slug.ts +50 -0
  102. package/src/editor/table-of-contents/storage.ts +35 -0
  103. package/src/editor/table-of-contents/theme.ts +153 -0
  104. package/src/editor/table-of-contents/types.ts +44 -0
  105. package/src/editor/theme.ts +72 -0
  106. package/src/editor/utils.ts +176 -0
  107. package/src/editor/view-plugin.ts +189 -0
  108. package/src/index.ts +5 -0
  109. package/src/lib/index.ts +2 -0
  110. package/src/lib/input-handler.ts +47 -0
  111. package/src/plugins/code-plugin.theme.ts +545 -0
  112. package/src/plugins/code-plugin.ts +1892 -0
  113. package/src/plugins/emoji-plugin.ts +140 -0
  114. package/src/plugins/heading-plugin.ts +194 -0
  115. package/src/plugins/hr-plugin.ts +102 -0
  116. package/src/plugins/html-plugin.ts +353 -0
  117. package/src/plugins/image-plugin.ts +806 -0
  118. package/src/plugins/index.ts +71 -0
  119. package/src/plugins/inline-plugin.ts +311 -0
  120. package/src/plugins/link-plugin.ts +509 -0
  121. package/src/plugins/list-plugin.ts +492 -0
  122. package/src/plugins/math-plugin.ts +526 -0
  123. package/src/plugins/mermaid-plugin.ts +513 -0
  124. package/src/plugins/paragraph-plugin.ts +38 -0
  125. package/src/plugins/quote-plugin.ts +733 -0
  126. package/src/plugins/table-controls-theme.ts +126 -0
  127. package/src/plugins/table-controls.ts +423 -0
  128. package/src/plugins/table-model.ts +661 -0
  129. package/src/plugins/table-plugin.ts +2111 -0
  130. package/src/preview/context.ts +45 -0
  131. package/src/preview/css-generator.ts +64 -0
  132. package/src/preview/default-renderers.ts +29 -0
  133. package/src/preview/index.ts +29 -0
  134. package/src/preview/preview.ts +41 -0
  135. package/src/preview/renderer.ts +184 -0
  136. package/src/preview/syntax-theme.ts +112 -0
  137. package/src/preview/toc.ts +23 -0
  138. package/src/preview/types.ts +89 -0
@@ -0,0 +1,45 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { Highlighter } from "@lezer/highlight";
3
+ import { ThemeEnum } from "../editor/utils";
4
+ import { PreviewContext } from "./types";
5
+ import DOMPurify from "dompurify";
6
+
7
+ /**
8
+ * Creates a PreviewContext for rendering
9
+ */
10
+ export function createPreviewContext(
11
+ doc: string,
12
+ theme: ThemeEnum,
13
+ renderChildren: (node: SyntaxNode) => Promise<string>,
14
+ sanitizeHtml: boolean = true,
15
+ syntaxHighlighters: readonly Highlighter[] = [],
16
+ headingIdForNode?: (node: SyntaxNode) => string | null
17
+ ): PreviewContext {
18
+ const context: PreviewContext = {
19
+ doc,
20
+ theme,
21
+ syntaxHighlighters,
22
+
23
+ sliceDoc(from: number, to: number): string {
24
+ return doc.slice(from, to);
25
+ },
26
+
27
+ sanitize(html: string): string {
28
+ if (!sanitizeHtml) return html;
29
+
30
+ // DOMPurify works in browser; in Node, it needs jsdom
31
+ if (typeof window !== "undefined") {
32
+ return DOMPurify.sanitize(html);
33
+ }
34
+
35
+ // Server-side: return as-is (user should sanitize at application level)
36
+ // or use isomorphic-dompurify in their setup
37
+ return html;
38
+ },
39
+
40
+ renderChildren,
41
+ };
42
+
43
+ if (headingIdForNode) context.headingIdForNode = headingIdForNode;
44
+ return context;
45
+ }
@@ -0,0 +1,64 @@
1
+ import { ThemeEnum } from "../editor/utils";
2
+ import { GenerateCSSConfig } from "./types";
3
+ import { generateSyntaxThemeCSS } from "./syntax-theme";
4
+
5
+ /**
6
+ * Base CSS styles for preview rendering
7
+ */
8
+ const baseStyles = `.mardora-preview {
9
+ padding: 0 0.5rem;
10
+ }`;
11
+
12
+ /**
13
+ * Generate CSS for preview rendering
14
+ *
15
+ * @param config - CSS generation configuration
16
+ * @returns CSS string
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { generateCSS } from 'mardora/preview';
21
+ * import { HeadingPlugin, ListPlugin } from 'mardora/plugins';
22
+ *
23
+ * const css = generateCSS({
24
+ * plugins: [new HeadingPlugin(), new ListPlugin()],
25
+ * theme: ThemeEnum.AUTO,
26
+ * includeBase: true,
27
+ * });
28
+ * ```
29
+ */
30
+ export function generateCSS(config: GenerateCSSConfig = {}): string {
31
+ const {
32
+ plugins = [],
33
+ theme = ThemeEnum.AUTO,
34
+ wrapperClass = "mardora-preview",
35
+ includeBase = true,
36
+ syntaxTheme,
37
+ } = config;
38
+
39
+ const cssChunks: string[] = [];
40
+
41
+ // Include base styles
42
+ if (includeBase) {
43
+ // Replace default wrapper class if custom one is provided
44
+ if (wrapperClass !== "mardora-preview") {
45
+ cssChunks.push(baseStyles.replace(/\.mardora-preview/g, `.${wrapperClass}`));
46
+ } else {
47
+ cssChunks.push(baseStyles);
48
+ }
49
+ }
50
+
51
+ // Collect syntax highlight styles (`tok-*` classes) from CodeMirror theme/extensions
52
+ const syntaxCSS = generateSyntaxThemeCSS(syntaxTheme, wrapperClass);
53
+ if (syntaxCSS) {
54
+ cssChunks.push("/* syntax-theme */\n" + syntaxCSS);
55
+ }
56
+
57
+ // Collect styles from plugins
58
+ for (const plugin of plugins) {
59
+ const pluginCSS = plugin.getPreviewStyles(theme, wrapperClass);
60
+ if (pluginCSS) cssChunks.push(`/* ${plugin.name} - ${plugin.version} */\n` + pluginCSS);
61
+ }
62
+
63
+ return cssChunks.join("\n\n");
64
+ }
@@ -0,0 +1,29 @@
1
+ import { NodeRenderer, NodeRendererMap } from "./types";
2
+
3
+ /**
4
+ * Escape HTML special characters
5
+ */
6
+ export function escapeHtml(text: string): string {
7
+ return text
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&#39;");
13
+ }
14
+
15
+ // ============================================
16
+ // DEFAULT RENDERERS
17
+ // ============================================
18
+
19
+ const renderDocument: NodeRenderer = (_node, children) => {
20
+ return children;
21
+ };
22
+
23
+ /**
24
+ * Default node renderers for all markdown node types
25
+ */
26
+ export const defaultRenderers: NodeRendererMap = {
27
+ // Document structure
28
+ Document: renderDocument,
29
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * mardora/preview - Static HTML rendering for markdown
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ // Main preview function
8
+ export { preview } from "./preview";
9
+
10
+ // CSS generation
11
+ export { generateCSS } from "./css-generator";
12
+ export { generateSyntaxThemeCSS } from "./syntax-theme";
13
+ export { extractPreviewTocFromMarkdown } from "./toc";
14
+
15
+ // Types
16
+ export type {
17
+ PreviewConfig,
18
+ PreviewContext,
19
+ GenerateCSSConfig,
20
+ SyntaxThemeInput,
21
+ NodeRenderer,
22
+ NodeRendererMap,
23
+ } from "./types";
24
+
25
+ // Utilities
26
+ export { escapeHtml, defaultRenderers } from "./default-renderers";
27
+
28
+ // Renderer class (for advanced usage)
29
+ export { PreviewRenderer } from "./renderer";
@@ -0,0 +1,41 @@
1
+ import { ThemeEnum } from "../editor/utils";
2
+ import { PreviewRenderer } from "./renderer";
3
+ import { PreviewConfig } from "./types";
4
+
5
+ /**
6
+ * Render markdown to semantic HTML
7
+ *
8
+ * @param markdown - Markdown string to render
9
+ * @param config - Preview configuration
10
+ * @returns HTML string
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { preview } from 'mardora/preview';
15
+ * import { HeadingPlugin, ListPlugin } from 'mardora/plugins';
16
+ *
17
+ * const html = preview('# Hello World', {
18
+ * plugins: [new HeadingPlugin(), new ListPlugin()],
19
+ * wrapperClass: 'mardora-preview',
20
+ * });
21
+ * ```
22
+ */
23
+ export async function preview(markdown: string, config: PreviewConfig = {}): Promise<string> {
24
+ const {
25
+ plugins = [],
26
+ markdown: markdownConfig = [],
27
+ wrapperClass = "mardora-preview",
28
+ wrapperTag = "article",
29
+ sanitize = true,
30
+ theme = ThemeEnum.AUTO,
31
+ syntaxTheme,
32
+ } = config;
33
+
34
+ // Create renderer and generate HTML
35
+ const renderer = new PreviewRenderer(markdown, plugins, markdownConfig, theme, sanitize, syntaxTheme);
36
+ const content = await renderer.render();
37
+
38
+ // Wrap in container
39
+ const classAttr = wrapperClass ? ` class="${wrapperClass}"` : "";
40
+ return `<${wrapperTag}${classAttr}>\n${content}</${wrapperTag}>`;
41
+ }
@@ -0,0 +1,184 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
3
+ import { EditorState } from "@codemirror/state";
4
+ import { MarkdownConfig } from "@lezer/markdown";
5
+ import { languages } from "@codemirror/language-data";
6
+
7
+ import { MardoraPlugin } from "../editor/plugin";
8
+ import { extractTocItemsFromState } from "../editor/table-of-contents";
9
+ import { ThemeEnum } from "../editor/utils";
10
+ import { createPreviewContext } from "./context";
11
+ import { defaultRenderers, escapeHtml } from "./default-renderers";
12
+ import { resolveSyntaxHighlighters } from "./syntax-theme";
13
+ import { NodeRendererMap, PreviewContext } from "./types";
14
+
15
+ /**
16
+ * Renderer class that walks the syntax tree and produces HTML
17
+ */
18
+ export class PreviewRenderer {
19
+ private doc: string;
20
+ private theme: ThemeEnum;
21
+ private plugins: MardoraPlugin[];
22
+ private markdown: MarkdownConfig[];
23
+ private sanitizeHtml: boolean;
24
+ private syntaxTheme: import("./types").SyntaxThemeInput | import("./types").SyntaxThemeInput[] | undefined;
25
+ private renderers: NodeRendererMap;
26
+ private ctx: PreviewContext;
27
+ private nodeToPlugins: Map<string, MardoraPlugin[]>;
28
+
29
+ constructor(
30
+ doc: string,
31
+ plugins: MardoraPlugin[] = [],
32
+ markdown: MarkdownConfig[],
33
+ theme: ThemeEnum = ThemeEnum.AUTO,
34
+ sanitize: boolean = true,
35
+ syntaxTheme?: import("./types").SyntaxThemeInput | import("./types").SyntaxThemeInput[]
36
+ ) {
37
+ this.doc = doc;
38
+ this.theme = theme;
39
+ this.plugins = plugins;
40
+ this.markdown = markdown;
41
+ this.sanitizeHtml = sanitize;
42
+ this.syntaxTheme = syntaxTheme;
43
+ this.renderers = { ...defaultRenderers };
44
+
45
+ const syntaxHighlighters = resolveSyntaxHighlighters(this.syntaxTheme, true);
46
+
47
+ // Create context with reference to renderChildren
48
+ this.ctx = createPreviewContext(doc, theme, this.renderChildren.bind(this), sanitize, syntaxHighlighters);
49
+
50
+ // Build node-to-plugin map for O(1) lookup
51
+ this.nodeToPlugins = this.buildNodePluginMap();
52
+ }
53
+
54
+ /**
55
+ * Build a map from node names to plugins that handle them
56
+ */
57
+ private buildNodePluginMap(): Map<string, MardoraPlugin[]> {
58
+ const map = new Map<string, MardoraPlugin[]>();
59
+ for (const plugin of this.plugins) {
60
+ if (plugin.renderToHTML && plugin.requiredNodes.length > 0) {
61
+ for (const nodeName of plugin.requiredNodes) {
62
+ const list = map.get(nodeName) || [];
63
+ list.push(plugin);
64
+ map.set(nodeName, list);
65
+ }
66
+ }
67
+ }
68
+ return map;
69
+ }
70
+
71
+ /**
72
+ * Render the document to HTML
73
+ */
74
+ async render(): Promise<string> {
75
+ // Collect markdown extensions from plugins
76
+ const extensions = [
77
+ ...this.markdown,
78
+ ...this.plugins.map((p) => p.getMarkdownConfig()).filter((ext): ext is NonNullable<typeof ext> => ext !== null),
79
+ ];
80
+
81
+ // Build parser through @codemirror/lang-markdown to match editor behavior exactly
82
+ const markdownSupport = markdown({
83
+ base: markdownLanguage,
84
+ codeLanguages: languages,
85
+ extensions,
86
+ addKeymap: true,
87
+ completeHTMLTags: true,
88
+ pasteURLAsLink: true,
89
+ });
90
+ const parser = markdownSupport.language.parser;
91
+
92
+ // Parse the document
93
+ const tree = parser.parse(this.doc);
94
+ const state = EditorState.create({
95
+ doc: this.doc,
96
+ extensions: [markdownSupport],
97
+ });
98
+ const tocItems = extractTocItemsFromState(state);
99
+ const headingIds = new Map(tocItems.map((item) => [`${item.from}:${item.to}`, item.id]));
100
+ const syntaxHighlighters = resolveSyntaxHighlighters(this.syntaxTheme, true);
101
+
102
+ this.ctx = createPreviewContext(
103
+ this.doc,
104
+ this.theme,
105
+ this.renderChildren.bind(this),
106
+ this.sanitizeHtml,
107
+ syntaxHighlighters,
108
+ (node) => headingIds.get(`${node.from}:${node.to}`) ?? null
109
+ );
110
+
111
+ // Render from root
112
+ return await this.renderNode(tree.topNode);
113
+ }
114
+
115
+ /**
116
+ * Render a single node to HTML
117
+ */
118
+ private async renderNode(node: SyntaxNode): Promise<string> {
119
+ return (await this.renderNodeWithExtent(node)).html;
120
+ }
121
+
122
+ private async renderNodeWithExtent(node: SyntaxNode): Promise<{ html: string; to: number }> {
123
+ // Get plugins that handle this node type (O(1) lookup)
124
+ const plugins = this.nodeToPlugins.get(node.name);
125
+ if (plugins) {
126
+ for (const plugin of plugins) {
127
+ const children = await this.renderChildren(node);
128
+ const result = await plugin.renderToHTML!(node, children, this.ctx);
129
+ if (result !== null) {
130
+ return {
131
+ html: result,
132
+ to: Math.max(node.to, plugin.getPreviewConsumedTo?.(node, this.ctx) ?? node.to),
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ // Use default renderer
139
+ const renderer = this.renderers[node.name];
140
+ if (renderer) {
141
+ const children = await this.renderChildren(node);
142
+ return { html: renderer(node, children, this.ctx), to: node.to };
143
+ }
144
+
145
+ // Unknown node - render children or text
146
+ if (node.firstChild) {
147
+ return { html: await this.renderChildren(node), to: node.to };
148
+ }
149
+
150
+ // Leaf node - return text content
151
+ return { html: this.ctx.sliceDoc(node.from, node.to), to: node.to };
152
+ }
153
+
154
+ /**
155
+ * Render all children of a node, including text between nodes
156
+ */
157
+ private async renderChildren(node: SyntaxNode): Promise<string> {
158
+ let result = "";
159
+ let pos = node.from; // Track position to find text gaps
160
+ let child = node.firstChild;
161
+
162
+ while (child) {
163
+ // Add any text between the last position and this child
164
+ if (child.from > pos) {
165
+ result += escapeHtml(this.ctx.sliceDoc(pos, child.from));
166
+ }
167
+
168
+ // Render the child node
169
+ const rendered = await this.renderNodeWithExtent(child);
170
+ result += rendered.html;
171
+
172
+ // Update position to end of this child
173
+ pos = rendered.to;
174
+ child = child.nextSibling;
175
+ }
176
+
177
+ // Add any trailing text after the last child
178
+ if (pos < node.to) {
179
+ result += escapeHtml(this.ctx.sliceDoc(pos, node.to));
180
+ }
181
+
182
+ return result;
183
+ }
184
+ }
@@ -0,0 +1,112 @@
1
+ import { classHighlighter, Highlighter } from "@lezer/highlight";
2
+ import type { SyntaxThemeInput } from "./types";
3
+
4
+ type HighlightSpec = {
5
+ tag?: unknown;
6
+ class?: string;
7
+ [key: string]: unknown;
8
+ };
9
+
10
+ type RuntimeHighlightStyle = {
11
+ specs?: HighlightSpec[];
12
+ style?: (tags: readonly import("@lezer/highlight").Tag[]) => string | null;
13
+ module?: { getRules(): string } | null;
14
+ };
15
+
16
+ const MAX_WALK_DEPTH = 8;
17
+
18
+ /**
19
+ * Extract syntax highlight CSS from resolved CodeMirror HighlightStyle modules.
20
+ */
21
+ export function generateSyntaxThemeCSS(
22
+ syntaxTheme: SyntaxThemeInput | SyntaxThemeInput[] | undefined,
23
+ _wrapperClass: string
24
+ ): string {
25
+ void _wrapperClass;
26
+ if (!syntaxTheme) return "";
27
+
28
+ const styles = extractRuntimeHighlightStyles(syntaxTheme);
29
+ if (!styles.length) return "";
30
+
31
+ const cssChunks: string[] = [];
32
+
33
+ for (const style of styles) {
34
+ const rules = style.module?.getRules();
35
+ if (!rules) continue;
36
+ cssChunks.push(rules);
37
+ }
38
+
39
+ if (!cssChunks.length) return "";
40
+
41
+ return Array.from(new Set(cssChunks)).join("\n");
42
+ }
43
+
44
+ export function resolveSyntaxHighlighters(
45
+ syntaxTheme: SyntaxThemeInput | SyntaxThemeInput[] | undefined,
46
+ includeLegacyClassHighlighter: boolean = true
47
+ ): readonly Highlighter[] {
48
+ const resolved: Highlighter[] = [];
49
+ if (includeLegacyClassHighlighter) {
50
+ resolved.push(classHighlighter);
51
+ }
52
+
53
+ const styles = extractRuntimeHighlightStyles(syntaxTheme);
54
+ for (const style of styles) {
55
+ if (typeof style.style === "function") {
56
+ resolved.push(style as unknown as Highlighter);
57
+ }
58
+ }
59
+
60
+ return Array.from(new Set(resolved));
61
+ }
62
+
63
+ function extractRuntimeHighlightStyles(
64
+ input: SyntaxThemeInput | SyntaxThemeInput[] | undefined
65
+ ): RuntimeHighlightStyle[] {
66
+ if (!input) return [];
67
+
68
+ const values = Array.isArray(input) ? input : [input];
69
+ const styles: RuntimeHighlightStyle[] = [];
70
+ const visited = new WeakSet<object>();
71
+
72
+ for (const value of values) {
73
+ walk(value, 0, visited, styles);
74
+ }
75
+
76
+ return styles;
77
+ }
78
+
79
+ function walk(value: unknown, depth: number, visited: WeakSet<object>, out: RuntimeHighlightStyle[]): void {
80
+ if (value === null || value === undefined) return;
81
+ if (depth > MAX_WALK_DEPTH) return;
82
+
83
+ if (isRuntimeHighlightStyle(value)) {
84
+ out.push(value);
85
+ }
86
+
87
+ if (Array.isArray(value)) {
88
+ for (const item of value) {
89
+ walk(item, depth + 1, visited, out);
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (typeof value !== "object") return;
95
+ if (visited.has(value)) return;
96
+ visited.add(value);
97
+
98
+ const keys = Object.getOwnPropertyNames(value);
99
+ for (const key of keys) {
100
+ try {
101
+ walk((value as Record<string, unknown>)[key], depth + 1, visited, out);
102
+ } catch {
103
+ // Ignore inaccessible properties
104
+ }
105
+ }
106
+ }
107
+
108
+ function isRuntimeHighlightStyle(value: unknown): value is RuntimeHighlightStyle {
109
+ if (!value || typeof value !== "object") return false;
110
+ const style = value as RuntimeHighlightStyle;
111
+ return Array.isArray(style.specs) && typeof style.style === "function";
112
+ }
@@ -0,0 +1,23 @@
1
+ import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
2
+ import { EditorState } from "@codemirror/state";
3
+ import type { MarkdownConfig } from "@lezer/markdown";
4
+ import type { MardoraTocConfig, MardoraTocItem } from "../editor/table-of-contents";
5
+ import { extractTocItemsFromState } from "../editor/table-of-contents";
6
+
7
+ export function extractPreviewTocFromMarkdown(
8
+ doc: string,
9
+ config: MardoraTocConfig = {},
10
+ markdownConfig: MarkdownConfig[] = []
11
+ ): MardoraTocItem[] {
12
+ const state = EditorState.create({
13
+ doc,
14
+ extensions: [markdown({ base: markdownLanguage, extensions: markdownConfig })],
15
+ });
16
+
17
+ return extractTocItemsFromState(state, config).map((item) => ({
18
+ id: item.id,
19
+ level: item.level,
20
+ text: item.text,
21
+ active: item.active,
22
+ }));
23
+ }
@@ -0,0 +1,89 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { ThemeEnum } from "../editor/utils";
3
+
4
+ export type SyntaxThemeInput =
5
+ | import("@codemirror/language").HighlightStyle
6
+ | import("@codemirror/state").Extension
7
+ | readonly import("@codemirror/state").Extension[];
8
+
9
+ /**
10
+ * Context passed to plugin preview methods
11
+ */
12
+ export interface PreviewContext {
13
+ /** Full document text */
14
+ readonly doc: string;
15
+
16
+ /** Current theme */
17
+ readonly theme: ThemeEnum;
18
+
19
+ /** Slice document text between positions */
20
+ sliceDoc(from: number, to: number): string;
21
+
22
+ /** Sanitize HTML content (for HTMLBlock/HTMLTag) */
23
+ sanitize(html: string): string;
24
+
25
+ /** Render children of a node to HTML */
26
+ renderChildren(node: SyntaxNode): Promise<string>;
27
+
28
+ /** Active syntax highlighters used for code rendering */
29
+ readonly syntaxHighlighters?: readonly import("@lezer/highlight").Highlighter[];
30
+
31
+ /** Return a stable heading id for TOC-aware heading renderers */
32
+ headingIdForNode?(node: SyntaxNode): string | null;
33
+ }
34
+
35
+ /**
36
+ * Configuration for the preview renderer
37
+ */
38
+ export interface PreviewConfig {
39
+ /** Plugins to use for rendering */
40
+ plugins?: import("../editor/plugin").MardoraPlugin[];
41
+
42
+ /** Markdown extensions to use for rendering */
43
+ markdown?: import("@lezer/markdown").MarkdownConfig[];
44
+
45
+ /** CSS class for the wrapper element */
46
+ wrapperClass?: string;
47
+
48
+ /** HTML tag for the wrapper element */
49
+ wrapperTag?: "article" | "div" | "section";
50
+
51
+ /** Whether to sanitize HTML blocks (default: true) */
52
+ sanitize?: boolean;
53
+
54
+ /** Theme to use */
55
+ theme?: ThemeEnum;
56
+
57
+ /** CodeMirror syntax theme input used for static preview highlighting */
58
+ syntaxTheme?: SyntaxThemeInput | SyntaxThemeInput[];
59
+ }
60
+
61
+ /**
62
+ * Result of CSS generation
63
+ */
64
+ export interface GenerateCSSConfig {
65
+ /** Plugins to extract styles from */
66
+ plugins?: import("../editor/plugin").MardoraPlugin[];
67
+
68
+ /** Theme to use */
69
+ theme?: ThemeEnum;
70
+
71
+ /** Wrapper class for scoping (default: "mardora-preview") */
72
+ wrapperClass?: string;
73
+
74
+ /** Include base styles */
75
+ includeBase?: boolean;
76
+
77
+ /** CodeMirror syntax theme input used for static preview syntax highlighting */
78
+ syntaxTheme?: SyntaxThemeInput | SyntaxThemeInput[];
79
+ }
80
+
81
+ /**
82
+ * Node renderer function type
83
+ */
84
+ export type NodeRenderer = (node: SyntaxNode, children: string, ctx: PreviewContext) => string;
85
+
86
+ /**
87
+ * Map of node names to their renderers
88
+ */
89
+ export type NodeRendererMap = Record<string, NodeRenderer>;