@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.
- package/README.md +5 -0
- package/index.d.ts +33 -5
- package/index.js +174 -32
- 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/
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
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 ``). 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 ``). 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
|
|
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: {
|
|
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
|
-
|
|
182
|
-
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
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 +
|
|
308
|
-
total += rowHeight +
|
|
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 ?
|
|
441
|
+
rowWidth += (rowWidth > 0 ? imageGap() : 0) + box.width;
|
|
313
442
|
rowHeight = Math.max(rowHeight, box.height);
|
|
314
443
|
}
|
|
315
|
-
return Math.round(total + rowHeight +
|
|
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 +
|
|
342
|
-
} else if (result?.error !== void 0) return layout.lineHeight +
|
|
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 +
|
|
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 -
|
|
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
|
|
561
|
-
|
|
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 -
|
|
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:
|
|
712
|
-
padding:
|
|
851
|
+
gap: em(PREVIEW_EM.imageGap),
|
|
852
|
+
padding: previewSpacing
|
|
713
853
|
},
|
|
714
854
|
".sm-image-preview img": {
|
|
715
855
|
maxWidth: "100%",
|
|
716
|
-
maxHeight:
|
|
717
|
-
borderRadius:
|
|
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:
|
|
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.
|
|
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.
|
|
32
|
-
"@simple-mark/model": "^0.
|
|
31
|
+
"@simple-mark/math": "^0.3.1",
|
|
32
|
+
"@simple-mark/model": "^0.3.1"
|
|
33
33
|
}
|
|
34
34
|
}
|