@simple-mark/codemirror 0.3.3 → 0.3.5

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 (3) hide show
  1. package/index.d.ts +18 -5
  2. package/index.js +112 -25
  3. package/package.json +3 -3
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EditorState, Extension, Facet, StateCommand } from "@codemirror/state";
2
2
  import { DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
3
- import { LineLook, Theme } from "@simple-mark/model";
3
+ import { ResolvedLineLook, Theme } from "@simple-mark/model";
4
4
  //#region packages/codemirror/src/links.d.ts
5
5
  /** What to open: a web address, or a path as written (an image, another document, …). */
6
6
  interface LinkTarget {
@@ -22,11 +22,15 @@ export declare function links(): Extension;
22
22
  //#region packages/codemirror/src/image-resolver.d.ts
23
23
  /**
24
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.
25
+ * (the first that loads is shown), or null if it can't be loaded. `undefined` means the host
26
+ * doesn't know yet (its file is still loading): the editor shows that it is loading, and the
27
+ * host calls `refreshImages` once it knows.
26
28
  */
27
- type ImageResolver = (src: string) => string | readonly string[] | null;
29
+ type ImageResolver = (src: string) => string | readonly string[] | null | undefined;
28
30
  /** How image (and file) paths are resolved; by default they are used as written. */
29
31
  export declare const imageResolver: Facet<ImageResolver, ImageResolver>;
32
+ /** Resolves the images again, e.g. when files the resolver was waiting for have loaded. */
33
+ export declare function refreshImages(view: EditorView): void;
30
34
  //#endregion
31
35
  //#region packages/codemirror/src/previews.d.ts
32
36
  /** Image and math previews; the source text always stays visible. */
@@ -36,7 +40,7 @@ export declare function previews(): Extension;
36
40
  /** The theme the editor draws with. */
37
41
  export declare const markdownTheme: Facet<Theme, Theme>;
38
42
  /** A look as an inline CSS declaration list. */
39
- export declare function lookCss(look: LineLook, theme: Theme): string;
43
+ export declare function lookCss(look: ResolvedLineLook, theme: Theme): string;
40
44
  declare class MarkdownDecorations {
41
45
  decorations: DecorationSet;
42
46
  private theme;
@@ -45,6 +49,12 @@ declare class MarkdownDecorations {
45
49
  constructor(view: EditorView);
46
50
  update(update: ViewUpdate): void;
47
51
  private build;
52
+ /**
53
+ * One element behind each run of neighbouring spans with the same background (inline code
54
+ * with its backticks), so the background has rounded corners only at its ends. The spans
55
+ * themselves draw no background. Being longer, a box wraps the spans' elements.
56
+ */
57
+ private backgroundBoxes;
48
58
  /** Draws runs of wide characters exactly two columns each, so table pipes line up. */
49
59
  private wideRuns;
50
60
  private lineDecoration;
@@ -84,7 +94,10 @@ export interface EditorOptions {
84
94
  theme?: Theme;
85
95
  /** Line ending to read and write ("\n", "\r\n"); by default the one `doc` uses. */
86
96
  lineSeparator?: string;
87
- /** Turns an image path from the document into a URL to load (default: as written). */
97
+ /**
98
+ * Turns an image path from the document into a URL to load (default: as written). Return
99
+ * undefined while the file is still loading, and call `refreshImages(view)` when it has.
100
+ */
88
101
  resolveImage?: ImageResolver;
89
102
  /**
90
103
  * Opens a followed link (Ctrl/Cmd+Click) or a clicked image preview. Default: web addresses
package/index.js CHANGED
@@ -2,21 +2,27 @@ 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, headingAnchors, linkAt, linkKind, markdownExtensions, markdownStyles, mathSource, parseTable, previewBlocks, renderTable, resolveLineLook, resolveLook, rowLine } from "@simple-mark/model";
5
+ import { STROKES, 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
7
  //#region packages/codemirror/src/image-resolver.ts
8
- /** The URLs a resolver gives for `src`, in order. */
8
+ /** The URLs a resolver gives for `src`, in order; undefined while the host is still loading. */
9
9
  function resolvedUrls(resolve, src) {
10
10
  const result = resolve(src);
11
- return result === null ? [] : typeof result === "string" ? [result] : [...result];
11
+ return result === void 0 ? void 0 : result === null ? [] : typeof result === "string" ? [result] : [...result];
12
12
  }
13
13
  /** How image (and file) paths are resolved; by default they are used as written. */
14
14
  const imageResolver = Facet.define({ combine: (values) => values[0] ?? ((src) => src) });
15
+ /** Asks the editor to resolve its images again. */
16
+ const imagesChanged = StateEffect.define();
17
+ /** Resolves the images again, e.g. when files the resolver was waiting for have loaded. */
18
+ function refreshImages(view) {
19
+ view.dispatch({ effects: imagesChanged.of(null) });
20
+ }
15
21
  //#endregion
16
22
  //#region packages/codemirror/src/links.ts
17
23
  /** In a browser: web addresses and paths (resolved like images) open in a new tab. */
18
24
  const openInBrowser = (target, view) => {
19
- const url = target.kind === "web" ? target.url : resolvedUrls(view.state.facet(imageResolver), target.url)[0];
25
+ const url = target.kind === "web" ? target.url : resolvedUrls(view.state.facet(imageResolver), target.url)?.[0];
20
26
  if (url) window.open(url, "_blank", "noopener,noreferrer");
21
27
  };
22
28
  /** How links are opened. */
@@ -123,12 +129,26 @@ function lookCss(look, theme) {
123
129
  if (look.monospace !== void 0) css.push(`font-family: ${look.monospace ? theme.monospaceFamily : theme.fontFamily}`);
124
130
  if (look.color !== void 0) css.push(`color: ${look.color}`);
125
131
  if (look.background !== void 0) css.push(`background-color: ${look.background}`);
132
+ if (look.radius !== void 0) css.push(`border-radius: ${look.radius}em`);
126
133
  if (look.strike !== void 0 || look.underline !== void 0) {
127
134
  const lines = [look.strike && "line-through", look.underline && "underline"].filter(Boolean);
128
135
  css.push(`text-decoration: ${lines.join(" ") || "none"}`);
129
136
  }
137
+ const em = (size) => `${size / (look.fontScale ?? 1)}em`;
130
138
  if (look.lineBackground !== void 0) css.push(`background-color: ${look.lineBackground}`);
131
- if (look.borderLeft !== void 0) css.push(`box-shadow: inset ${3 / 15}em 0 0 ${look.borderLeft}`, `padding-left: ${12 / 15}em`);
139
+ if (look.radiusTop || look.radiusBottom) {
140
+ const [top, bottom] = [em(look.radiusTop ?? 0), em(look.radiusBottom ?? 0)];
141
+ css.push(`border-radius: ${top} ${top} ${bottom} ${bottom}`);
142
+ }
143
+ const shadows = [];
144
+ if (look.borderLeft !== void 0) shadows.push(`inset ${em(STROKES.bar)} 0 0 ${look.borderLeft}`);
145
+ if (look.borderBottom !== void 0) shadows.push(`inset 0 -${em(STROKES.border)} 0 ${look.borderBottom}`);
146
+ if (shadows.length) css.push(`box-shadow: ${shadows.join(", ")}`);
147
+ if (look.rule !== void 0) {
148
+ const half = em(STROKES.rule / 2);
149
+ css.push(`background-image: linear-gradient(transparent calc(50% - ${half}), ${look.rule} 0, ${look.rule} calc(50% + ${half}), transparent 0)`);
150
+ }
151
+ if (look.indent !== void 0) css.push(`padding-left: ${em(look.indent)}`);
132
152
  return css.join("; ");
133
153
  }
134
154
  const fontChecks = /* @__PURE__ */ new Map();
@@ -180,32 +200,86 @@ var MarkdownDecorations = class {
180
200
  const { doc } = view.state;
181
201
  const { from, to } = view.viewport;
182
202
  const { blocks, spans } = markdownStyles(syntaxTree(view.state), from, to);
203
+ /** Block styles of each line, and the styles whose block starts or ends there. */
183
204
  const lines = /* @__PURE__ */ new Map();
184
205
  for (const block of blocks) {
185
- const start = Math.max(block.from, from);
186
- let end = Math.min(block.to, to);
187
- if (end > start && doc.lineAt(end).from === end) end--;
188
- for (let pos = start; pos <= end;) {
206
+ let blockEnd = block.to;
207
+ if (blockEnd > block.from && doc.lineAt(blockEnd).from === blockEnd) blockEnd--;
208
+ const firstLine = doc.lineAt(block.from).from;
209
+ const lastLine = doc.lineAt(blockEnd).from;
210
+ for (let pos = Math.max(block.from, from), end = Math.min(blockEnd, to); pos <= end;) {
189
211
  const line = doc.lineAt(pos);
190
- let styles = lines.get(line.from);
191
- if (!styles) lines.set(line.from, styles = /* @__PURE__ */ new Set());
192
- styles.add(block.style);
212
+ let entry = lines.get(line.from);
213
+ if (!entry) lines.set(line.from, entry = {
214
+ styles: /* @__PURE__ */ new Set(),
215
+ first: /* @__PURE__ */ new Set(),
216
+ last: /* @__PURE__ */ new Set()
217
+ });
218
+ entry.styles.add(block.style);
219
+ if (line.from === firstLine) entry.first.add(block.style);
220
+ if (line.from === lastLine) entry.last.add(block.style);
193
221
  pos = line.to + 1;
194
222
  }
195
223
  }
196
224
  const ranges = [];
197
- for (const [lineFrom, styles] of lines) ranges.push(this.lineDecoration([...styles]).range(lineFrom));
225
+ for (const [lineFrom, entry] of lines) ranges.push(this.lineDecoration(entry).range(lineFrom));
226
+ for (const box of this.backgroundBoxes(spans)) ranges.push(box);
198
227
  for (const span of spans) ranges.push(this.markDecoration(span.styles).range(span.from, span.to));
199
- for (const [lineFrom, styles] of lines) {
228
+ for (const [lineFrom, { styles }] of lines) {
200
229
  const line = doc.lineAt(lineFrom);
201
230
  if (line.length && [...styles].some((s) => /^h[1-6]$/.test(s))) ranges.push(headingText.range(line.from, line.to));
202
231
  }
203
232
  if (this.fixWide) {
204
233
  const cursorLines = new Set(view.state.selection.ranges.map((r) => doc.lineAt(r.head).from));
205
- for (const [lineFrom, styles] of lines) if (styles.has("table") && !cursorLines.has(lineFrom)) this.wideRuns(doc.lineAt(lineFrom), ranges);
234
+ for (const [lineFrom, { styles }] of lines) if (styles.has("table") && !cursorLines.has(lineFrom)) this.wideRuns(doc.lineAt(lineFrom), ranges);
206
235
  }
207
236
  return Decoration.set(ranges, true);
208
237
  }
238
+ /**
239
+ * One element behind each run of neighbouring spans with the same background (inline code
240
+ * with its backticks), so the background has rounded corners only at its ends. The spans
241
+ * themselves draw no background. Being longer, a box wraps the spans' elements.
242
+ */
243
+ backgroundBoxes(spans) {
244
+ const boxes = [];
245
+ let open = null;
246
+ const flush = () => {
247
+ if (!open) return;
248
+ const { background, radius, monospace } = open.look;
249
+ const look = {
250
+ background,
251
+ ...radius !== void 0 ? { radius } : {},
252
+ ...monospace !== void 0 ? { monospace } : {}
253
+ };
254
+ const key = `box:${JSON.stringify(look)}`;
255
+ let deco = this.cache.get(key);
256
+ if (!deco) {
257
+ const css = `${lookCss(look, this.theme)}; line-height: 0; -webkit-box-decoration-break: clone; box-decoration-break: clone`;
258
+ deco = Decoration.mark({
259
+ class: "sm-box",
260
+ attributes: { style: css }
261
+ });
262
+ this.cache.set(key, deco);
263
+ }
264
+ boxes.push(deco.range(open.from, open.to));
265
+ open = null;
266
+ };
267
+ for (const span of spans) {
268
+ const look = resolveLook([], span.styles, this.theme);
269
+ if (look.background === void 0) flush();
270
+ else if (open && open.to === span.from && open.look.background === look.background) open.to = span.to;
271
+ else {
272
+ flush();
273
+ open = {
274
+ from: span.from,
275
+ to: span.to,
276
+ look
277
+ };
278
+ }
279
+ }
280
+ flush();
281
+ return boxes;
282
+ }
209
283
  /** Draws runs of wide characters exactly two columns each, so table pipes line up. */
210
284
  wideRuns(line, ranges) {
211
285
  let start = -1;
@@ -235,13 +309,14 @@ var MarkdownDecorations = class {
235
309
  }
236
310
  flush(i);
237
311
  }
238
- lineDecoration(styles) {
239
- const key = `line:${styles.sort().join(" ")}`;
312
+ lineDecoration(entry) {
313
+ const styles = [...entry.styles].sort();
314
+ const key = `line:${styles.join(" ")}|${[...entry.first].sort().join(" ")}|${[...entry.last].sort().join(" ")}`;
240
315
  let deco = this.cache.get(key);
241
316
  if (!deco) {
242
317
  deco = Decoration.line({
243
318
  class: styles.map((s) => `sm-line-${s}`).join(" "),
244
- attributes: { style: lookCss(resolveLineLook(styles, this.theme), this.theme) }
319
+ attributes: { style: lookCss(resolveLineLook(styles, this.theme, entry), this.theme) }
245
320
  });
246
321
  this.cache.set(key, deco);
247
322
  }
@@ -252,7 +327,7 @@ var MarkdownDecorations = class {
252
327
  let deco = this.cache.get(key);
253
328
  if (!deco) {
254
329
  const linked = styles.includes("link") || styles.includes("url");
255
- const look = resolveLook([], styles, this.theme);
330
+ const { background: _background, radius: _radius, ...look } = resolveLook([], styles, this.theme);
256
331
  const keepLine = look.monospace !== void 0 || look.fontScale !== void 0;
257
332
  deco = Decoration.mark({
258
333
  class: styles.map((s) => `sm-${s}`).join(" "),
@@ -687,7 +762,15 @@ function imageRef(image, resolve) {
687
762
  alt: image.alt,
688
763
  problem: `No definition for ${image.raw}`
689
764
  };
690
- const [src, ...fallbacks] = resolvedUrls(resolve, image.src);
765
+ const urls = resolvedUrls(resolve, image.src);
766
+ if (urls === void 0) return {
767
+ raw: image.raw,
768
+ src: null,
769
+ alt: image.alt,
770
+ problem: `Loading ${image.raw}…`,
771
+ loading: true
772
+ };
773
+ const [src, ...fallbacks] = urls;
691
774
  if (src === void 0) return {
692
775
  raw: image.raw,
693
776
  src: null,
@@ -732,14 +815,14 @@ var ImageWidget = class extends WidgetType {
732
815
  if (--waiting === 0) rememberHeight(view, this.key, dom);
733
816
  };
734
817
  for (const image of this.images) {
735
- const problem = (text) => {
818
+ const problem = (text, className = "sm-image-error") => {
736
819
  const span = document.createElement("span");
737
- span.className = "sm-image-error";
820
+ span.className = className;
738
821
  span.textContent = text;
739
822
  return span;
740
823
  };
741
824
  if (image.src === null) {
742
- dom.appendChild(problem(image.problem ?? `Can't load ${image.raw}`));
825
+ dom.appendChild(problem(image.problem ?? `Can't load ${image.raw}`, image.loading ? "sm-image-loading" : "sm-image-error"));
743
826
  continue;
744
827
  }
745
828
  waiting++;
@@ -801,7 +884,7 @@ function blockPreviews(state) {
801
884
  const blockPreviewField = StateField.define({
802
885
  create: blockPreviews,
803
886
  update(previews, tr) {
804
- 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;
887
+ return tr.docChanged || syntaxTree(tr.state) !== syntaxTree(tr.startState) || tr.state.facet(imageResolver) !== tr.startState.facet(imageResolver) || tr.effects.some((effect) => effect.is(refreshEstimates) || effect.is(imagesChanged)) ? blockPreviews(tr.state) : previews;
805
888
  },
806
889
  provide: (field) => EditorView.decorations.from(field)
807
890
  });
@@ -863,6 +946,10 @@ const previewTheme = EditorView.theme({
863
946
  color: "#cf222e",
864
947
  fontSize: "0.9em"
865
948
  },
949
+ ".sm-image-loading": {
950
+ color: "#818b98",
951
+ fontSize: "0.9em"
952
+ },
866
953
  ".sm-math-preview": {
867
954
  textAlign: "center",
868
955
  padding: previewSpacing,
@@ -1143,4 +1230,4 @@ function createEditor(parent, options = {}) {
1143
1230
  });
1144
1231
  }
1145
1232
  //#endregion
1146
- export { createEditor, followLink, imageResolver, insertRowBelow, keepLineSeparator, linkOpener, links, lookCss, markdownDecorations, markdownTheme, nextCell, previews, previousCell, simpleMark, tableEditing };
1233
+ export { createEditor, followLink, imageResolver, insertRowBelow, keepLineSeparator, linkOpener, links, lookCss, markdownDecorations, markdownTheme, nextCell, previews, previousCell, refreshImages, simpleMark, tableEditing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-mark/codemirror",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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.3.3",
32
- "@simple-mark/model": "^0.3.3"
31
+ "@simple-mark/math": "^0.3.5",
32
+ "@simple-mark/model": "^0.3.5"
33
33
  }
34
34
  }