draftly 0.1.0-alpha.0 → 1.0.0-alpha.2

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 (74) hide show
  1. package/README.md +346 -0
  2. package/dist/chunk-2B3A3VSQ.cjs +3382 -0
  3. package/dist/chunk-2B3A3VSQ.cjs.map +1 -0
  4. package/dist/chunk-72ZYRGRT.cjs +399 -0
  5. package/dist/chunk-72ZYRGRT.cjs.map +1 -0
  6. package/dist/chunk-CG4M4TC7.js +392 -0
  7. package/dist/chunk-CG4M4TC7.js.map +1 -0
  8. package/dist/chunk-DFQYXFOP.js +86 -0
  9. package/dist/chunk-DFQYXFOP.js.map +1 -0
  10. package/dist/chunk-HPSMS2WB.js +182 -0
  11. package/dist/chunk-HPSMS2WB.js.map +1 -0
  12. package/dist/chunk-KBQDZ5IW.cjs +192 -0
  13. package/dist/chunk-KBQDZ5IW.cjs.map +1 -0
  14. package/dist/chunk-KDEDLC3D.cjs +93 -0
  15. package/dist/chunk-KDEDLC3D.cjs.map +1 -0
  16. package/dist/chunk-N3WL3XPB.js +3360 -0
  17. package/dist/chunk-N3WL3XPB.js.map +1 -0
  18. package/dist/draftly-BLnx3uGX.d.cts +293 -0
  19. package/dist/draftly-BLnx3uGX.d.ts +293 -0
  20. package/dist/editor/index.cjs +57 -0
  21. package/dist/editor/index.cjs.map +1 -0
  22. package/dist/editor/index.d.cts +15 -0
  23. package/dist/editor/index.d.ts +15 -0
  24. package/dist/editor/index.js +4 -0
  25. package/dist/editor/index.js.map +1 -0
  26. package/dist/index.cjs +120 -1129
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +9 -257
  29. package/dist/index.d.ts +9 -257
  30. package/dist/index.js +4 -1126
  31. package/dist/index.js.map +1 -1
  32. package/dist/plugins/index.cjs +66 -0
  33. package/dist/plugins/index.cjs.map +1 -0
  34. package/dist/plugins/index.d.cts +515 -0
  35. package/dist/plugins/index.d.ts +515 -0
  36. package/dist/plugins/index.js +5 -0
  37. package/dist/plugins/index.js.map +1 -0
  38. package/dist/preview/index.cjs +29 -0
  39. package/dist/preview/index.cjs.map +1 -0
  40. package/dist/preview/index.d.cts +143 -0
  41. package/dist/preview/index.d.ts +143 -0
  42. package/dist/preview/index.js +4 -0
  43. package/dist/preview/index.js.map +1 -0
  44. package/package.json +22 -1
  45. package/src/{draftly.ts → editor/draftly.ts} +28 -27
  46. package/src/editor/index.ts +5 -0
  47. package/src/{plugin.ts → editor/plugin.ts} +62 -34
  48. package/src/editor/theme.ts +62 -0
  49. package/src/editor/utils.ts +143 -0
  50. package/src/{view-plugin.ts → editor/view-plugin.ts} +25 -140
  51. package/src/index.ts +4 -7
  52. package/src/plugins/code-plugin.ts +1119 -0
  53. package/src/plugins/heading-plugin.ts +108 -74
  54. package/src/plugins/hr-plugin.ts +102 -0
  55. package/src/plugins/html-plugin.ts +59 -53
  56. package/src/plugins/image-plugin.ts +447 -0
  57. package/src/plugins/index.ts +57 -0
  58. package/src/plugins/inline-plugin.ts +178 -39
  59. package/src/plugins/link-plugin.ts +509 -0
  60. package/src/plugins/list-plugin.ts +492 -211
  61. package/src/plugins/math-plugin.ts +514 -0
  62. package/src/plugins/mermaid-plugin.ts +500 -0
  63. package/src/plugins/paragraph-plugin.ts +38 -0
  64. package/src/plugins/quote-plugin.ts +146 -0
  65. package/src/preview/context.ts +38 -0
  66. package/src/preview/css-generator.ts +51 -0
  67. package/src/preview/default-renderers.ts +29 -0
  68. package/src/preview/index.ts +20 -0
  69. package/src/preview/preview.ts +40 -0
  70. package/src/preview/renderer.ts +157 -0
  71. package/src/preview/types.ts +72 -0
  72. package/src/plugins/plugins.ts +0 -9
  73. package/src/theme.ts +0 -86
  74. package/src/utils.ts +0 -21
