@powerduck/md-editor 0.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.
- package/README.md +118 -0
- package/dist/Editor.d.ts +19 -0
- package/dist/Renderer.d.ts +22 -0
- package/dist/StatusBar.d.ts +28 -0
- package/dist/Toolbar.d.ts +20 -0
- package/dist/icons.d.ts +15 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.mjs +482 -0
- package/dist/perf/IncrementalRenderer.d.ts +41 -0
- package/dist/perf/blocks.d.ts +23 -0
- package/dist/perf/schedule.d.ts +21 -0
- package/dist/plugins/math.d.ts +7 -0
- package/dist/plugins/mindmap.d.ts +24 -0
- package/dist/react/MarkdownEditor.d.ts +32 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react.cjs +1 -0
- package/dist/react.mjs +80 -0
- package/dist/style.css +1 -0
- package/package.json +94 -0
- package/src/styles/tokens.css +202 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @powerduck/md-editor
|
|
2
|
+
|
|
3
|
+
High-performance embeddable Markdown editor built on the markdown-it ecosystem. Supports KaTeX math formulas, Markmap mindmaps, native React integration, block-level incremental rendering with debounced scheduling. Ships in two modes: **simple** (no toolbar, pure edit + preview) and **complex** (toolbar + status bar). Styled via CSS custom properties with light/dark themes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Markdown editing** powered by CodeMirror 6 with markdown syntax highlighting
|
|
8
|
+
- **Math formulas** via KaTeX (`$E=mc^2$` inline, `$$...$$` block)
|
|
9
|
+
- **Mindmaps** via Markmap (```` ```mindmap ```` code fences)
|
|
10
|
+
- **Block-level incremental rendering** -- only changed sections re-render
|
|
11
|
+
- **Adaptive debounce** -- preview render delay scales with document length
|
|
12
|
+
- **Simple / Complex modes** -- toggle toolbar and status bar at runtime
|
|
13
|
+
- **Light / Dark themes** -- CSS custom properties, inherits host design tokens
|
|
14
|
+
- **React component** -- `forwardRef` with imperative handle
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @powerduck/md-editor
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Vanilla JS
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { MarkdownEditor } from '@powerduck/md-editor';
|
|
28
|
+
import '@powerduck/md-editor/dist/style.css';
|
|
29
|
+
|
|
30
|
+
const editor = new MarkdownEditor('#editor', {
|
|
31
|
+
value: '# Hello\n\nStart typing...',
|
|
32
|
+
mode: 'complex', // 'simple' | 'complex'
|
|
33
|
+
theme: 'light', // 'light' | 'dark'
|
|
34
|
+
math: true,
|
|
35
|
+
mindmap: true,
|
|
36
|
+
onChange: (value) => console.log(value)
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### React
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { MarkdownEditorReact } from '@powerduck/md-editor/react';
|
|
44
|
+
import '@powerduck/md-editor/dist/style.css';
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<MarkdownEditorReact
|
|
49
|
+
defaultValue="# Hello"
|
|
50
|
+
mode="complex"
|
|
51
|
+
onChange={(v) => console.log(v)}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### MarkdownEditorOptions
|
|
60
|
+
|
|
61
|
+
| Option | Type | Default | Description |
|
|
62
|
+
|--------|------|---------|-------------|
|
|
63
|
+
| `value` | `string` | `''` | Initial content |
|
|
64
|
+
| `mode` | `'simple' \| 'complex'` | `'simple'` | Toolbar + status bar in complex mode |
|
|
65
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | Color theme |
|
|
66
|
+
| `math` | `boolean` | `true` | Enable KaTeX math |
|
|
67
|
+
| `mindmap` | `boolean` | `true` | Enable Markmap mindmaps |
|
|
68
|
+
| `preview` | `boolean` | `true` | Show preview pane |
|
|
69
|
+
| `autoPreview` | `boolean` | `true` | Auto-render preview on edit |
|
|
70
|
+
| `renderDebounce` | `number \| false` | adaptive | Preview debounce in ms |
|
|
71
|
+
| `onChange` | `(value: string) => void` | - | Change callback |
|
|
72
|
+
|
|
73
|
+
### Methods
|
|
74
|
+
|
|
75
|
+
- `getValue(): string`
|
|
76
|
+
- `setValue(value: string): void`
|
|
77
|
+
- `getHtml(): string`
|
|
78
|
+
- `renderNow(): void` -- manual preview render (use with `autoPreview: false`)
|
|
79
|
+
- `setMode(mode: EditorMode): void`
|
|
80
|
+
- `setTheme(theme: EditorTheme): void`
|
|
81
|
+
- `setAutoPreview(auto: boolean): void`
|
|
82
|
+
- `focus(): void`
|
|
83
|
+
- `destroy(): void`
|
|
84
|
+
|
|
85
|
+
## Mindmap Syntax
|
|
86
|
+
|
|
87
|
+
````markdown
|
|
88
|
+
```mindmap
|
|
89
|
+
# Root topic
|
|
90
|
+
## Branch one
|
|
91
|
+
- Child A
|
|
92
|
+
- Child B
|
|
93
|
+
## Branch two
|
|
94
|
+
- Child C
|
|
95
|
+
```
|
|
96
|
+
````
|
|
97
|
+
|
|
98
|
+
## Performance
|
|
99
|
+
|
|
100
|
+
- **Block splitting**: documents are split at ATX headings (`#` through `######`). Content inside code fences is never split.
|
|
101
|
+
- **Incremental rendering**: unchanged blocks reuse their DOM nodes directly, including already-hydrated mindmap SVGs.
|
|
102
|
+
- **Content-hash cache**: render results are cached by content hash, so undo/redo and copy-paste hit the cache.
|
|
103
|
+
- **Adaptive debounce**: `adaptiveDebounceMs(docLength)` scales from 120ms to 600ms based on document size.
|
|
104
|
+
- **`content-visibility: auto`**: off-screen blocks skip layout and paint entirely.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install
|
|
110
|
+
npm run typecheck # src only
|
|
111
|
+
npm run typecheck:test # src + tests
|
|
112
|
+
npm test # run all tests (vitest + jsdom)
|
|
113
|
+
npm run build # vite build + d.ts generation
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/dist/Editor.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EditorView } from '@codemirror/view';
|
|
2
|
+
export interface CodeEditorOptions {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange?: (value: string) => void;
|
|
5
|
+
/** Show line numbers. Off in simple mode, on in complex mode by default. */
|
|
6
|
+
lineNumbers?: boolean;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class CodeEditor {
|
|
10
|
+
readonly view: EditorView;
|
|
11
|
+
private lineNumbersCompartment;
|
|
12
|
+
constructor(container: HTMLElement, options: CodeEditorOptions);
|
|
13
|
+
getValue(): string;
|
|
14
|
+
setValue(value: string): void;
|
|
15
|
+
setLineNumbers(enabled: boolean): void;
|
|
16
|
+
insertAtCursor(text: string): void;
|
|
17
|
+
focus(): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
export interface RendererOptions {
|
|
3
|
+
/** Enable KaTeX math rendering. Default: true. */
|
|
4
|
+
math?: boolean;
|
|
5
|
+
/** Enable Markmap mindmap rendering. Default: true. */
|
|
6
|
+
mindmap?: boolean;
|
|
7
|
+
/** Allow raw HTML in markdown source. Default: false (safer). */
|
|
8
|
+
html?: boolean;
|
|
9
|
+
/** Convert soft line breaks to <br>. Default: false. */
|
|
10
|
+
breaks?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class Renderer {
|
|
13
|
+
readonly md: MarkdownIt;
|
|
14
|
+
private mindmapEnabled;
|
|
15
|
+
constructor(options?: RendererOptions);
|
|
16
|
+
render(source: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Call after the rendered HTML is inserted into the DOM to hydrate
|
|
19
|
+
* mindmap placeholders into real SVG diagrams.
|
|
20
|
+
*/
|
|
21
|
+
hydrate(container: HTMLElement): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface StatusBarState {
|
|
2
|
+
charCount: number;
|
|
3
|
+
wordCount: number;
|
|
4
|
+
blockCount: number;
|
|
5
|
+
mode: 'simple' | 'complex';
|
|
6
|
+
autoPreview: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface StatusBarCallbacks {
|
|
9
|
+
onRefreshPreview: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Character count is more intuitive for CJK text; word count (split on
|
|
13
|
+
* whitespace) is provided for Latin-script content. Both are computed
|
|
14
|
+
* directly from the current value and are cheap operations.
|
|
15
|
+
*/
|
|
16
|
+
export declare function countText(value: string): {
|
|
17
|
+
charCount: number;
|
|
18
|
+
wordCount: number;
|
|
19
|
+
};
|
|
20
|
+
export declare class StatusBar {
|
|
21
|
+
readonly el: HTMLDivElement;
|
|
22
|
+
private countEl;
|
|
23
|
+
private blockEl;
|
|
24
|
+
private refreshBtn;
|
|
25
|
+
constructor(callbacks: StatusBarCallbacks);
|
|
26
|
+
update(state: StatusBarState): void;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ToolbarAction {
|
|
2
|
+
name: string;
|
|
3
|
+
title: string;
|
|
4
|
+
/** SVG/HTML icon markup */
|
|
5
|
+
icon: string;
|
|
6
|
+
handler: () => void;
|
|
7
|
+
/** Append a group separator after this button */
|
|
8
|
+
groupEnd?: boolean;
|
|
9
|
+
/** Whether this is a toggle button (e.g. preview visibility, theme) */
|
|
10
|
+
toggle?: boolean;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class Toolbar {
|
|
14
|
+
readonly el: HTMLDivElement;
|
|
15
|
+
private buttons;
|
|
16
|
+
constructor(actions: readonly ToolbarAction[]);
|
|
17
|
+
setActive(name: string, active: boolean): void;
|
|
18
|
+
setIcon(name: string, icon: string): void;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const icons: {
|
|
2
|
+
readonly bold: string;
|
|
3
|
+
readonly italic: string;
|
|
4
|
+
readonly heading: string;
|
|
5
|
+
readonly link: string;
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly table: string;
|
|
8
|
+
readonly sigma: string;
|
|
9
|
+
readonly mindmap: string;
|
|
10
|
+
readonly eye: string;
|
|
11
|
+
readonly refresh: string;
|
|
12
|
+
readonly moon: string;
|
|
13
|
+
readonly sun: string;
|
|
14
|
+
};
|
|
15
|
+
export type IconName = keyof typeof icons;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";var S=Object.create;var f=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var N=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var R=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of T(e))!L.call(n,i)&&i!==t&&f(n,i,{get:()=>e[i],enumerable:!(r=x(e,i))||r.enumerable});return n};var k=(n,e,t)=>(t=n!=null?S(N(n)):{},R(e||!n||!n.__esModule?f(t,"default",{value:n,enumerable:!0}):t,n));Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("@codemirror/state"),m=require("@codemirror/view"),p=require("@codemirror/commands"),H=require("@codemirror/lang-markdown"),I=require("@codemirror/language-data"),A=require("markdown-it"),V=require("markdown-it-texmath"),$=require("katex");class v{constructor(e,t){this.lineNumbersCompartment=new b.Compartment;const r=[p.history(),m.keymap.of([...p.defaultKeymap,...p.historyKeymap]),H.markdown({codeLanguages:I.languages}),m.EditorView.lineWrapping,this.lineNumbersCompartment.of(t.lineNumbers?m.lineNumbers():[]),m.EditorView.updateListener.of(i=>{var s;i.docChanged&&((s=t.onChange)==null||s.call(t,i.state.doc.toString()))})];this.view=new m.EditorView({state:b.EditorState.create({doc:t.value??"",extensions:r}),parent:e})}getValue(){return this.view.state.doc.toString()}setValue(e){this.view.dispatch({changes:{from:0,to:this.view.state.doc.length,insert:e}})}setLineNumbers(e){this.view.dispatch({effects:this.lineNumbersCompartment.reconfigure(e?m.lineNumbers():[])})}insertAtCursor(e){const{from:t,to:r}=this.view.state.selection.main;this.view.dispatch({changes:{from:t,to:r,insert:e},selection:{anchor:t+e.length}}),this.view.focus()}focus(){this.view.focus()}destroy(){this.view.destroy()}}function q(n){n.use(V,{engine:$,delimiters:"dollars",katexOptions:{throwOnError:!1,strict:!1}})}let j=0;function D(n){const e=n.renderer.rules.fence?n.renderer.rules.fence.bind(n.renderer.rules):(t,r,i,s,o)=>o.renderToken(t,r,i);n.renderer.rules.fence=(t,r,i,s,o)=>{const a=t[r];if(!a)return e(t,r,i,s,o);if(a.info.trim().toLowerCase()==="mindmap"){const l=`md-editor-mindmap-${++j}`,u=encodeURIComponent(a.content);return`<div class="md-editor-mindmap" id="${l}" data-source="${u}"></div>`}return e(t,r,i,s,o)}}async function K(n){const e=n.querySelectorAll(".md-editor-mindmap:not([data-hydrated])");if(e.length===0)return;const{Transformer:t}=await import("markmap-lib"),r=await import("markmap-view"),i=r.Markmap??r.default;if(!i)return;const s=new t;for(const o of Array.from(e)){o.setAttribute("data-hydrated","1");const a=decodeURIComponent(o.dataset.source??"")||"- Empty mindmap",{root:d}=s.transform(a);o.innerHTML="";const l=document.createElementNS("http://www.w3.org/2000/svg","svg");l.setAttribute("style","width:100%;height:100%;min-height:280px;"),o.appendChild(l),i.create(l,{autoFit:!0},d)}}class g{constructor(e={}){this.md=new A({html:e.html??!1,linkify:!0,breaks:e.breaks??!1,typographer:!0}),e.math!==!1&&q(this.md),this.mindmapEnabled=e.mindmap!==!1,this.mindmapEnabled&&D(this.md)}render(e){return this.md.render(e||"")}async hydrate(e){this.mindmapEnabled&&await K(e)}}class y{constructor(e){this.buttons=new Map,this.el=document.createElement("div"),this.el.className="md-editor-toolbar",this.el.setAttribute("role","toolbar");for(const t of e){const r=document.createElement("button");if(r.type="button",r.className="md-editor-toolbar-btn",t.toggle&&r.classList.add("is-toggle"),t.active&&r.classList.add("is-active"),r.title=t.title,r.setAttribute("aria-label",t.title),r.setAttribute("data-action",t.name),r.innerHTML=t.icon,r.addEventListener("click",t.handler),this.buttons.set(t.name,r),this.el.appendChild(r),t.groupEnd){const i=document.createElement("span");i.className="md-editor-toolbar-sep",this.el.appendChild(i)}}}setActive(e,t){var r;(r=this.buttons.get(e))==null||r.classList.toggle("is-active",t)}setIcon(e,t){const r=this.buttons.get(e);r&&(r.innerHTML=t)}destroy(){for(const e of this.buttons.values())e.onclick=null;this.buttons.clear(),this.el.remove()}}function w(n){const e=n.trim();return{charCount:n.length,wordCount:e?e.split(/\s+/).length:0}}class C{constructor(e){this.el=document.createElement("div"),this.el.className="md-editor-statusbar",this.countEl=document.createElement("span"),this.blockEl=document.createElement("span"),this.blockEl.className="md-editor-statusbar-muted",this.refreshBtn=document.createElement("button"),this.refreshBtn.type="button",this.refreshBtn.className="md-editor-statusbar-refresh",this.refreshBtn.textContent="Refresh preview",this.refreshBtn.style.display="none",this.refreshBtn.addEventListener("click",e.onRefreshPreview),this.el.appendChild(this.countEl),this.el.appendChild(this.blockEl),this.el.appendChild(this.refreshBtn)}update(e){this.countEl.textContent=`${e.charCount} chars · ${e.wordCount} words`,this.blockEl.textContent=`${e.blockCount} render blocks`,this.refreshBtn.style.display=e.autoPreview?"none":""}destroy(){this.refreshBtn.onclick=null,this.el.remove()}}const O=/^ {0,3}#{1,6}\s/,F=/^ {0,3}(`{3,}|~{3,})/;function _(n){let e=5381;for(let t=0;t<n.length;t++)e=e*33^n.charCodeAt(t);return(e>>>0).toString(36)+":"+n.length}function M(n){var a;const e=n.split(`
|
|
2
|
+
`),t=[];let r=[],i=!1,s="";for(const d of e){const l=d.match(F);if(l){const u=((a=l[1])==null?void 0:a[0])??"";i?u===s&&(i=!1):(i=!0,s=u),r.push(d);continue}if(!i&&O.test(d)&&r.length>0){t.push(r.join(`
|
|
3
|
+
`)),r=[d];continue}r.push(d)}r.length>0&&t.push(r.join(`
|
|
4
|
+
`)),t.length===0&&t.push("");const o=new Map;return t.map(d=>{const l=_(d),u=(o.get(l)??0)+1;return o.set(l,u),{key:`${l}#${u}`,hash:l,source:d}})}class B{constructor(e,t,r=800){this.container=e,this.renderBlock=t,this.maxCacheEntries=r,this.nodeByKey=new Map,this.htmlByHash=new Map,this.prevBlocks=[]}update(e){const t=M(e),r=document.createDocumentFragment(),i=new Map;let s=0;for(const o of t){let a=this.nodeByKey.get(o.key);if(!a){let d=this.htmlByHash.get(o.hash);d===void 0&&(d=this.renderBlock(o.source),this.htmlByHash.set(o.hash,d),s++),a=document.createElement("div"),a.className="md-editor-block",a.dataset.blockKey=o.key,a.innerHTML=d}i.set(o.key,a),r.appendChild(a)}return this.container.innerHTML="",this.container.appendChild(r),this.nodeByKey=i,this.prevBlocks=t,this.evictCacheIfNeeded(t),{rerenderedCount:s,totalCount:t.length}}evictCacheIfNeeded(e){if(this.htmlByHash.size<=this.maxCacheEntries)return;const t=new Set(e.map(r=>r.hash));for(const r of Array.from(this.htmlByHash.keys()))t.has(r)||this.htmlByHash.delete(r)}getBlockCount(){return this.prevBlocks.length}destroy(){this.container.innerHTML="",this.nodeByKey.clear(),this.htmlByHash.clear(),this.prevBlocks=[]}}function E(n,e){let t,r;const i=(...s)=>{r=s,t!==void 0&&clearTimeout(t),t=setTimeout(()=>{t=void 0,r&&n(...r)},e)};return i.cancel=()=>{t!==void 0&&clearTimeout(t),t=void 0},i.flush=()=>{t!==void 0&&(clearTimeout(t),t=void 0,r&&n(...r))},i}function P(n,e=120,t=600){const r=Math.max(0,n),i=Math.floor(r/2e4)*60;return Math.min(t,e+i)}function W(n,e=300){const t=window;return typeof t.requestIdleCallback=="function"?t.requestIdleCallback(n,{timeout:e}):window.setTimeout(n,0)}function Z(n){const e=window;typeof e.cancelIdleCallback=="function"?e.cancelIdleCallback(n):clearTimeout(n)}const c=n=>`<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">${n}</svg>`,h={bold:c('<path d="M4 3h4.2a2.4 2.4 0 0 1 0 4.8H4V3Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M4 7.8h4.8a2.4 2.4 0 1 1 0 4.8H4V7.8Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>'),italic:c('<path d="M9.5 3h-3M6.5 13h-3M8.2 3 5.8 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'),heading:c('<path d="M3.5 3v10M11.5 3v10M3.5 8h8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'),link:c('<path d="M6.8 9.2 9.2 6.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M7.6 4.4 8.4 3.6a2.6 2.6 0 1 1 3.7 3.7l-.9.9M8.4 11.6l-.8.8a2.6 2.6 0 1 1-3.7-3.7l.9-.9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>'),code:c('<path d="M5.5 4.5 2 8l3.5 3.5M10.5 4.5 14 8l-3.5 3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>'),table:c('<rect x="2.5" y="3" width="11" height="10" rx="1.2" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6.6h11M6.5 3v10" stroke="currentColor" stroke-width="1.3"/>'),sigma:c('<path d="M11.5 3.5h-7l3.5 4.5-3.5 4.5h7" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round"/>'),mindmap:c('<circle cx="3.2" cy="8" r="1.6" stroke="currentColor" stroke-width="1.3"/><circle cx="12.2" cy="3.6" r="1.6" stroke="currentColor" stroke-width="1.3"/><circle cx="12.2" cy="12.4" r="1.6" stroke="currentColor" stroke-width="1.3"/><path d="M4.6 7.3 10.7 4.2M4.6 8.7l6.1 3.1" stroke="currentColor" stroke-width="1.3"/>'),eye:c('<path d="M1.5 8S4 3.5 8 3.5 14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="8" cy="8" r="1.8" stroke="currentColor" stroke-width="1.3"/>'),refresh:c('<path d="M13 8a5 5 0 1 1-1.6-3.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M13 3v3.2H9.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>'),moon:c('<path d="M13 9.6A5.4 5.4 0 1 1 6.4 3a4.3 4.3 0 0 0 6.6 6.6Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>'),sun:c('<circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.3"/><path d="M8 1.8v1.4M8 12.8v1.4M14.2 8h-1.4M3.2 8H1.8M12.4 3.6l-1 1M4.6 11.4l-1 1M12.4 12.4l-1-1M4.6 4.6l-1-1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>')},U={bold:["**","**","bold text"],italic:["_","_","italic text"],heading:["## ","","Heading"],link:["[","](https://)","link text"],code:["```\n","\n```","code"],math:["$","$","E=mc^2"],mindmapBlock:[`\`\`\`mindmap
|
|
5
|
+
# Root topic
|
|
6
|
+
## Branch one
|
|
7
|
+
- Child A
|
|
8
|
+
- Child B
|
|
9
|
+
## Branch two
|
|
10
|
+
\`\`\`
|
|
11
|
+
`,"",""],table:[`| Col 1 | Col 2 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `,` | content |
|
|
14
|
+
`,"content"]};class z{constructor(e,t={}){this.previewPane=null,this.incremental=null,this.toolbar=null,this.statusBar=null,this.scheduledRender=null,this.scheduledWait=0;const r=typeof e=="string"?document.querySelector(e):e;if(!r)throw new Error("MarkdownEditor: mount container not found");this.container=r,this.mode=t.mode??"simple",this.theme=t.theme??"light",this.showPreview=t.preview??!0,this.autoPreview=t.autoPreview??!0,this.renderDebounceOpt=t.renderDebounce,this.onChange=t.onChange,this.container.classList.add("md-editor-root",`md-editor-mode-${this.mode}`),this.container.setAttribute("data-theme",this.theme),this.renderer=new g({math:t.math??!0,mindmap:t.mindmap??!0}),this.buildLayout(),this.codeEditor=new v(this.editorPane,{value:t.value??"",lineNumbers:this.mode==="complex",onChange:i=>{var s;(s=this.onChange)==null||s.call(this,i),this.autoPreview&&this.requestPreviewRender(i),this.updateStatusBar(i)}}),this.mode==="complex"&&(this.buildToolbar(),this.buildStatusBar()),this.previewPane&&(this.incremental=new B(this.previewPane,i=>this.renderer.render(i))),this.renderPreview(t.value??""),this.updateStatusBar(t.value??"")}buildLayout(){this.container.innerHTML="",this.body=document.createElement("div"),this.body.className="md-editor-body",this.editorPane=document.createElement("div"),this.editorPane.className="md-editor-pane md-editor-source",this.body.appendChild(this.editorPane),this.showPreview&&(this.previewPane=document.createElement("div"),this.previewPane.className="md-editor-pane md-editor-preview",this.body.appendChild(this.previewPane)),this.container.appendChild(this.body)}buildToolbar(){const e=[{name:"heading",title:"Heading",icon:h.heading,handler:()=>this.insertSnippet("heading")},{name:"bold",title:"Bold",icon:h.bold,handler:()=>this.insertSnippet("bold")},{name:"italic",title:"Italic",icon:h.italic,handler:()=>this.insertSnippet("italic"),groupEnd:!0},{name:"link",title:"Link",icon:h.link,handler:()=>this.insertSnippet("link")},{name:"code",title:"Code block",icon:h.code,handler:()=>this.insertSnippet("code")},{name:"table",title:"Table",icon:h.table,handler:()=>this.insertSnippet("table"),groupEnd:!0},{name:"math",title:"Math formula",icon:h.sigma,handler:()=>this.insertSnippet("math")},{name:"mindmap",title:"Insert mindmap",icon:h.mindmap,handler:()=>this.insertSnippet("mindmapBlock"),groupEnd:!0},{name:"preview",title:"Toggle preview",icon:h.eye,toggle:!0,active:this.showPreview,handler:()=>this.togglePreview()},{name:"theme",title:"Toggle theme",icon:this.theme==="dark"?h.sun:h.moon,handler:()=>this.setTheme(this.theme==="dark"?"light":"dark")}];this.toolbar=new y(e),this.container.insertBefore(this.toolbar.el,this.body)}buildStatusBar(){this.statusBar=new C({onRefreshPreview:()=>this.renderPreview(this.codeEditor.getValue())}),this.container.appendChild(this.statusBar.el)}insertSnippet(e){const[t,r,i]=U[e];this.codeEditor.insertAtCursor(`${t}${i}${r}`)}togglePreview(){var t;if(!this.previewPane)return;const e=this.previewPane.style.display!=="none";this.previewPane.style.display=e?"none":"",(t=this.toolbar)==null||t.setActive("preview",!e)}requestPreviewRender(e){var r,i;const t=this.renderDebounceOpt===!1?0:this.renderDebounceOpt??P(e.length);if(t===0){(r=this.scheduledRender)==null||r.cancel(),this.scheduledRender=null,this.renderPreview(e);return}(!this.scheduledRender||this.scheduledWait!==t)&&((i=this.scheduledRender)==null||i.cancel(),this.scheduledRender=E(s=>this.renderPreview(s),t),this.scheduledWait=t),this.scheduledRender(e)}renderPreview(e){if(!this.previewPane||!this.incremental)return;const{totalCount:t}=this.incremental.update(e);if(this.renderer.hydrate(this.previewPane),this.statusBar){const{charCount:r,wordCount:i}=w(e);this.statusBar.update({charCount:r,wordCount:i,blockCount:t,mode:this.mode,autoPreview:this.autoPreview})}}updateStatusBar(e){var i;if(!this.statusBar)return;const{charCount:t,wordCount:r}=w(e);this.statusBar.update({charCount:t,wordCount:r,blockCount:((i=this.incremental)==null?void 0:i.getBlockCount())??0,mode:this.mode,autoPreview:this.autoPreview})}getValue(){return this.codeEditor.getValue()}setValue(e){var t;this.codeEditor.setValue(e),(t=this.scheduledRender)==null||t.cancel(),this.renderPreview(e),this.updateStatusBar(e)}getHtml(){return this.renderer.render(this.getValue())}renderNow(){var e;(e=this.scheduledRender)==null||e.cancel(),this.renderPreview(this.codeEditor.getValue())}setAutoPreview(e){this.autoPreview=e,e&&this.renderPreview(this.codeEditor.getValue()),this.updateStatusBar(this.codeEditor.getValue())}setMode(e){var t,r;e!==this.mode&&(this.mode=e,this.container.classList.remove("md-editor-mode-simple","md-editor-mode-complex"),this.container.classList.add(`md-editor-mode-${e}`),this.codeEditor.setLineNumbers(e==="complex"),(t=this.toolbar)==null||t.destroy(),this.toolbar=null,(r=this.statusBar)==null||r.destroy(),this.statusBar=null,e==="complex"&&(this.buildToolbar(),this.buildStatusBar(),this.updateStatusBar(this.codeEditor.getValue())))}setTheme(e){var t;this.theme=e,this.container.setAttribute("data-theme",e),(t=this.toolbar)==null||t.setIcon("theme",e==="dark"?h.sun:h.moon)}focus(){this.codeEditor.focus()}destroy(){var e,t,r,i;(e=this.scheduledRender)==null||e.cancel(),this.scheduledRender=null,this.codeEditor.destroy(),(t=this.toolbar)==null||t.destroy(),this.toolbar=null,(r=this.statusBar)==null||r.destroy(),this.statusBar=null,(i=this.incremental)==null||i.destroy(),this.incremental=null,this.container.innerHTML="",this.container.classList.remove("md-editor-root",`md-editor-mode-${this.mode}`),this.container.removeAttribute("data-theme")}}exports.CodeEditor=v;exports.IncrementalRenderer=B;exports.MarkdownEditor=z;exports.Renderer=g;exports.StatusBar=C;exports.Toolbar=y;exports.adaptiveDebounceMs=P;exports.cancelIdle=Z;exports.countText=w;exports.debounce=E;exports.icons=h;exports.scheduleIdle=W;exports.splitIntoBlocks=M;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import './styles/editor.css';
|
|
2
|
+
export type EditorMode = 'simple' | 'complex';
|
|
3
|
+
export type EditorTheme = 'light' | 'dark';
|
|
4
|
+
export interface MarkdownEditorOptions {
|
|
5
|
+
/** Initial content */
|
|
6
|
+
value?: string;
|
|
7
|
+
/** 'simple': no toolbar/nav, pure edit + preview; 'complex': toolbar + status bar */
|
|
8
|
+
mode?: EditorMode;
|
|
9
|
+
theme?: EditorTheme;
|
|
10
|
+
/** Enable math formulas. Default: true. */
|
|
11
|
+
math?: boolean;
|
|
12
|
+
/** Enable mindmaps. Default: true. */
|
|
13
|
+
mindmap?: boolean;
|
|
14
|
+
/** Show the preview pane. Default: true. */
|
|
15
|
+
preview?: boolean;
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Preview debounce in milliseconds. Defaults to adaptive based on document
|
|
19
|
+
* length (see adaptiveDebounceMs). Pass false to disable debounce (not
|
|
20
|
+
* recommended for large documents).
|
|
21
|
+
*/
|
|
22
|
+
renderDebounce?: number | false;
|
|
23
|
+
/**
|
|
24
|
+
* Auto-preview on every edit. Default: true.
|
|
25
|
+
* For very large documents (hundreds of thousands of characters), set to
|
|
26
|
+
* false and trigger preview manually via the "Refresh preview" button or
|
|
27
|
+
* renderNow() to eliminate all rendering overhead during typing.
|
|
28
|
+
*/
|
|
29
|
+
autoPreview?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare class MarkdownEditor {
|
|
32
|
+
private container;
|
|
33
|
+
private body;
|
|
34
|
+
private editorPane;
|
|
35
|
+
private previewPane;
|
|
36
|
+
private codeEditor;
|
|
37
|
+
private renderer;
|
|
38
|
+
private incremental;
|
|
39
|
+
private toolbar;
|
|
40
|
+
private statusBar;
|
|
41
|
+
private mode;
|
|
42
|
+
private theme;
|
|
43
|
+
private showPreview;
|
|
44
|
+
private autoPreview;
|
|
45
|
+
private renderDebounceOpt;
|
|
46
|
+
private onChange?;
|
|
47
|
+
private scheduledRender;
|
|
48
|
+
private scheduledWait;
|
|
49
|
+
constructor(container: HTMLElement | string, options?: MarkdownEditorOptions);
|
|
50
|
+
private buildLayout;
|
|
51
|
+
private buildToolbar;
|
|
52
|
+
private buildStatusBar;
|
|
53
|
+
private insertSnippet;
|
|
54
|
+
private togglePreview;
|
|
55
|
+
private requestPreviewRender;
|
|
56
|
+
private renderPreview;
|
|
57
|
+
private updateStatusBar;
|
|
58
|
+
getValue(): string;
|
|
59
|
+
setValue(value: string): void;
|
|
60
|
+
getHtml(): string;
|
|
61
|
+
/** Manually trigger a preview render (use when autoPreview is false). */
|
|
62
|
+
renderNow(): void;
|
|
63
|
+
setAutoPreview(auto: boolean): void;
|
|
64
|
+
/** Switch between simple and complex modes at runtime. */
|
|
65
|
+
setMode(mode: EditorMode): void;
|
|
66
|
+
setTheme(theme: EditorTheme): void;
|
|
67
|
+
focus(): void;
|
|
68
|
+
destroy(): void;
|
|
69
|
+
}
|
|
70
|
+
export { CodeEditor } from './Editor.js';
|
|
71
|
+
export { Renderer, type RendererOptions } from './Renderer.js';
|
|
72
|
+
export { Toolbar, type ToolbarAction } from './Toolbar.js';
|
|
73
|
+
export { StatusBar, countText } from './StatusBar.js';
|
|
74
|
+
export { IncrementalRenderer } from './perf/IncrementalRenderer.js';
|
|
75
|
+
export { splitIntoBlocks, type SourceBlock } from './perf/blocks.js';
|
|
76
|
+
export { debounce, adaptiveDebounceMs, scheduleIdle, cancelIdle } from './perf/schedule.js';
|
|
77
|
+
export { icons, type IconName } from './icons.js';
|