@simple-mark/codemirror 0.0.0-stage → 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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  3. package/index.d.ts +75 -0
  4. package/index.js +1001 -0
  5. package/package.json +32 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 one-prime
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @simple-mark/codemirror
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A CodeMirror 6 Markdown editor that never hides syntax — part of
4
+ [SimpleMark](https://github.com/one-prime/simple-mark). `# Heading` stays `# Heading`: the
5
+ `#` is dimmed and the heading is large. The text is exactly what the user typed.
6
+
7
+ ```ts
8
+ import { createEditor } from '@simple-mark/codemirror';
9
+
10
+ const view = createEditor(document.getElementById('editor')!, {
11
+ doc: '# Hello **world**\n\n| a | b |\n|---|---|\n| 1 | 2 |\n',
12
+ onChange: (text) => save(text), // in the document's own line endings
13
+ resolveImage: (src) => new URL(src, base).href,
14
+ });
15
+ ```
16
+
17
+ - **Tables**: Tab / Shift-Tab move between cells, Enter adds a row; each re-aligns the
18
+ table. Korean, Chinese and Japanese line up (drawn exactly two columns wide), and typing
19
+ them with an IME is safe.
20
+ - **Images** and `$$` math preview below their source; `$inline$` math in a tooltip. Math
21
+ (`@simple-mark/math`) loads only when a document has math.
22
+ - **Line endings** are kept (`\r\n`, `\r`), so an unedited file saves back byte for byte.
23
+ - Accurate scroll heights for documents with many images and formulas.
24
+ - `simpleMark(theme)` gives the extensions for your own `EditorState`.
25
+
26
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { EditorState, Extension, Facet, StateCommand } from "@codemirror/state";
2
+ import { DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
3
+ import { LineLook, Theme } from "@simple-mark/model";
4
+ //#region packages/codemirror/src/previews.d.ts
5
+ /** Turns an image path as written in the document into a URL to load, or null if it can't. */
6
+ type ImageResolver = (src: string) => string | null;
7
+ /** How image paths are resolved; by default they are used as written. */
8
+ export declare const imageResolver: Facet<ImageResolver, ImageResolver>;
9
+ /** Image and math previews; the source text always stays visible. */
10
+ export declare function previews(): Extension;
11
+ //#endregion
12
+ //#region packages/codemirror/src/decorations.d.ts
13
+ /** The theme the editor draws with. */
14
+ export declare const markdownTheme: Facet<Theme, Theme>;
15
+ /** A look as an inline CSS declaration list. */
16
+ export declare function lookCss(look: LineLook, theme: Theme): string;
17
+ declare class MarkdownDecorations {
18
+ decorations: DecorationSet;
19
+ private theme;
20
+ private fixWide;
21
+ private cache;
22
+ constructor(view: EditorView);
23
+ update(update: ViewUpdate): void;
24
+ private build;
25
+ /** Draws runs of wide characters exactly two columns each, so table pipes line up. */
26
+ private wideRuns;
27
+ private lineDecoration;
28
+ private markDecoration;
29
+ }
30
+ /** Styles the visible Markdown from the syntax tree; never hides or replaces text. */
31
+ export declare const markdownDecorations: ViewPlugin<MarkdownDecorations, undefined>;
32
+ //#endregion
33
+ //#region packages/codemirror/src/line-separator.d.ts
34
+ /**
35
+ * Keeps a file's line endings. CodeMirror otherwise reads \r\n and \r as \n and writes \n,
36
+ * so a Windows file would not save back byte for byte. Read the text back with
37
+ * `state.sliceDoc()`, not `doc.toString()` (which always joins lines with \n).
38
+ */
39
+ export declare function keepLineSeparator(text: string): Extension;
40
+ //#endregion
41
+ //#region packages/codemirror/src/table-commands.d.ts
42
+ /** Tab: re-align and go to the next cell, adding a row after the last one. */
43
+ export declare const nextCell: StateCommand;
44
+ /** Shift-Tab: re-align and go to the previous cell. */
45
+ export declare const previousCell: StateCommand;
46
+ /**
47
+ * Enter: re-align and add an empty row below, cursor in its first cell. On an empty last row,
48
+ * remove it and leave the table: the cursor goes below a blank line, because GFM would read
49
+ * a line right after a table as another row.
50
+ */
51
+ export declare const insertRowBelow: StateCommand;
52
+ /** Table editing keys, ahead of the default and Markdown keymaps, and their IME counterparts. */
53
+ export declare function tableEditing(): Extension;
54
+ //#endregion
55
+ //#region packages/codemirror/src/index.d.ts
56
+ /** The SimpleMark language and styling, for hosts that build their own EditorState. */
57
+ export declare function simpleMark(theme?: Theme): Extension;
58
+ export interface EditorOptions {
59
+ doc?: string;
60
+ readOnly?: boolean;
61
+ theme?: Theme;
62
+ /** Line ending to read and write ("\n", "\r\n"); by default the one `doc` uses. */
63
+ lineSeparator?: string;
64
+ /** Turns an image path from the document into a URL to load (default: as written). */
65
+ resolveImage?: ImageResolver;
66
+ /** Off when the host owns undo (VS Code). */
67
+ history?: boolean;
68
+ /** Called after every change with the full text, in the original line endings. */
69
+ onChange?: (text: string, update: ViewUpdate) => void;
70
+ extensions?: Extension[];
71
+ }
72
+ /** A Markdown editor in `parent`. The text is kept exactly as given, line endings included. */
73
+ export declare function createEditor(parent: HTMLElement, options?: EditorOptions): EditorView;
74
+ //#endregion
75
+ export type { ImageResolver };
package/index.js ADDED
@@ -0,0 +1,1001 @@
1
+ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
2
+ import { commonmarkLanguage, markdown } from "@codemirror/lang-markdown";
3
+ import { EditorState, Facet, Prec, StateEffect, StateField } from "@codemirror/state";
4
+ import { Decoration, EditorView, ViewPlugin, WidgetType, hoverTooltip, keymap, showTooltip } from "@codemirror/view";
5
+ import { cellAt, charWidth, defaultTheme, markdownExtensions, markdownStyles, mathSource, parseTable, previewBlocks, renderTable, resolveLineLook, resolveLook, rowLine } from "@simple-mark/model";
6
+ import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
7
+ //#region packages/codemirror/src/decorations.ts
8
+ /** The theme the editor draws with. */
9
+ const markdownTheme = Facet.define({ combine: (values) => values[0] ?? defaultTheme });
10
+ /** A look as an inline CSS declaration list. */
11
+ function lookCss(look, theme) {
12
+ const css = [];
13
+ if (look.fontScale !== void 0) css.push(`font-size: ${look.fontScale}em`);
14
+ if (look.fontWeight !== void 0) css.push(`font-weight: ${look.fontWeight}`);
15
+ if (look.italic !== void 0) css.push(`font-style: ${look.italic ? "italic" : "normal"}`);
16
+ if (look.monospace !== void 0) css.push(`font-family: ${look.monospace ? theme.monospaceFamily : theme.fontFamily}`);
17
+ if (look.color !== void 0) css.push(`color: ${look.color}`);
18
+ if (look.background !== void 0) css.push(`background-color: ${look.background}`);
19
+ if (look.strike !== void 0 || look.underline !== void 0) {
20
+ const lines = [look.strike && "line-through", look.underline && "underline"].filter(Boolean);
21
+ css.push(`text-decoration: ${lines.join(" ") || "none"}`);
22
+ }
23
+ if (look.lineBackground !== void 0) css.push(`background-color: ${look.lineBackground}`);
24
+ if (look.borderLeft !== void 0) css.push(`box-shadow: inset 3px 0 0 ${look.borderLeft}`, "padding-left: 12px");
25
+ return css.join("; ");
26
+ }
27
+ const fontChecks = /* @__PURE__ */ new Map();
28
+ /**
29
+ * Whether `family` draws CJK characters exactly two columns wide. When a Latin-only monospace
30
+ * font falls back to a CJK font of another width, padded tables would not line up.
31
+ */
32
+ function cjkFitsColumns(family) {
33
+ let fits = fontChecks.get(family);
34
+ if (fits === void 0) {
35
+ const ctx = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
36
+ if (!ctx) return true;
37
+ ctx.font = `20px ${family}`;
38
+ const column = ctx.measureText("0").width;
39
+ fits = [
40
+ "가",
41
+ "中",
42
+ "あ"
43
+ ].every((ch) => Math.abs(ctx.measureText(ch).width - 2 * column) < .5);
44
+ fontChecks.set(family, fits);
45
+ }
46
+ return fits;
47
+ }
48
+ const headingText = Decoration.mark({ class: "sm-heading-text" });
49
+ /** The lines holding a cursor, as a comparable string. */
50
+ function cursorLinesKey(state) {
51
+ return state.selection.ranges.map((r) => state.doc.lineAt(r.head).from).join(",");
52
+ }
53
+ var MarkdownDecorations = class {
54
+ decorations;
55
+ theme;
56
+ fixWide;
57
+ cache = /* @__PURE__ */ new Map();
58
+ constructor(view) {
59
+ this.theme = view.state.facet(markdownTheme);
60
+ this.fixWide = !cjkFitsColumns(this.theme.monospaceFamily);
61
+ this.decorations = this.build(view);
62
+ }
63
+ update(update) {
64
+ const theme = update.state.facet(markdownTheme);
65
+ if (theme !== this.theme) {
66
+ this.theme = theme;
67
+ this.fixWide = !cjkFitsColumns(theme.monospaceFamily);
68
+ this.cache.clear();
69
+ } else if (!update.docChanged && !update.viewportChanged && syntaxTree(update.state) === syntaxTree(update.startState) && !(this.fixWide && update.selectionSet && cursorLinesKey(update.state) !== cursorLinesKey(update.startState))) return;
70
+ this.decorations = this.build(update.view);
71
+ }
72
+ build(view) {
73
+ const { doc } = view.state;
74
+ const { from, to } = view.viewport;
75
+ const { blocks, spans } = markdownStyles(syntaxTree(view.state), from, to);
76
+ const lines = /* @__PURE__ */ new Map();
77
+ for (const block of blocks) {
78
+ const start = Math.max(block.from, from);
79
+ let end = Math.min(block.to, to);
80
+ if (end > start && doc.lineAt(end).from === end) end--;
81
+ for (let pos = start; pos <= end;) {
82
+ const line = doc.lineAt(pos);
83
+ let styles = lines.get(line.from);
84
+ if (!styles) lines.set(line.from, styles = /* @__PURE__ */ new Set());
85
+ styles.add(block.style);
86
+ pos = line.to + 1;
87
+ }
88
+ }
89
+ const ranges = [];
90
+ for (const [lineFrom, styles] of lines) ranges.push(this.lineDecoration([...styles]).range(lineFrom));
91
+ for (const span of spans) ranges.push(this.markDecoration(span.styles).range(span.from, span.to));
92
+ for (const [lineFrom, styles] of lines) {
93
+ const line = doc.lineAt(lineFrom);
94
+ if (line.length && [...styles].some((s) => /^h[1-6]$/.test(s))) ranges.push(headingText.range(line.from, line.to));
95
+ }
96
+ if (this.fixWide) {
97
+ const cursorLines = new Set(view.state.selection.ranges.map((r) => doc.lineAt(r.head).from));
98
+ for (const [lineFrom, styles] of lines) if (styles.has("table") && !cursorLines.has(lineFrom)) this.wideRuns(doc.lineAt(lineFrom), ranges);
99
+ }
100
+ return Decoration.set(ranges, true);
101
+ }
102
+ /** Draws runs of wide characters exactly two columns each, so table pipes line up. */
103
+ wideRuns(line, ranges) {
104
+ let start = -1;
105
+ let columns = 0;
106
+ const flush = (end) => {
107
+ if (start < 0) return;
108
+ const key = `wide:${columns}`;
109
+ let deco = this.cache.get(key);
110
+ if (!deco) {
111
+ deco = Decoration.mark({
112
+ class: "sm-wide",
113
+ attributes: { style: `display: inline-block; width: ${columns}ch; text-align: center` }
114
+ });
115
+ this.cache.set(key, deco);
116
+ }
117
+ ranges.push(deco.range(line.from + start, line.from + end));
118
+ start = -1;
119
+ columns = 0;
120
+ };
121
+ let i = 0;
122
+ for (const ch of line.text) {
123
+ if (charWidth(ch.codePointAt(0)) === 2) {
124
+ if (start < 0) start = i;
125
+ columns += 2;
126
+ } else flush(i);
127
+ i += ch.length;
128
+ }
129
+ flush(i);
130
+ }
131
+ lineDecoration(styles) {
132
+ const key = `line:${styles.sort().join(" ")}`;
133
+ let deco = this.cache.get(key);
134
+ if (!deco) {
135
+ deco = Decoration.line({
136
+ class: styles.map((s) => `sm-line-${s}`).join(" "),
137
+ attributes: { style: lookCss(resolveLineLook(styles, this.theme), this.theme) }
138
+ });
139
+ this.cache.set(key, deco);
140
+ }
141
+ return deco;
142
+ }
143
+ markDecoration(styles) {
144
+ const key = `mark:${styles.join(" ")}`;
145
+ let deco = this.cache.get(key);
146
+ if (!deco) {
147
+ deco = Decoration.mark({
148
+ class: styles.map((s) => `sm-${s}`).join(" "),
149
+ attributes: { style: lookCss(resolveLook([], styles, this.theme), this.theme) }
150
+ });
151
+ this.cache.set(key, deco);
152
+ }
153
+ return deco;
154
+ }
155
+ };
156
+ /** Styles the visible Markdown from the syntax tree; never hides or replaces text. */
157
+ const markdownDecorations = ViewPlugin.fromClass(MarkdownDecorations, { decorations: (plugin) => plugin.decorations });
158
+ //#endregion
159
+ //#region packages/codemirror/src/line-separator.ts
160
+ /**
161
+ * Keeps a file's line endings. CodeMirror otherwise reads \r\n and \r as \n and writes \n,
162
+ * so a Windows file would not save back byte for byte. Read the text back with
163
+ * `state.sliceDoc()`, not `doc.toString()` (which always joins lines with \n).
164
+ */
165
+ function keepLineSeparator(text) {
166
+ const separator = text.includes("\r\n") ? "\r\n" : text.includes("\r") ? "\r" : null;
167
+ return separator ? EditorState.lineSeparator.of(separator) : [];
168
+ }
169
+ //#endregion
170
+ //#region packages/codemirror/src/math-loader.ts
171
+ let loaded;
172
+ let loading;
173
+ function loadMath() {
174
+ loading ??= import("@simple-mark/math").then((m) => loaded = m);
175
+ return loading;
176
+ }
177
+ /** The math renderer if it has loaded, for synchronous use of its cache. */
178
+ function loadedMath() {
179
+ return loaded;
180
+ }
181
+ /** Gap between images on one line (CSS gap). */
182
+ const IMAGE_GAP = 8;
183
+ /** Editor facts the estimates depend on, measured by `previewLayout`. */
184
+ const layout = {
185
+ contentWidth: 700,
186
+ exPx: 8,
187
+ lineHeight: 24,
188
+ fontSize: 15,
189
+ fontFamily: "sans-serif"
190
+ };
191
+ /** The measured editor facts (content width, line height, resolved text font). */
192
+ function layoutFacts() {
193
+ return layout;
194
+ }
195
+ const measured = /* @__PURE__ */ new Map();
196
+ const imageSizes = /* @__PURE__ */ new Map();
197
+ const probing = /* @__PURE__ */ new Set();
198
+ const mathQueue = /* @__PURE__ */ new Set();
199
+ let mathRunning = false;
200
+ const listeners = /* @__PURE__ */ new Set();
201
+ let notifyScheduled = false;
202
+ /** Tells every editor that estimates improved (batched). */
203
+ function estimatesImproved() {
204
+ if (notifyScheduled) return;
205
+ notifyScheduled = true;
206
+ setTimeout(() => {
207
+ notifyScheduled = false;
208
+ for (const listener of listeners) listener();
209
+ }, 30);
210
+ }
211
+ /** Refreshes the previews so their widgets carry the current estimates. */
212
+ const refreshEstimates = StateEffect.define();
213
+ /** Remembers the height of a drawn preview, as the estimate for when it is off screen. */
214
+ function rememberHeight(view, key, dom) {
215
+ view.requestMeasure({
216
+ read: () => dom.isConnected ? dom.getBoundingClientRect().height / view.scaleY : 0,
217
+ write: (height) => {
218
+ if (height > 0) measured.set(key, Math.round(height));
219
+ }
220
+ });
221
+ }
222
+ const sizeWaiters = /* @__PURE__ */ new Map();
223
+ function sizeKnown(src, size) {
224
+ if (imageSizes.has(src)) return;
225
+ imageSizes.set(src, size);
226
+ for (const waiter of sizeWaiters.get(src) ?? []) waiter(size);
227
+ sizeWaiters.delete(src);
228
+ estimatesImproved();
229
+ }
230
+ /**
231
+ * Watches an image until its size is known. The size is in the first bytes of the file, so it
232
+ * is checked every frame while loading instead of waiting for the whole (possibly large) file.
233
+ */
234
+ function watchImage(img, src) {
235
+ if (imageSizes.has(src)) return;
236
+ const natural = () => img.naturalWidth ? {
237
+ width: img.naturalWidth,
238
+ height: img.naturalHeight
239
+ } : {
240
+ width: 300,
241
+ height: 150
242
+ };
243
+ img.addEventListener("load", () => sizeKnown(src, natural()));
244
+ img.addEventListener("error", () => sizeKnown(src, null));
245
+ const check = () => {
246
+ if (imageSizes.has(src) || img.complete) return;
247
+ if (img.naturalWidth > 0) sizeKnown(src, natural());
248
+ else requestAnimationFrame(check);
249
+ };
250
+ requestAnimationFrame(check);
251
+ }
252
+ /** The size of an image if known: null if it failed, undefined while unknown. */
253
+ function knownImageSize(src) {
254
+ return imageSizes.get(src);
255
+ }
256
+ /** Calls `then` once the image's size is known (someone must be watching or probing it). */
257
+ function whenImageSized(src, then) {
258
+ const size = imageSizes.get(src);
259
+ if (size !== void 0) return then(size);
260
+ const waiters = sizeWaiters.get(src) ?? [];
261
+ waiters.push(then);
262
+ sizeWaiters.set(src, waiters);
263
+ }
264
+ /** Gives an <img> its final layout size before its pixels arrive (CSS caps it at the width). */
265
+ function reserveImageBox(img, size) {
266
+ img.style.aspectRatio = `${size.width} / ${size.height}`;
267
+ img.style.width = `${Math.min(size.width, 320 * size.width / size.height)}px`;
268
+ }
269
+ function probeImage(src) {
270
+ if (probing.has(src) || imageSizes.has(src) || typeof Image === "undefined") return;
271
+ probing.add(src);
272
+ const img = new Image();
273
+ watchImage(img, src);
274
+ img.src = src;
275
+ }
276
+ /** Displayed size of one image: fits the width, at most IMAGE_MAX_HEIGHT tall, never enlarged. */
277
+ function imageBox(src) {
278
+ const errorLine = {
279
+ width: layout.contentWidth,
280
+ height: layout.lineHeight
281
+ };
282
+ if (src === null) return errorLine;
283
+ const size = imageSizes.get(src);
284
+ if (size === void 0) {
285
+ probeImage(src);
286
+ return {
287
+ width: layout.contentWidth,
288
+ height: 160
289
+ };
290
+ }
291
+ if (size === null) return errorLine;
292
+ const scale = Math.min(1, layout.contentWidth / size.width, 320 / size.height);
293
+ return {
294
+ width: size.width * scale,
295
+ height: size.height * scale
296
+ };
297
+ }
298
+ /** Estimated height of a row of images (they wrap like the CSS flex row). */
299
+ function imagesEstimate(key, srcs) {
300
+ const known = measured.get(key);
301
+ if (known) return known;
302
+ let total = 0;
303
+ let rowWidth = 0;
304
+ let rowHeight = 0;
305
+ for (const src of srcs) {
306
+ const box = imageBox(src);
307
+ if (rowWidth > 0 && rowWidth + IMAGE_GAP + box.width > layout.contentWidth) {
308
+ total += rowHeight + IMAGE_GAP;
309
+ rowWidth = 0;
310
+ rowHeight = 0;
311
+ }
312
+ rowWidth += (rowWidth > 0 ? IMAGE_GAP : 0) + box.width;
313
+ rowHeight = Math.max(rowHeight, box.height);
314
+ }
315
+ return Math.round(total + rowHeight + 12);
316
+ }
317
+ /** Renders queued formulas one at a time, so their heights are known before they are shown. */
318
+ function renderQueuedMath() {
319
+ if (mathRunning) return;
320
+ mathRunning = true;
321
+ loadMath().then(async (math) => {
322
+ let done = 0;
323
+ for (const tex of mathQueue) {
324
+ await math.renderMath(tex, true);
325
+ mathQueue.delete(tex);
326
+ if (++done % 10 === 0) estimatesImproved();
327
+ await new Promise((resolve) => setTimeout(resolve, 0));
328
+ }
329
+ mathRunning = false;
330
+ estimatesImproved();
331
+ if (mathQueue.size) renderQueuedMath();
332
+ });
333
+ }
334
+ /** Estimated height of a display formula: from its rendered SVG, once known. */
335
+ function mathEstimate(key, tex) {
336
+ const known = measured.get(key);
337
+ if (known) return known;
338
+ const result = loadedMath()?.cachedMath(tex, true);
339
+ if (result?.svg !== void 0) {
340
+ const height = /height="([\d.]+)ex"/.exec(result.svg);
341
+ if (height) return Math.round(parseFloat(height[1]) * layout.exPx + 12);
342
+ } else if (result?.error !== void 0) return layout.lineHeight + 12;
343
+ else {
344
+ mathQueue.add(tex);
345
+ renderQueuedMath();
346
+ }
347
+ return 3 * layout.lineHeight + 12;
348
+ }
349
+ /** Measures the editor (content width, x-height, line height) and refreshes previews on news. */
350
+ const previewLayout = ViewPlugin.fromClass(class {
351
+ view;
352
+ constructor(view) {
353
+ this.view = view;
354
+ listeners.add(this.refresh);
355
+ this.measure();
356
+ }
357
+ update(update) {
358
+ if (update.geometryChanged) this.measure();
359
+ }
360
+ destroy() {
361
+ listeners.delete(this.refresh);
362
+ }
363
+ refresh = () => {
364
+ this.view.dispatch({ effects: refreshEstimates.of(null) });
365
+ };
366
+ measure() {
367
+ this.view.requestMeasure({
368
+ read: (view) => {
369
+ const style = getComputedStyle(view.contentDOM);
370
+ const width = view.contentDOM.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
371
+ const ctx = document.createElement("canvas").getContext("2d");
372
+ let exPx = layout.exPx;
373
+ if (ctx) {
374
+ ctx.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
375
+ exPx = ctx.measureText("x").actualBoundingBoxAscent || exPx;
376
+ }
377
+ const lineHeight = style.lineHeight.endsWith("px") ? parseFloat(style.lineHeight) : view.defaultLineHeight;
378
+ return {
379
+ contentWidth: width,
380
+ exPx,
381
+ lineHeight,
382
+ fontSize: parseFloat(style.fontSize),
383
+ fontFamily: style.fontFamily
384
+ };
385
+ },
386
+ write: (next) => {
387
+ if ((Math.abs(next.contentWidth - layout.contentWidth) > 1 || Math.abs(next.exPx - layout.exPx) > .2 || Math.abs(next.lineHeight - layout.lineHeight) > .5 || next.fontSize !== layout.fontSize || next.fontFamily !== layout.fontFamily) && next.contentWidth > 0) {
388
+ if (Math.abs(next.contentWidth - layout.contentWidth) > 1) measured.clear();
389
+ Object.assign(layout, next);
390
+ estimatesImproved();
391
+ }
392
+ }
393
+ });
394
+ }
395
+ });
396
+ //#endregion
397
+ //#region packages/codemirror/src/line-heights.ts
398
+ var HeightHint = class extends WidgetType {
399
+ height;
400
+ constructor(height) {
401
+ super();
402
+ this.height = height;
403
+ }
404
+ eq(other) {
405
+ return other.height === this.height;
406
+ }
407
+ updateDOM() {
408
+ return true;
409
+ }
410
+ get estimatedHeight() {
411
+ return this.height;
412
+ }
413
+ toDOM() {
414
+ const span = document.createElement("span");
415
+ span.className = "sm-height-hint";
416
+ span.style.display = "none";
417
+ return span;
418
+ }
419
+ ignoreEvent() {
420
+ return true;
421
+ }
422
+ };
423
+ let context;
424
+ function measureContext() {
425
+ if (context === void 0) context = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
426
+ return context;
427
+ }
428
+ const HEADING = /^ {0,3}(#{1,6})(?:\s|$)/;
429
+ /** Estimated drawn height of a line, or 0 when CodeMirror's own one-line guess is right. */
430
+ function lineEstimate(text, theme) {
431
+ const heading = HEADING.exec(text);
432
+ if (!heading && text.length < 16) return 0;
433
+ const ctx = measureContext();
434
+ if (!ctx) return 0;
435
+ const facts = layoutFacts();
436
+ const look = heading ? theme.block[`h${heading[1].length}`] : void 0;
437
+ const scale = look?.fontScale ?? 1;
438
+ ctx.font = `${look?.fontWeight ?? 400} ${facts.fontSize * scale}px ${facts.fontFamily}`;
439
+ const lines = Math.max(1, Math.ceil(ctx.measureText(text).width / (facts.contentWidth * .97)));
440
+ const height = Math.round(lines * facts.lineHeight * scale);
441
+ return height > facts.lineHeight + 1 ? height : 0;
442
+ }
443
+ function hintAt(state, lineNumber, theme) {
444
+ const line = state.doc.line(lineNumber);
445
+ const height = lineEstimate(line.text, theme);
446
+ return height ? Decoration.widget({
447
+ widget: new HeightHint(height),
448
+ side: -1
449
+ }).range(line.from) : null;
450
+ }
451
+ function cursorLines(state) {
452
+ return new Set(state.selection.ranges.map((r) => state.doc.lineAt(r.head).number));
453
+ }
454
+ function allHints(state) {
455
+ const theme = state.facet(markdownTheme);
456
+ const skip = cursorLines(state);
457
+ const ranges = [];
458
+ for (let n = 1; n <= state.doc.lines; n++) {
459
+ if (skip.has(n)) continue;
460
+ const hint = hintAt(state, n, theme);
461
+ if (hint) ranges.push(hint);
462
+ }
463
+ return Decoration.set(ranges);
464
+ }
465
+ const heightHints = StateField.define({
466
+ create: allHints,
467
+ update(hints, tr) {
468
+ if (tr.effects.some((effect) => effect.is(refreshEstimates)) || tr.state.facet(markdownTheme) !== tr.startState.facet(markdownTheme)) return allHints(tr.state);
469
+ if (!tr.docChanged && !tr.selection) return hints;
470
+ const { state } = tr;
471
+ hints = hints.map(tr.changes);
472
+ const redo = /* @__PURE__ */ new Set();
473
+ tr.changes.iterChangedRanges((_fromA, _toA, fromB, toB) => {
474
+ for (let n = state.doc.lineAt(fromB).number; n <= state.doc.lineAt(toB).number; n++) redo.add(n);
475
+ });
476
+ for (const r of tr.startState.selection.ranges) redo.add(state.doc.lineAt(tr.changes.mapPos(r.head)).number);
477
+ const skip = cursorLines(state);
478
+ for (const n of skip) redo.add(n);
479
+ const theme = state.facet(markdownTheme);
480
+ for (const n of redo) {
481
+ const line = state.doc.line(n);
482
+ const hint = skip.has(n) ? null : hintAt(state, n, theme);
483
+ hints = hints.update({
484
+ filter: () => false,
485
+ filterFrom: line.from,
486
+ filterTo: line.from,
487
+ add: hint ? [hint] : []
488
+ });
489
+ }
490
+ return hints;
491
+ },
492
+ provide: (field) => EditorView.decorations.from(field)
493
+ });
494
+ /** Better height estimates for headings and wrapping lines that are not drawn yet. */
495
+ function lineHeightHints() {
496
+ return heightHints;
497
+ }
498
+ //#endregion
499
+ //#region packages/codemirror/src/previews.ts
500
+ /** How image paths are resolved; by default they are used as written. */
501
+ const imageResolver = Facet.define({ combine: (values) => values[0] ?? ((src) => src) });
502
+ /** Fills `dom` with rendered math now if known, otherwise when it is ready. */
503
+ function renderInto(dom, tex, display, onDone) {
504
+ const show = (result) => {
505
+ dom.textContent = "";
506
+ if (result.svg !== void 0) dom.innerHTML = result.svg;
507
+ else {
508
+ const error = dom.appendChild(document.createElement("span"));
509
+ error.className = "sm-math-error";
510
+ error.textContent = `Math error: ${result.error}`;
511
+ }
512
+ onDone?.();
513
+ };
514
+ const known = loadedMath()?.cachedMath(tex, display);
515
+ if (known) return show(known);
516
+ dom.textContent = "…";
517
+ loadMath().then((m) => m.renderMath(tex, display)).then(show, (error) => show({ error: `could not load the math renderer (${String(error)})` }));
518
+ }
519
+ var MathBlockWidget = class extends WidgetType {
520
+ tex;
521
+ key;
522
+ estimate;
523
+ constructor(tex) {
524
+ super();
525
+ this.tex = tex;
526
+ this.key = `math\n${tex}`;
527
+ this.estimate = mathEstimate(this.key, tex);
528
+ }
529
+ eq(other) {
530
+ return other.key === this.key && other.estimate === this.estimate;
531
+ }
532
+ /** Only the estimate changed: keep the drawn formula. */
533
+ updateDOM(dom) {
534
+ return dom.dataset["key"] === this.key;
535
+ }
536
+ get estimatedHeight() {
537
+ return this.estimate;
538
+ }
539
+ toDOM(view) {
540
+ const dom = document.createElement("div");
541
+ dom.className = "sm-math-preview";
542
+ dom.dataset["key"] = this.key;
543
+ dom.style.minHeight = `${this.estimate - 12}px`;
544
+ renderInto(dom, this.tex, true, () => {
545
+ dom.style.minHeight = "";
546
+ view.requestMeasure();
547
+ rememberHeight(view, this.key, dom);
548
+ });
549
+ return dom;
550
+ }
551
+ };
552
+ /** An image from the document with its path resolved by the host. */
553
+ function imageRef(image, resolve) {
554
+ if (image.src === null) return {
555
+ raw: image.raw,
556
+ src: null,
557
+ alt: image.alt,
558
+ problem: `No definition for ${image.raw}`
559
+ };
560
+ const src = resolve(image.src);
561
+ return src === null ? {
562
+ raw: image.raw,
563
+ src,
564
+ alt: image.alt,
565
+ problem: `Can't load ${image.raw}`
566
+ } : {
567
+ raw: image.raw,
568
+ src,
569
+ alt: image.alt
570
+ };
571
+ }
572
+ var ImageWidget = class extends WidgetType {
573
+ images;
574
+ key;
575
+ estimate;
576
+ constructor(images) {
577
+ super();
578
+ this.images = images;
579
+ this.key = `images\n${JSON.stringify(images)}`;
580
+ this.estimate = imagesEstimate(this.key, images.map((image) => image.src));
581
+ }
582
+ eq(other) {
583
+ return other.key === this.key && other.estimate === this.estimate;
584
+ }
585
+ /** Only the estimate changed: keep the drawn images. */
586
+ updateDOM(dom) {
587
+ return dom.dataset["key"] === this.key;
588
+ }
589
+ get estimatedHeight() {
590
+ return this.estimate;
591
+ }
592
+ toDOM(view) {
593
+ const dom = document.createElement("div");
594
+ dom.className = "sm-image-preview";
595
+ dom.dataset["key"] = this.key;
596
+ let waiting = 0;
597
+ let unsized = 0;
598
+ const settled = () => {
599
+ view.requestMeasure();
600
+ if (--waiting === 0) rememberHeight(view, this.key, dom);
601
+ };
602
+ for (const image of this.images) {
603
+ const problem = (text) => {
604
+ const span = document.createElement("span");
605
+ span.className = "sm-image-error";
606
+ span.textContent = text;
607
+ return span;
608
+ };
609
+ if (image.src === null) {
610
+ dom.appendChild(problem(image.problem ?? `Can't load ${image.raw}`));
611
+ continue;
612
+ }
613
+ waiting++;
614
+ const img = dom.appendChild(document.createElement("img"));
615
+ const size = knownImageSize(image.src);
616
+ if (size) reserveImageBox(img, size);
617
+ else if (size === void 0) {
618
+ unsized++;
619
+ watchImage(img, image.src);
620
+ whenImageSized(image.src, (known) => {
621
+ if (known) reserveImageBox(img, known);
622
+ if (--unsized === 0) dom.style.minHeight = "";
623
+ view.requestMeasure();
624
+ });
625
+ }
626
+ img.decoding = "async";
627
+ img.src = image.src;
628
+ img.alt = image.alt;
629
+ img.title = image.raw;
630
+ img.onload = settled;
631
+ img.onerror = () => {
632
+ img.replaceWith(problem(`Image not found: ${image.raw}`));
633
+ settled();
634
+ };
635
+ }
636
+ if (unsized > 0) dom.style.minHeight = `${this.estimate - 12}px`;
637
+ if (waiting === 0) rememberHeight(view, this.key, dom);
638
+ return dom;
639
+ }
640
+ };
641
+ /** What to preview below which lines: the images of each line, and closed non-empty $$ blocks. */
642
+ function blockPreviewSources(state) {
643
+ const resolve = state.facet(imageResolver);
644
+ return previewBlocks(syntaxTree(state), state.doc.toString()).map((block) => "tex" in block ? block : {
645
+ at: block.at,
646
+ images: block.images.map((image) => imageRef(image, resolve))
647
+ });
648
+ }
649
+ function blockPreviews(state) {
650
+ const ranges = blockPreviewSources(state).map((preview) => {
651
+ const widget = "tex" in preview ? new MathBlockWidget(preview.tex) : new ImageWidget(preview.images);
652
+ return Decoration.widget({
653
+ widget,
654
+ block: true,
655
+ side: 1
656
+ }).range(preview.at);
657
+ });
658
+ return Decoration.set(ranges, true);
659
+ }
660
+ /** Block widgets change line heights, so CodeMirror wants them from a state field. */
661
+ const blockPreviewField = StateField.define({
662
+ create: blockPreviews,
663
+ update(previews, tr) {
664
+ return tr.docChanged || syntaxTree(tr.state) !== syntaxTree(tr.startState) || tr.state.facet(imageResolver) !== tr.startState.facet(imageResolver) || tr.effects.some((effect) => effect.is(refreshEstimates)) ? blockPreviews(tr.state) : previews;
665
+ },
666
+ provide: (field) => EditorView.decorations.from(field)
667
+ });
668
+ function inlineMathAt(state, pos) {
669
+ const tree = syntaxTree(state);
670
+ for (const side of [1, -1]) for (let node = tree.resolveInner(pos, side); node; node = node.parent) if (node.name === "InlineMath") return node;
671
+ return null;
672
+ }
673
+ function mathTooltip(state, pos) {
674
+ const node = inlineMathAt(state, pos);
675
+ const tex = node && mathSource(node, (from, to) => state.sliceDoc(from, to));
676
+ if (!node || !tex) return null;
677
+ const display = node.getChild("MathMark").to - node.from === 2;
678
+ return {
679
+ pos: node.from,
680
+ end: node.to,
681
+ above: true,
682
+ create: (view) => {
683
+ const dom = document.createElement("div");
684
+ dom.className = "sm-math-tooltip";
685
+ renderInto(dom, tex, display, () => view.requestMeasure());
686
+ return { dom };
687
+ }
688
+ };
689
+ }
690
+ /** The tooltip for inline math under the cursor. */
691
+ const cursorMathTooltip = StateField.define({
692
+ create: (state) => state.selection.main.empty ? mathTooltip(state, state.selection.main.head) : null,
693
+ update(tooltip, tr) {
694
+ if (!tr.docChanged && !tr.selection) return tooltip;
695
+ const { main } = tr.state.selection;
696
+ const next = main.empty ? mathTooltip(tr.state, main.head) : null;
697
+ if (next && tooltip && !tr.docChanged && next.pos === tooltip.pos && next.end === tooltip.end) return tooltip;
698
+ return next;
699
+ },
700
+ provide: (field) => showTooltip.from(field)
701
+ });
702
+ const hoverMathTooltip = hoverTooltip((view, pos) => {
703
+ const cursor = view.state.field(cursorMathTooltip, false);
704
+ const tooltip = mathTooltip(view.state, pos);
705
+ return tooltip && cursor?.pos === tooltip.pos ? null : tooltip;
706
+ });
707
+ const previewTheme = EditorView.theme({
708
+ ".sm-image-preview": {
709
+ display: "flex",
710
+ flexWrap: "wrap",
711
+ gap: "8px",
712
+ padding: "4px 0 8px"
713
+ },
714
+ ".sm-image-preview img": {
715
+ maxWidth: "100%",
716
+ maxHeight: "320px",
717
+ borderRadius: "4px"
718
+ },
719
+ ".sm-image-error, .sm-math-error": {
720
+ color: "#cf222e",
721
+ fontSize: "0.9em"
722
+ },
723
+ ".sm-math-preview": {
724
+ textAlign: "center",
725
+ padding: "4px 0 8px",
726
+ overflowX: "auto",
727
+ overflowY: "hidden"
728
+ },
729
+ ".sm-math-tooltip": { padding: "6px 10px" }
730
+ });
731
+ /** Image and math previews; the source text always stays visible. */
732
+ function previews() {
733
+ return [
734
+ blockPreviewField,
735
+ previewLayout,
736
+ lineHeightHints(),
737
+ cursorMathTooltip,
738
+ hoverMathTooltip,
739
+ previewTheme
740
+ ];
741
+ }
742
+ //#endregion
743
+ //#region packages/codemirror/src/table-commands.ts
744
+ /** Children of a Table node that are whole lines (the delimiter line is a TableDelimiter). */
745
+ const ROW_NODES = /* @__PURE__ */ new Set([
746
+ "TableHeader",
747
+ "TableDelimiter",
748
+ "TableRow"
749
+ ]);
750
+ function findTable(state, pos) {
751
+ const tree = ensureSyntaxTree(state, pos, 50) ?? syntaxTree(state);
752
+ for (const side of [1, -1]) for (let node = tree.resolveInner(pos, side); node; node = node.parent) if (node.name === "Table") return node;
753
+ return null;
754
+ }
755
+ /** The table under the cursor and the cell the cursor is in. */
756
+ function tableAt(state) {
757
+ const pos = state.selection.main.head;
758
+ const node = findTable(state, pos);
759
+ if (!node) return null;
760
+ const lines = [];
761
+ for (let child = node.firstChild; child; child = child.nextSibling) {
762
+ if (!ROW_NODES.has(child.name)) continue;
763
+ const line = state.doc.lineAt(child.from);
764
+ if (lines.at(-1)?.from === line.from) continue;
765
+ lines.push({
766
+ from: line.from,
767
+ to: line.to,
768
+ prefix: state.sliceDoc(line.from, child.from),
769
+ contentFrom: child.from,
770
+ content: state.sliceDoc(child.from, line.to)
771
+ });
772
+ }
773
+ const index = lines.findIndex((l) => pos >= l.from && pos <= l.to);
774
+ if (lines.length < 2 || index === -1) return null;
775
+ const line = lines[index];
776
+ return {
777
+ lines,
778
+ table: parseTable(lines.map((l) => l.content)),
779
+ row: index <= 1 ? 0 : index - 1,
780
+ col: cellAt(line.content, Math.max(0, pos - line.contentFrom))
781
+ };
782
+ }
783
+ /**
784
+ * Replaces the table with `table` re-aligned and puts the cursor at the end of the text of cell
785
+ * (`row`, `col`). `after` is inserted right after the table; with `cursorAfter` the cursor goes
786
+ * to its end instead.
787
+ */
788
+ function writeTable(state, at, table, row, col, after = "", cursorAfter = false) {
789
+ const { lines, cells } = renderTable(table);
790
+ const prefix = (i) => at.lines[Math.min(i, at.lines.length - 1)].prefix;
791
+ const full = lines.map((line, i) => prefix(i) + line);
792
+ const insert = full.join(state.lineBreak) + after;
793
+ const from = at.lines[0].from;
794
+ let cursor = from + insert.length;
795
+ if (!cursorAfter) {
796
+ const target = rowLine(row);
797
+ cursor = from + full.slice(0, target).reduce((sum, line) => sum + line.length + state.lineBreak.length, 0) + prefix(target).length + cells[row][col].to;
798
+ }
799
+ return state.update({
800
+ changes: {
801
+ from,
802
+ to: at.lines.at(-1).to,
803
+ insert
804
+ },
805
+ selection: { anchor: cursor },
806
+ scrollIntoView: true,
807
+ userEvent: "input.table"
808
+ });
809
+ }
810
+ const emptyRow = (table) => table.aligns.map(() => "");
811
+ /** Tab: re-align and go to the next cell, adding a row after the last one. */
812
+ const nextCell = ({ state, dispatch }) => {
813
+ const at = tableAt(state);
814
+ if (!at) return false;
815
+ const { table } = at;
816
+ const columns = table.aligns.length;
817
+ let { row, col } = at;
818
+ if (col + 1 < columns) col++;
819
+ else {
820
+ row++;
821
+ col = 0;
822
+ if (row >= table.rows.length) table.rows.push(emptyRow(table));
823
+ }
824
+ dispatch(writeTable(state, at, table, row, col));
825
+ return true;
826
+ };
827
+ /** Shift-Tab: re-align and go to the previous cell. */
828
+ const previousCell = ({ state, dispatch }) => {
829
+ const at = tableAt(state);
830
+ if (!at) return false;
831
+ let { row, col } = at;
832
+ if (col > 0) col = Math.min(col, at.table.aligns.length) - 1;
833
+ else if (row > 0) {
834
+ row--;
835
+ col = at.table.aligns.length - 1;
836
+ }
837
+ dispatch(writeTable(state, at, at.table, row, col));
838
+ return true;
839
+ };
840
+ /**
841
+ * Enter: re-align and add an empty row below, cursor in its first cell. On an empty last row,
842
+ * remove it and leave the table: the cursor goes below a blank line, because GFM would read
843
+ * a line right after a table as another row.
844
+ */
845
+ const insertRowBelow = ({ state, dispatch }) => {
846
+ const at = tableAt(state);
847
+ if (!at) return false;
848
+ const { table, row } = at;
849
+ if (row > 0 && row === table.rows.length - 1 && table.rows[row].every((cell) => cell === "")) {
850
+ table.rows.splice(row, 1);
851
+ dispatch(writeTable(state, at, table, 0, 0, state.lineBreak + state.lineBreak, true));
852
+ return true;
853
+ }
854
+ table.rows.splice(row + 1, 0, emptyRow(table));
855
+ dispatch(writeTable(state, at, table, row + 1, 0));
856
+ return true;
857
+ };
858
+ /**
859
+ * A typed line break inside a table becomes "add a row below". This catches the line breaks
860
+ * the Enter command never sees: while an IME is composing (Korean: Enter right after a
861
+ * syllable), CodeMirror skips its keymaps and the browser inserts the break itself, sometimes
862
+ * before the syllable still being composed. The typed text stays in its cell.
863
+ */
864
+ const lineBreakToRow = EditorState.transactionFilter.of((tr) => {
865
+ if (!tr.docChanged || !tr.isUserEvent("input") || tr.isUserEvent("input.paste") || tr.isUserEvent("input.drop") || tr.isUserEvent("input.table")) return tr;
866
+ const { startState } = tr;
867
+ const kept = [];
868
+ let breakAt = null;
869
+ let other = false;
870
+ tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => {
871
+ const removedBreaks = startState.doc.lineAt(toA).number - startState.doc.lineAt(fromA).number;
872
+ const lines = inserted.toJSON();
873
+ if (removedBreaks === 0 && lines.length === 2 && !breakAt) {
874
+ breakAt = {
875
+ from: fromA,
876
+ offset: lines[0].length
877
+ };
878
+ kept.push({
879
+ from: fromA,
880
+ to: toA,
881
+ insert: lines.join("")
882
+ });
883
+ } else if (lines.length - 1 !== removedBreaks) other = true;
884
+ else kept.push({
885
+ from: fromA,
886
+ to: toA,
887
+ insert: inserted.sliceString(0, inserted.length, startState.lineBreak)
888
+ });
889
+ });
890
+ const at = breakAt;
891
+ if (!at || other) return tr;
892
+ const withoutBreak = startState.changes(kept);
893
+ const pos = withoutBreak.mapPos(at.from, -1) + at.offset;
894
+ const state = startState.update({
895
+ changes: withoutBreak,
896
+ selection: { anchor: pos }
897
+ }).state;
898
+ let row = null;
899
+ if (!insertRowBelow({
900
+ state,
901
+ dispatch: (t) => row = t
902
+ })) return tr;
903
+ const done = row;
904
+ return {
905
+ changes: withoutBreak.compose(done.changes),
906
+ selection: done.selection,
907
+ scrollIntoView: true,
908
+ userEvent: "input.table"
909
+ };
910
+ });
911
+ /**
912
+ * Tab while an IME is composing: CodeMirror skips its keymaps then, and the browser would move
913
+ * the focus out of the editor. Take the key, let the IME commit, then move to the cell.
914
+ */
915
+ const tabWhileComposing = ViewPlugin.fromClass(class {
916
+ view;
917
+ constructor(view) {
918
+ this.view = view;
919
+ view.dom.addEventListener("keydown", this.onKeydown, true);
920
+ }
921
+ destroy() {
922
+ this.view.dom.removeEventListener("keydown", this.onKeydown, true);
923
+ }
924
+ onKeydown = (event) => {
925
+ const { view } = this;
926
+ if (event.key !== "Tab" || event.ctrlKey || event.altKey || event.metaKey) return;
927
+ if (!(event.isComposing || event.keyCode === 229 || view.compositionStarted) || !tableAt(view.state)) return;
928
+ event.preventDefault();
929
+ event.stopPropagation();
930
+ const command = event.shiftKey ? previousCell : nextCell;
931
+ const start = Date.now();
932
+ const run = () => {
933
+ if (!view.compositionStarted) command(view);
934
+ else if (Date.now() - start < 1e3) setTimeout(run, 20);
935
+ };
936
+ setTimeout(run, 0);
937
+ };
938
+ });
939
+ /** Table editing keys, ahead of the default and Markdown keymaps, and their IME counterparts. */
940
+ function tableEditing() {
941
+ return [
942
+ Prec.high(keymap.of([{
943
+ key: "Tab",
944
+ run: nextCell,
945
+ shift: previousCell
946
+ }, {
947
+ key: "Enter",
948
+ run: insertRowBelow
949
+ }])),
950
+ lineBreakToRow,
951
+ tabWhileComposing
952
+ ];
953
+ }
954
+ //#endregion
955
+ //#region packages/codemirror/src/index.ts
956
+ /** The SimpleMark language and styling, for hosts that build their own EditorState. */
957
+ function simpleMark(theme = defaultTheme) {
958
+ return [
959
+ tableEditing(),
960
+ markdown({
961
+ base: commonmarkLanguage,
962
+ extensions: markdownExtensions,
963
+ completeHTMLTags: false
964
+ }),
965
+ markdownTheme.of(theme),
966
+ markdownDecorations,
967
+ previews(),
968
+ EditorView.theme({
969
+ "&": { color: theme.color },
970
+ ".cm-scroller": {
971
+ fontFamily: theme.fontFamily,
972
+ lineHeight: "1.6"
973
+ }
974
+ })
975
+ ];
976
+ }
977
+ /** A Markdown editor in `parent`. The text is kept exactly as given, line endings included. */
978
+ function createEditor(parent, options = {}) {
979
+ const { doc = "", readOnly = false, theme = defaultTheme, resolveImage, onChange, extensions = [] } = options;
980
+ const useHistory = options.history ?? true;
981
+ return new EditorView({
982
+ parent,
983
+ state: EditorState.create({
984
+ doc,
985
+ extensions: [
986
+ options.lineSeparator ? EditorState.lineSeparator.of(options.lineSeparator) : keepLineSeparator(doc),
987
+ useHistory ? history() : [],
988
+ keymap.of([...defaultKeymap, ...useHistory ? historyKeymap : []]),
989
+ EditorView.lineWrapping,
990
+ simpleMark(theme),
991
+ resolveImage ? imageResolver.of(resolveImage) : [],
992
+ EditorState.readOnly.of(readOnly),
993
+ EditorView.editable.of(!readOnly),
994
+ onChange ? EditorView.updateListener.of((update) => update.docChanged && onChange(update.state.sliceDoc(), update)) : [],
995
+ extensions
996
+ ]
997
+ })
998
+ });
999
+ }
1000
+ //#endregion
1001
+ export { createEditor, imageResolver, insertRowBelow, keepLineSeparator, lookCss, markdownDecorations, markdownTheme, nextCell, previews, previousCell, simpleMark, tableEditing };
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@simple-mark/codemirror",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "CodeMirror 6 editor that styles Markdown without hiding its syntax.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/one-prime/simple-mark.git",
10
+ "directory": "packages/codemirror"
11
+ },
12
+ "keywords": [
13
+ "markdown",
14
+ "wysiwyg",
15
+ "editor",
16
+ "gfm"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./index.d.ts",
21
+ "import": "./index.js"
22
+ }
23
+ },
24
+ "dependencies": {
25
+ "@codemirror/commands": "^6.11.1",
26
+ "@codemirror/lang-markdown": "^6.5.2",
27
+ "@codemirror/language": "^6.13.1",
28
+ "@codemirror/state": "^6.7.6",
29
+ "@codemirror/view": "^6.43.15",
30
+ "@lezer/common": "^1.5.3",
31
+ "@simple-mark/math": "^0.2.0",
32
+ "@simple-mark/model": "^0.2.0"
33
+ }
34
+ }