@simple-mark/codemirror 0.2.0 → 0.3.1

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 (4) hide show
  1. package/README.md +5 -0
  2. package/index.d.ts +33 -5
  3. package/index.js +174 -32
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -11,6 +11,7 @@ const view = createEditor(document.getElementById('editor')!, {
11
11
  doc: '# Hello **world**\n\n| a | b |\n|---|---|\n| 1 | 2 |\n',
12
12
  onChange: (text) => save(text), // in the document's own line endings
13
13
  resolveImage: (src) => new URL(src, base).href,
14
+ openLink: (target) => open(target), // { kind: 'web' | 'file' | 'image', url }
14
15
  });
15
16
  ```
16
17
 
@@ -19,6 +20,10 @@ const view = createEditor(document.getElementById('editor')!, {
19
20
  them with an IME is safe.
20
21
  - **Images** and `$$` math preview below their source; `$inline$` math in a tooltip. Math
21
22
  (`@simple-mark/math`) loads only when a document has math.
23
+ - **Links**: Ctrl+Click (⌘+Click on macOS) follows a link — a plain click keeps placing the
24
+ cursor, since links are editable text. `#heading` links jump within the document; other
25
+ schemes than http(s)/mailto are refused; web pages and paths go to `openLink` (default: a
26
+ new browser tab). A click on an image preview opens the image.
22
27
  - **Line endings** are kept (`\r\n`, `\r`), so an unedited file saves back byte for byte.
23
28
  - Accurate scroll heights for documents with many images and formulas.
24
29
  - `simpleMark(theme)` gives the extensions for your own `EditorState`.
package/index.d.ts CHANGED
@@ -1,11 +1,34 @@
1
1
  import { EditorState, Extension, Facet, StateCommand } from "@codemirror/state";