@@ -0,0 +1,38 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { ThemeEnum } from "../editor/utils";
3
+ import { PreviewContext } from "./types";
4
+ import DOMPurify from "dompurify";
5
+
6
+ /**
7
+ * Creates a PreviewContext for rendering
8
+ */
9
+ export function createPreviewContext(
10
+ doc: string,
11
+ theme: ThemeEnum,
12
+ renderChildren: (node: SyntaxNode) => Promise<string>,
13
+ sanitizeHtml: boolean = true
14
+ ): PreviewContext {
15
+ return {
16
+ doc,
17
+ theme,
18
+
19
+ sliceDoc(from: number, to: number): string {
20
+ return doc.slice(from, to);
21
+ },
22
+
23
+ sanitize(html: string): string {
24
+ if (!sanitizeHtml) return html;
25
+
26
+ // DOMPurify works in browser; in Node, it needs jsdom
27
+ if (typeof window !== "undefined") {
28
+ return DOMPurify.sanitize(html);
29
+ }
30
+
31
+ // Server-side: return as-is (user should sanitize at application level)
32
+ // or use isomorphic-dompurify in their setup
33
+ return html;
34
+ },
35
+
36
+ renderChildren,
37
+ };
38
+ }
@@ -0,0 +1,51 @@
1
+ import { ThemeEnum } from "../editor/utils";
2
+ import { GenerateCSSConfig } from "./types";
3
+
4
+ /**
5
+ * Base CSS styles for preview rendering
6
+ */
7
+ const baseStyles = `.draftly-preview {
8
+ padding: 0 0.5rem;
9
+ }`;
10
+
11
+ /**
12
+ * Generate CSS for preview rendering
13
+ *
14
+ * @param config - CSS generation configuration
15
+ * @returns CSS string
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { generateCSS } from 'draftly/preview';
20
+ * import { HeadingPlugin, ListPlugin } from 'draftly/plugins';
21
+ *
22
+ * const css = generateCSS({
23
+ * plugins: [new HeadingPlugin(), new ListPlugin()],
24
+ * theme: ThemeEnum.AUTO,
25
+ * includeBase: true,
26
+ * });
27
+ * ```
28
+ */
29
+ export function generateCSS(config: GenerateCSSConfig = {}): string {
30
+ const { plugins = [], theme = ThemeEnum.AUTO, wrapperClass = "draftly-preview", includeBase = true } = config;
31
+
32
+ const cssChunks: string[] = [];
33
+
34
+ // Include base styles
35
+ if (includeBase) {
36
+ // Replace default wrapper class if custom one is provided
37
+ if (wrapperClass !== "draftly-preview") {
38
+ cssChunks.push(baseStyles.replace(/\.draftly-preview/g, `.${wrapperClass}`));
39
+ } else {
40
+ cssChunks.push(baseStyles);
41
+ }
42
+ }
43
+
44
+ // Collect styles from plugins
45
+ for (const plugin of plugins) {
46
+ const pluginCSS = plugin.getPreviewStyles(theme, wrapperClass);
47
+ if (pluginCSS) cssChunks.push(`/* ${plugin.name} - ${plugin.version} */\n` + pluginCSS);
48
+ }
49
+
50
+ return cssChunks.join("\n\n");
51
+ }
@@ -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,20 @@
1
+ /**
2
+ * draftly/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
+
13
+ // Types
14
+ export type { PreviewConfig, PreviewContext, GenerateCSSConfig, NodeRenderer, NodeRendererMap } from "./types";
15
+
16
+ // Utilities
17
+ export { escapeHtml, defaultRenderers } from "./default-renderers";
18
+
19
+ // Renderer class (for advanced usage)
20
+ export { PreviewRenderer } from "./renderer";
@@ -0,0 +1,40 @@
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 'draftly/preview';
15
+ * import { HeadingPlugin, ListPlugin } from 'draftly/plugins';
16
+ *
17
+ * const html = preview('# Hello World', {
18
+ * plugins: [new HeadingPlugin(), new ListPlugin()],
19
+ * wrapperClass: 'draftly-preview',
20
+ * });
21
+ * ```
22
+ */
23
+ export async function preview(markdown: string, config: PreviewConfig = {}): Promise<string> {
24
+ const {
25
+ plugins = [],
26
+ markdown: markdownConfig = [],
27
+ wrapperClass = "draftly-preview",
28
+ wrapperTag = "article",
29
+ sanitize = true,
30
+ theme = ThemeEnum.AUTO,
31
+ } = config;
32
+
33
+ // Create renderer and generate HTML
34
+ const renderer = new PreviewRenderer(markdown, plugins, markdownConfig, theme, sanitize);
35
+ const content = await renderer.render();
36
+
37
+ // Wrap in container
38
+ const classAttr = wrapperClass ? ` class="${wrapperClass}"` : "";
39
+ return `<${wrapperTag}${classAttr}>\n${content}</${wrapperTag}>`;
40
+ }
@@ -0,0 +1,157 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { Emoji, GFM, MarkdownConfig, parser as markdownParser, Subscript, Superscript } from "@lezer/markdown";
3
+
4
+ import { DraftlyPlugin } from "../editor/plugin";
5
+ import { ThemeEnum } from "../editor/utils";
6
+ import { createPreviewContext } from "./context";
7
+ import { defaultRenderers, escapeHtml } from "./default-renderers";
8
+ import { NodeRendererMap, PreviewContext } from "./types";
9
+ import { foldNodeProp } from "@codemirror/language";
10
+
11
+ /**
12
+ * Renderer class that walks the syntax tree and produces HTML
13
+ */
14
+ export class PreviewRenderer {
15
+ private doc: string;
16
+ private theme: ThemeEnum;
17
+ private plugins: DraftlyPlugin[];
18
+ private markdown: MarkdownConfig[];
19
+ private sanitizeHtml: boolean;
20
+ private renderers: NodeRendererMap;
21
+ private ctx: PreviewContext;
22
+ private nodeToPlugins: Map<string, DraftlyPlugin[]>;
23
+
24
+ constructor(
25
+ doc: string,
26
+ plugins: DraftlyPlugin[] = [],
27
+ markdown: MarkdownConfig[],
28
+ theme: ThemeEnum = ThemeEnum.AUTO,
29
+ sanitize: boolean = true
30
+ ) {
31
+ this.doc = doc;
32
+ this.theme = theme;
33
+ this.plugins = plugins;
34
+ this.markdown = markdown;
35
+ this.sanitizeHtml = sanitize;
36
+ this.renderers = { ...defaultRenderers };
37
+
38
+ // Create context with reference to renderChildren
39
+ this.ctx = createPreviewContext(doc, theme, this.renderChildren.bind(this), sanitize);
40
+
41
+ // Build node-to-plugin map for O(1) lookup
42
+ this.nodeToPlugins = this.buildNodePluginMap();
43
+ }
44
+
45
+ /**
46
+ * Build a map from node names to plugins that handle them
47
+ */
48
+ private buildNodePluginMap(): Map<string, DraftlyPlugin[]> {
49
+ const map = new Map<string, DraftlyPlugin[]>();
50
+ for (const plugin of this.plugins) {
51
+ if (plugin.renderToHTML && plugin.requiredNodes.length > 0) {
52
+ for (const nodeName of plugin.requiredNodes) {
53
+ const list = map.get(nodeName) || [];
54
+ list.push(plugin);
55
+ map.set(nodeName, list);
56
+ }
57
+ }
58
+ }
59
+ return map;
60
+ }
61
+
62
+ /**
63
+ * Render the document to HTML
64
+ */
65
+ async render(): Promise<string> {
66
+ // Collect markdown extensions from plugins
67
+ const extensions = [
68
+ ...this.markdown,
69
+ ...this.plugins.map((p) => p.getMarkdownConfig()).filter((ext): ext is NonNullable<typeof ext> => ext !== null),
70
+ ];
71
+
72
+ // Use GFM extensions to match the editor (markdownLanguage includes GFM by default)
73
+ // GFM includes: Table, TaskList, Strikethrough, Autolink
74
+ const baseParser = markdownParser.configure([
75
+ GFM,
76
+ Subscript,
77
+ Superscript,
78
+ Emoji,
79
+ {
80
+ props: [
81
+ foldNodeProp.add({
82
+ Table: (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to }),
83
+ }),
84
+ ],
85
+ },
86
+ ]);
87
+ const parser = extensions.length > 0 ? baseParser.configure(extensions) : baseParser;
88
+
89
+ // Parse the document
90
+ const tree = parser.parse(this.doc);
91
+
92
+ // Render from root
93
+ return await this.renderNode(tree.topNode);
94
+ }
95
+
96
+ /**
97
+ * Render a single node to HTML
98
+ */
99
+ private async renderNode(node: SyntaxNode): Promise<string> {
100
+ // Get plugins that handle this node type (O(1) lookup)
101
+ const plugins = this.nodeToPlugins.get(node.name);
102
+ if (plugins) {
103
+ for (const plugin of plugins) {
104
+ const children = await this.renderChildren(node);
105
+ const result = await plugin.renderToHTML!(node, children, this.ctx);
106
+ if (result !== null) {
107
+ return result;
108
+ }
109
+ }
110
+ }
111
+
112
+ // Use default renderer
113
+ const renderer = this.renderers[node.name];
114
+ if (renderer) {
115
+ const children = await this.renderChildren(node);
116
+ return renderer(node, children, this.ctx);
117
+ }
118
+
119
+ // Unknown node - render children or text
120
+ if (node.firstChild) {
121
+ return await this.renderChildren(node);
122
+ }
123
+
124
+ // Leaf node - return text content
125
+ return this.ctx.sliceDoc(node.from, node.to);
126
+ }
127
+
128
+ /**
129
+ * Render all children of a node, including text between nodes
130
+ */
131
+ private async renderChildren(node: SyntaxNode): Promise<string> {
132
+ let result = "";
133
+ let pos = node.from; // Track position to find text gaps
134
+ let child = node.firstChild;
135
+
136
+ while (child) {
137
+ // Add any text between the last position and this child
138
+ if (child.from > pos) {
139
+ result += escapeHtml(this.ctx.sliceDoc(pos, child.from));
140
+ }
141
+
142
+ // Render the child node
143
+ result += await this.renderNode(child);
144
+
145
+ // Update position to end of this child
146
+ pos = child.to;
147
+ child = child.nextSibling;
148
+ }
149
+
150
+ // Add any trailing text after the last child
151
+ if (pos < node.to) {
152
+ result += escapeHtml(this.ctx.sliceDoc(pos, node.to));
153
+ }
154
+
155
+ return result;
156
+ }
157
+ }
@@ -0,0 +1,72 @@
1
+ import { SyntaxNode } from "@lezer/common";
2
+ import { ThemeEnum } from "../editor/utils";
3
+
4
+ /**
5
+ * Context passed to plugin preview methods
6
+ */
7
+ export interface PreviewContext {
8
+ /** Full document text */
9
+ readonly doc: string;
10
+
11
+ /** Current theme */
12
+ readonly theme: ThemeEnum;
13
+
14
+ /** Slice document text between positions */
15
+ sliceDoc(from: number, to: number): string;
16
+
17
+ /** Sanitize HTML content (for HTMLBlock/HTMLTag) */
18
+ sanitize(html: string): string;
19
+
20
+ /** Render children of a node to HTML */
21
+ renderChildren(node: SyntaxNode): Promise<string>;
22
+ }
23
+
24
+ /**
25
+ * Configuration for the preview renderer
26
+ */
27
+ export interface PreviewConfig {
28
+ /** Plugins to use for rendering */
29
+ plugins?: import("../editor/plugin").DraftlyPlugin[];
30
+
31
+ /** Markdown extensions to use for rendering */
32
+ markdown?: import("@lezer/markdown").MarkdownConfig[];
33
+
34
+ /** CSS class for the wrapper element */
35
+ wrapperClass?: string;
36
+
37
+ /** HTML tag for the wrapper element */
38
+ wrapperTag?: "article" | "div" | "section";
39
+
40
+ /** Whether to sanitize HTML blocks (default: true) */
41
+ sanitize?: boolean;
42
+
43
+ /** Theme to use */
44
+ theme?: ThemeEnum;
45
+ }
46
+
47
+ /**
48
+ * Result of CSS generation
49
+ */
50
+ export interface GenerateCSSConfig {
51
+ /** Plugins to extract styles from */
52
+ plugins?: import("../editor/plugin").DraftlyPlugin[];
53
+
54
+ /** Theme to use */
55
+ theme?: ThemeEnum;
56
+
57
+ /** Wrapper class for scoping (default: "draftly-preview") */
58
+ wrapperClass?: string;
59
+
60
+ /** Include base styles */
61
+ includeBase?: boolean;
62
+ }
63
+
64
+ /**
65
+ * Node renderer function type
66
+ */
67
+ export type NodeRenderer = (node: SyntaxNode, children: string, ctx: PreviewContext) => string;
68
+
69
+ /**
70
+ * Map of node names to their renderers
71
+ */
72
+ export type NodeRendererMap = Record<string, NodeRenderer>;
@@ -1,9 +0,0 @@
1
- import { DraftlyPlugin } from "../plugin";
2
- import { HeadingPlugin } from "./heading-plugin";
3
- import { InlinePlugin } from "./inline-plugin";
4
- import { ListPlugin } from "./list-plugin";
5
- import { HTMLPlugin } from "./html-plugin";
6
-
7
- const defaultPlugins: DraftlyPlugin[] = [new HeadingPlugin(), new InlinePlugin(), new ListPlugin(), new HTMLPlugin()];
8
-
9
- export { defaultPlugins };
package/src/theme.ts DELETED
@@ -1,86 +0,0 @@
1
- import { EditorView } from "@codemirror/view";
2
- import { HighlightStyle } from "@codemirror/language";
3
-
4
- export const highlightStyle = HighlightStyle.define([]);
5
-
6
- /**
7
- * Base theme for draftly styling
8
- * Note: Layout styles are scoped under .cm-draftly-enabled which is added by the view plugin
9
- */
10
- export const draftlyBaseTheme = EditorView.baseTheme({
11
- // Container styles - only apply when view plugin is enabled
12
- "&.cm-draftly-enabled": {
13
- fontSize: "16px",
14
- lineHeight: "1.6",
15
- },
16
-
17
- "&.cm-draftly-enabled .cm-content": {
18
- maxWidth: "48rem",
19
- margin: "0 auto",
20
- fontFamily: "var(--font-sans, sans-serif)",
21
- fontSize: "16px",
22
- lineHeight: "1.6",
23
- },
24
-
25
- // Inline code
26
- ".cm-draftly-inline-code": {
27
- fontFamily: "var(--font-mono, monospace)",
28
- fontSize: "0.9em",
29
- backgroundColor: "rgba(175, 184, 193, 0.2)",
30
- padding: "0.1em 0.3em",
31
- borderRadius: "4px",
32
- },
33
- ".cm-draftly-code-mark": {
34
- opacity: "0.4",
35
- },
36
-
37
- // Links
38
- ".cm-draftly-link": {
39
- color: "#0969da",
40
- textDecoration: "none",
41
- },
42
- ".cm-draftly-link:hover": {
43
- textDecoration: "underline",
44
- },
45
- ".cm-draftly-url": {
46
- opacity: "0.6",
47
- fontSize: "0.9em",
48
- },
49
-
50
- // Images (placeholder styling)
51
- ".cm-draftly-image": {
52
- color: "#8250df",
53
- },
54
-
55
- // Code blocks
56
- ".cm-draftly-fenced-code": {
57
- fontFamily: "var(--font-mono, monospace)",
58
- fontSize: "0.9em",
59
- },
60
- ".cm-draftly-line-code": {
61
- backgroundColor: "rgba(175, 184, 193, 0.15)",
62
- borderRadius: "0",
63
- },
64
- ".cm-draftly-code-info": {
65
- color: "#6e7781",
66
- fontStyle: "italic",
67
- },
68
-
69
- // Blockquote
70
- ".cm-draftly-line-blockquote": {
71
- borderLeft: "3px solid #d0d7de",
72
- paddingLeft: "1em",
73
- color: "#656d76",
74
- },
75
- ".cm-draftly-quote-mark": {
76
- opacity: "0.4",
77
- },
78
-
79
- // Horizontal rule
80
- ".cm-draftly-line-hr": {
81
- textAlign: "center",
82
- },
83
- ".cm-draftly-hr": {
84
- opacity: "0.4",
85
- },
86
- });
package/src/utils.ts DELETED
@@ -1,21 +0,0 @@
1
- import { EditorView } from "@codemirror/view";
2
-
3
- /**
4
- * Check if cursor is within the given range
5
- */
6
- export function cursorInRange(view: EditorView, from: number, to: number): boolean {
7
- const selection = view.state.selection.main;
8
- return selection.from <= to && selection.to >= from;
9
- }
10
-
11
- /**
12
- * Check if any selection overlaps with the given range
13
- */
14
- export function selectionOverlapsRange(view: EditorView, from: number, to: number): boolean {
15
- for (const range of view.state.selection.ranges) {
16
- if (range.from <= to && range.to >= from) {
17
- return true;
18
- }
19
- }
20
- return false;
21
- }