2
2
  import { DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
3
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. */
4
+ //#region packages/codemirror/src/links.d.ts
5
+ /** What to open: a web address, or a path as written (an image, another document, …). */
6
+ interface LinkTarget {
7
+ kind: 'web' | 'file' | 'image';
8
+ url: string;
9
+ }
10
+ /** Opens a link target; hosts replace this with their own (see `createEditor`'s `openLink`). */
11
+ type LinkOpener = (target: LinkTarget, view: EditorView) => void;
12
+ /** How links are opened. */
13
+ export declare const linkOpener: Facet<LinkOpener, LinkOpener>;
14
+ /**
15
+ * Follows a link target as written (`isImage`: from `![alt](src)`). Returns false when there
16
+ * is nothing to open: no target, an unsafe scheme, or an anchor no heading has.
17
+ */
18
+ export declare function followLink(view: EditorView, target: string | null, isImage?: boolean): boolean;
19
+ /** Ctrl/Cmd+Click follows links; `#anchors` jump to headings. */
20
+ export declare function links(): Extension;
21
+ //#endregion
22
+ //#region packages/codemirror/src/image-resolver.d.ts
23
+ /**
24
+ * Turns a path as written in the document into a URL to load, or several to try in order
25
+ * (the first that loads is shown), or null if it can't be loaded.
26
+ */
27
+ type ImageResolver = (src: string) => string | readonly string[] | null;
28
+ /** How image (and file) paths are resolved; by default they are used as written. */
8
29
  export declare const imageResolver: Facet<ImageResolver, ImageResolver>;
30
+ //#endregion
31
+ //#region packages/codemirror/src/previews.d.ts
9
32
  /** Image and math previews; the source text always stays visible. */
10
33
  export declare function previews(): Extension;
11
34
  //#endregion
@@ -63,6 +86,11 @@ export interface EditorOptions {
63
86
  lineSeparator?: string;
64
87
  /** Turns an image path from the document into a URL to load (default: as written). */
65
88
  resolveImage?: ImageResolver;
89
+ /**
90
+ * Opens a followed link (Ctrl/Cmd+Click) or a clicked image preview. Default: web addresses
91
+ * and paths (resolved like images) in a new browser tab.
92
+ */
93
+ openLink?: LinkOpener;
66
94
  /** Off when the host owns undo (VS Code). */
67
95
  history?: boolean;
68
96
  /** Called after every change with the full text, in the original line endings. */
@@ -72,4 +100,4 @@ export interface EditorOptions {
72
100
  /** A Markdown editor in `parent`. The text is kept exactly as given, line endings included. */
73
101
  export declare function createEditor(parent: HTMLElement, options?: EditorOptions): EditorView;
74
102
  //#endregion
75
- export type { ImageResolver };
103
+ export type { ImageResolver, LinkOpener, LinkTarget };
package/index.js CHANGED
@@ -2,8 +2,115 @@ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
2
2
  import { commonmarkLanguage, markdown } from "@codemirror/lang-markdown";
3
3
  import { EditorState, Facet, Prec, StateEffect, StateField } from "@codemirror/state";
4
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";
5
+ import { cellAt, charWidth, defaultTheme, headingAnchors, linkAt, linkKind, markdownExtensions, markdownStyles, mathSource, parseTable, previewBlocks, renderTable, resolveLineLook, resolveLook, rowLine } from "@simple-mark/model";
6
6
  import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
7
+ //#region packages/codemirror/src/image-resolver.ts
8
+ /** The URLs a resolver gives for `src`, in order. */
9
+ function resolvedUrls(resolve, src) {
10
+ const result = resolve(src);
11
+ return result === null ? [] : typeof result === "string" ? [result] : [...result];
12
+ }
13
+ /** How image (and file) paths are resolved; by default they are used as written. */
14
+ const imageResolver = Facet.define({ combine: (values) => values[0] ?? ((src) => src) });
15
+ //#endregion
16
+ //#region packages/codemirror/src/links.ts
17
+ /** In a browser: web addresses and paths (resolved like images) open in a new tab. */
18
+ const openInBrowser = (target, view) => {
19
+ const url = target.kind === "web" ? target.url : resolvedUrls(view.state.facet(imageResolver), target.url)[0];
20
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
21
+ };
22
+ /** How links are opened. */
23
+ const linkOpener = Facet.define({ combine: (values) => values[0] ?? openInBrowser });
24
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform);
25
+ /** The key held to follow a link: Cmd on macOS, Ctrl elsewhere. */
26
+ const followKeyName = isMac ? "⌘" : "Ctrl";
27
+ const followKey = (event) => isMac ? event.metaKey : event.ctrlKey;
28
+ /**
29
+ * Follows a link target as written (`isImage`: from `![alt](src)`). Returns false when there
30
+ * is nothing to open: no target, an unsafe scheme, or an anchor no heading has.
31
+ */
32
+ function followLink(view, target, isImage = false) {
33
+ if (target === null) return false;
34
+ const { kind, url } = linkKind(target);
35
+ if (kind === "unsafe") return false;
36
+ if (kind === "anchor") {
37
+ const wanted = decodeURIComponent(url.slice(1)).toLowerCase();
38
+ const heading = headingAnchors(syntaxTree(view.state), view.state.doc.toString()).find((h) => h.anchor === wanted);
39
+ if (!heading) return false;
40
+ view.dispatch({
41
+ selection: { anchor: heading.from },
42
+ effects: EditorView.scrollIntoView(heading.from, {
43
+ y: "start",
44
+ yMargin: 24
45
+ })
46
+ });
47
+ view.focus();
48
+ return true;
49
+ }
50
+ view.state.facet(linkOpener)({
51
+ kind: kind === "file" && isImage ? "image" : kind,
52
+ url
53
+ }, view);
54
+ return true;
55
+ }
56
+ /** Ctrl/Cmd+Click on a link follows it (instead of adding a cursor there). */
57
+ const clickToFollow = EditorView.domEventHandlers({ mousedown(event, view) {
58
+ if (event.button !== 0 || !followKey(event) || event.shiftKey || event.altKey) return false;
59
+ if (!(event.target instanceof Element) || !event.target.closest(".sm-link, .sm-url")) return false;
60
+ const pos = view.posAtCoords({
61
+ x: event.clientX,
62
+ y: event.clientY
63
+ });
64
+ if (pos === null) return false;
65
+ const link = linkAt(syntaxTree(view.state), view.state.doc.toString(), pos);
66
+ if (!link) return false;
67
+ event.preventDefault();
68
+ followLink(view, link.target, link.kind === "image");
69
+ return true;
70
+ } });
71
+ /** While Ctrl/Cmd is held, links look clickable (pointer, underline). */
72
+ const followKeyHint = ViewPlugin.fromClass(class {
73
+ view;
74
+ constructor(view) {
75
+ this.view = view;
76
+ for (const type of [
77
+ "keydown",
78
+ "keyup",
79
+ "mousemove"
80
+ ]) window.addEventListener(type, this.track, true);
81
+ window.addEventListener("blur", this.clear);
82
+ }
83
+ destroy() {
84
+ for (const type of [
85
+ "keydown",
86
+ "keyup",
87
+ "mousemove"
88
+ ]) window.removeEventListener(type, this.track, true);
89
+ window.removeEventListener("blur", this.clear);
90
+ }
91
+ track = (event) => {
92
+ this.view.dom.classList.toggle("sm-follow-links", followKey(event));
93
+ };
94
+ clear = () => {
95
+ this.view.dom.classList.remove("sm-follow-links");
96
+ };
97
+ });
98
+ const followTheme = EditorView.theme({
99
+ "&.sm-follow-links .sm-link, &.sm-follow-links .sm-url": {
100
+ cursor: "pointer",
101
+ textDecoration: "underline"
102
+ },
103
+ ".sm-image-preview img": { cursor: "pointer" }
104
+ });
105
+ /** Ctrl/Cmd+Click follows links; `#anchors` jump to headings. */
106
+ function links() {
107
+ return [
108
+ clickToFollow,
109
+ followKeyHint,
110
+ followTheme
111
+ ];
112
+ }
113
+ //#endregion
7
114
  //#region packages/codemirror/src/decorations.ts
8
115
  /** The theme the editor draws with. */
9
116
  const markdownTheme = Facet.define({ combine: (values) => values[0] ?? defaultTheme });
@@ -21,7 +128,7 @@ function lookCss(look, theme) {
21
128
  css.push(`text-decoration: ${lines.join(" ") || "none"}`);
22
129
  }
23
130
  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");
131
+ if (look.borderLeft !== void 0) css.push(`box-shadow: inset ${3 / 15}em 0 0 ${look.borderLeft}`, `padding-left: ${12 / 15}em`);
25
132
  return css.join("; ");
26
133
  }
27
134
  const fontChecks = /* @__PURE__ */ new Map();
@@ -144,9 +251,13 @@ var MarkdownDecorations = class {
144
251
  const key = `mark:${styles.join(" ")}`;
145
252
  let deco = this.cache.get(key);
146
253
  if (!deco) {
254
+ const linked = styles.includes("link") || styles.includes("url");
147
255
  deco = Decoration.mark({
148
256
  class: styles.map((s) => `sm-${s}`).join(" "),
149
- attributes: { style: lookCss(resolveLook([], styles, this.theme), this.theme) }
257
+ attributes: {
258
+ style: lookCss(resolveLook([], styles, this.theme), this.theme),
259
+ ...linked ? { title: `${followKeyName}+Click to open` } : {}
260
+ }
150
261
  });
151
262
  this.cache.set(key, deco);
152
263
  }
@@ -178,8 +289,26 @@ function loadMath() {
178
289
  function loadedMath() {
179
290
  return loaded;
180
291
  }
181
- /** Gap between images on one line (CSS gap). */
182
- const IMAGE_GAP = 8;
292
+ //#endregion
293
+ //#region packages/codemirror/src/preview-heights.ts
294
+ /**
295
+ * Preview sizes in em of the editor's font: at 15px, 4px above and 8px below a preview, images
296
+ * at most 320px tall and 8px apart. The same numbers as @simple-mark/canvas, so a canvas and the
297
+ * editor agree at any font size (a canvas text may be 48 units tall).
298
+ */
299
+ const PREVIEW_EM = {
300
+ top: 4 / 15,
301
+ bottom: 8 / 15,
302
+ imageMaxHeight: 320 / 15,
303
+ imageGap: 8 / 15,
304
+ radius: 4 / 15
305
+ };
306
+ /** Vertical padding of a preview in pixels, at the editor's font size. */
307
+ function previewPadding() {
308
+ return (PREVIEW_EM.top + PREVIEW_EM.bottom) * layout.fontSize;
309
+ }
310
+ const imageMaxHeight = () => PREVIEW_EM.imageMaxHeight * layout.fontSize;
311
+ const imageGap = () => PREVIEW_EM.imageGap * layout.fontSize;
183
312
  /** Editor facts the estimates depend on, measured by `previewLayout`. */
184
313
  const layout = {
185
314
  contentWidth: 700,
@@ -264,7 +393,7 @@ function whenImageSized(src, then) {
264
393
  /** Gives an <img> its final layout size before its pixels arrive (CSS caps it at the width). */
265
394
  function reserveImageBox(img, size) {
266
395
  img.style.aspectRatio = `${size.width} / ${size.height}`;
267
- img.style.width = `${Math.min(size.width, 320 * size.width / size.height)}px`;
396
+ img.style.width = `${Math.min(size.width, imageMaxHeight() * size.width / size.height)}px`;
268
397
  }
269
398
  function probeImage(src) {
270
399
  if (probing.has(src) || imageSizes.has(src) || typeof Image === "undefined") return;
@@ -273,7 +402,7 @@ function probeImage(src) {
273
402
  watchImage(img, src);
274
403
  img.src = src;
275
404
  }
276
- /** Displayed size of one image: fits the width, at most IMAGE_MAX_HEIGHT tall, never enlarged. */
405
+ /** Displayed size of one image: fits the width, at most `imageMaxHeight` tall, never enlarged. */
277
406
  function imageBox(src) {
278
407
  const errorLine = {
279
408
  width: layout.contentWidth,
@@ -285,11 +414,11 @@ function imageBox(src) {
285
414
  probeImage(src);
286
415
  return {
287
416
  width: layout.contentWidth,
288
- height: 160
417
+ height: imageMaxHeight() / 2
289
418
  };
290
419
  }
291
420
  if (size === null) return errorLine;
292
- const scale = Math.min(1, layout.contentWidth / size.width, 320 / size.height);
421
+ const scale = Math.min(1, layout.contentWidth / size.width, imageMaxHeight() / size.height);
293
422
  return {
294
423
  width: size.width * scale,
295
424
  height: size.height * scale
@@ -304,15 +433,15 @@ function imagesEstimate(key, srcs) {
304
433
  let rowHeight = 0;
305
434
  for (const src of srcs) {
306
435
  const box = imageBox(src);
307
- if (rowWidth > 0 && rowWidth + IMAGE_GAP + box.width > layout.contentWidth) {
308
- total += rowHeight + IMAGE_GAP;
436
+ if (rowWidth > 0 && rowWidth + imageGap() + box.width > layout.contentWidth) {
437
+ total += rowHeight + imageGap();
309
438
  rowWidth = 0;
310
439
  rowHeight = 0;
311
440
  }
312
- rowWidth += (rowWidth > 0 ? IMAGE_GAP : 0) + box.width;
441
+ rowWidth += (rowWidth > 0 ? imageGap() : 0) + box.width;
313
442
  rowHeight = Math.max(rowHeight, box.height);
314
443
  }
315
- return Math.round(total + rowHeight + 12);
444
+ return Math.round(total + rowHeight + previewPadding());
316
445
  }
317
446
  /** Renders queued formulas one at a time, so their heights are known before they are shown. */
318
447
  function renderQueuedMath() {
@@ -338,13 +467,13 @@ function mathEstimate(key, tex) {
338
467
  const result = loadedMath()?.cachedMath(tex, true);
339
468
  if (result?.svg !== void 0) {
340
469
  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;
470
+ if (height) return Math.round(parseFloat(height[1]) * layout.exPx + previewPadding());
471
+ } else if (result?.error !== void 0) return layout.lineHeight + previewPadding();
343
472
  else {
344
473
  mathQueue.add(tex);
345
474
  renderQueuedMath();
346
475
  }
347
- return 3 * layout.lineHeight + 12;
476
+ return 3 * layout.lineHeight + previewPadding();
348
477
  }
349
478
  /** Measures the editor (content width, x-height, line height) and refreshes previews on news. */
350
479
  const previewLayout = ViewPlugin.fromClass(class {
@@ -497,8 +626,6 @@ function lineHeightHints() {
497
626
  }
498
627
  //#endregion
499
628
  //#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
629
  /** Fills `dom` with rendered math now if known, otherwise when it is ready. */
503
630
  function renderInto(dom, tex, display, onDone) {
504
631
  const show = (result) => {
@@ -540,7 +667,7 @@ var MathBlockWidget = class extends WidgetType {
540
667
  const dom = document.createElement("div");
541
668
  dom.className = "sm-math-preview";
542
669
  dom.dataset["key"] = this.key;
543
- dom.style.minHeight = `${this.estimate - 12}px`;
670
+ dom.style.minHeight = `${this.estimate - previewPadding()}px`;
544
671
  renderInto(dom, this.tex, true, () => {
545
672
  dom.style.minHeight = "";
546
673
  view.requestMeasure();
@@ -557,15 +684,17 @@ function imageRef(image, resolve) {
557
684
  alt: image.alt,
558
685
  problem: `No definition for ${image.raw}`
559
686
  };
560
- const src = resolve(image.src);
561
- return src === null ? {
687
+ const [src, ...fallbacks] = resolvedUrls(resolve, image.src);
688
+ if (src === void 0) return {
562
689
  raw: image.raw,
563
- src,
690
+ src: null,
564
691
  alt: image.alt,
565
692
  problem: `Can't load ${image.raw}`
566
- } : {
693
+ };
694
+ return {
567
695
  raw: image.raw,
568
696
  src,
697
+ ...fallbacks.length ? { fallbacks } : {},
569
698
  alt: image.alt
570
699
  };
571
700
  }
@@ -626,14 +755,22 @@ var ImageWidget = class extends WidgetType {
626
755
  img.decoding = "async";
627
756
  img.src = image.src;
628
757
  img.alt = image.alt;
629
- img.title = image.raw;
758
+ img.title = `${image.raw} (click to open)`;
759
+ img.addEventListener("click", () => followLink(view, image.raw, true));
630
760
  img.onload = settled;
761
+ const fallbacks = [...image.fallbacks ?? []];
631
762
  img.onerror = () => {
763
+ const next = fallbacks.shift();
764
+ if (next !== void 0) {
765
+ watchImage(img, next);
766
+ img.src = next;
767
+ return;
768
+ }
632
769
  img.replaceWith(problem(`Image not found: ${image.raw}`));
633
770
  settled();
634
771
  };
635
772
  }
636
- if (unsized > 0) dom.style.minHeight = `${this.estimate - 12}px`;
773
+ if (unsized > 0) dom.style.minHeight = `${this.estimate - previewPadding()}px`;
637
774
  if (waiting === 0) rememberHeight(view, this.key, dom);
638
775
  return dom;
639
776
  }
@@ -704,17 +841,20 @@ const hoverMathTooltip = hoverTooltip((view, pos) => {
704
841
  const tooltip = mathTooltip(view.state, pos);
705
842
  return tooltip && cursor?.pos === tooltip.pos ? null : tooltip;
706
843
  });
844
+ const em = (value) => `${value}em`;
845
+ const previewSpacing = `${em(PREVIEW_EM.top)} 0 ${em(PREVIEW_EM.bottom)}`;
846
+ /** Sizes in em (see PREVIEW_EM), so previews scale with the text like on a canvas. */
707
847
  const previewTheme = EditorView.theme({
708
848
  ".sm-image-preview": {
709
849
  display: "flex",
710
850
  flexWrap: "wrap",
711
- gap: "8px",
712
- padding: "4px 0 8px"
851
+ gap: em(PREVIEW_EM.imageGap),
852
+ padding: previewSpacing
713
853
  },
714
854
  ".sm-image-preview img": {
715
855
  maxWidth: "100%",
716
- maxHeight: "320px",
717
- borderRadius: "4px"
856
+ maxHeight: em(PREVIEW_EM.imageMaxHeight),
857
+ borderRadius: em(PREVIEW_EM.radius)
718
858
  },
719
859
  ".sm-image-error, .sm-math-error": {
720
860
  color: "#cf222e",
@@ -722,7 +862,7 @@ const previewTheme = EditorView.theme({
722
862
  },
723
863
  ".sm-math-preview": {
724
864
  textAlign: "center",
725
- padding: "4px 0 8px",
865
+ padding: previewSpacing,
726
866
  overflowX: "auto",
727
867
  overflowY: "hidden"
728
868
  },
@@ -965,6 +1105,7 @@ function simpleMark(theme = defaultTheme) {
965
1105
  markdownTheme.of(theme),
966
1106
  markdownDecorations,
967
1107
  previews(),
1108
+ links(),
968
1109
  EditorView.theme({
969
1110
  "&": { color: theme.color },
970
1111
  ".cm-scroller": {
@@ -976,7 +1117,7 @@ function simpleMark(theme = defaultTheme) {
976
1117
  }
977
1118
  /** A Markdown editor in `parent`. The text is kept exactly as given, line endings included. */
978
1119
  function createEditor(parent, options = {}) {
979
- const { doc = "", readOnly = false, theme = defaultTheme, resolveImage, onChange, extensions = [] } = options;
1120
+ const { doc = "", readOnly = false, theme = defaultTheme, resolveImage, openLink, onChange, extensions = [] } = options;
980
1121
  const useHistory = options.history ?? true;
981
1122
  return new EditorView({
982
1123
  parent,
@@ -989,6 +1130,7 @@ function createEditor(parent, options = {}) {
989
1130
  EditorView.lineWrapping,
990
1131
  simpleMark(theme),
991
1132
  resolveImage ? imageResolver.of(resolveImage) : [],
1133
+ openLink ? linkOpener.of(openLink) : [],
992
1134
  EditorState.readOnly.of(readOnly),
993
1135
  EditorView.editable.of(!readOnly),
994
1136
  onChange ? EditorView.updateListener.of((update) => update.docChanged && onChange(update.state.sliceDoc(), update)) : [],
@@ -998,4 +1140,4 @@ function createEditor(parent, options = {}) {
998
1140
  });
999
1141
  }
1000
1142
  //#endregion
1001
- export { createEditor, imageResolver, insertRowBelow, keepLineSeparator, lookCss, markdownDecorations, markdownTheme, nextCell, previews, previousCell, simpleMark, tableEditing };
1143
+ export { createEditor, followLink, imageResolver, insertRowBelow, keepLineSeparator, linkOpener, links, lookCss, markdownDecorations, markdownTheme, nextCell, previews, previousCell, simpleMark, tableEditing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-mark/codemirror",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "CodeMirror 6 editor that styles Markdown without hiding its syntax.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "@codemirror/state": "^6.7.6",
29
29
  "@codemirror/view": "^6.43.15",
30
30
  "@lezer/common": "^1.5.3",
31
- "@simple-mark/math": "^0.2.0",
32
- "@simple-mark/model": "^0.2.0"
31
+ "@simple-mark/math": "^0.3.1",
32
+ "@simple-mark/model": "^0.3.1"
33
33
  }
34
34
  }