@memorysmithapp/markdown-renderer 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/NoteBody.d.ts +4 -1
- package/dist/NoteBody.js +1 -1
- package/dist/edit/NoteEditor.js +2 -1
- package/dist/edit/heights.d.ts +34 -0
- package/dist/edit/heights.js +101 -0
- package/dist/edit/live.d.ts +7 -2
- package/dist/edit/live.js +150 -27
- package/dist/host.d.ts +13 -1
- package/dist/strings.d.ts +2 -0
- package/dist/strings.js +1 -0
- package/dist/theme.d.ts +4 -1
- package/package.json +2 -2
- package/structure.css +13 -0
- package/theme-default.css +14 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ import 'katex/dist/katex.min.css';
|
|
|
20
20
|
resolve={(name) => addressOfTheOneNoteNamed(name)} // or null: the link is pending
|
|
21
21
|
keeps={(name) => notebookKeepsAFile(name)} // only an embed reaches a file
|
|
22
22
|
fileOf={(name) => describe(name)} // { mimeType, bytes, url, description }: drawn by its type, as MemorySmith does
|
|
23
|
-
renderLink={({ href, className, children }) => <
|
|
23
|
+
renderLink={({ href, className, children }) => <a href={href} className={className} onClick={follow(href)}>{children}</a>} // follow: your router's navigate, captured where it is
|
|
24
24
|
loadNote={(name) => fetchTheBodyOf(name)} // for an embedded note; the package never fetches
|
|
25
25
|
writable
|
|
26
26
|
onToggleTask={(ordinal) => save(toggleTaskAt(latest(), ordinal))} // a promise locks the item under "Updating…" until it settles
|
|
@@ -29,7 +29,7 @@ import 'katex/dist/katex.min.css';
|
|
|
29
29
|
/>;
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Every prop is optional, and every default is the safe one: with no `resolve` every link is pending, with no `keeps` no embed reaches a file. A file `fileOf` describes is drawn by its media type, never its name: an image, an audio or a video player, and anything else a card with its type, size, description, *Open* where a browser shows it and *Download*; give `renderAttachment` instead to draw files yourself. The stylesheet styles nothing outside `.markdown`. The faces of the default theme are Inter and Space Grotesk, which the page loads.
|
|
32
|
+
Every prop is optional, and every default is the safe one: with no `resolve` every link is pending, with no `keeps` no embed reaches a file. A file `fileOf` describes is drawn by its media type, never its name: an image, an audio or a video player, and anything else a card with its type, size, description, *Open* where a browser shows it and *Download*; give `renderAttachment` instead to draw files yourself. In edit mode, `renderLink`, `renderAttachment` and a theme's slots may be drawn in a root of their own, outside your React tree, so what they return must not read your context: a router's `<Link>` throws there, and the block shows its Markdown under a notice instead. The stylesheet styles nothing outside `.markdown`. The faces of the default theme are Inter and Space Grotesk, which the page loads.
|
|
33
33
|
|
|
34
34
|
## Themes
|
|
35
35
|
|
|
@@ -86,7 +86,7 @@ A note the host makes `editable` carries a button at its top right that switches
|
|
|
86
86
|
suggest={(typed) => namesOfNotesLike(typed)} // what [[ offers
|
|
87
87
|
attach={(file, { name, description }, progress) => keepInNotebook(file, name, description, progress)} // the name it is kept under
|
|
88
88
|
files={() => filesOfTheNotebook()} // what the attach dialog lists: { name, mimeType, bytes, url, description }
|
|
89
|
-
modeButton="
|
|
89
|
+
modeButton="label" // the icon and its word; "icon", the default, the icon alone
|
|
90
90
|
/>
|
|
91
91
|
```
|
|
92
92
|
|
package/dist/NoteBody.d.ts
CHANGED
|
@@ -25,7 +25,10 @@ export interface NoteBodyProps extends NoteHost {
|
|
|
25
25
|
* a dialog with the host's words and its actions (#86).
|
|
26
26
|
*/
|
|
27
27
|
readonly onChange?: (markdown: string) => unknown;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Whether the switch shows its icon alone or its icon and its word. Defaults to `icon`, like Show
|
|
30
|
+
* Markdown and Copy Markdown beside it, its word then its name and its title (#92).
|
|
31
|
+
*/
|
|
29
32
|
readonly modeButton?: 'icon' | 'label';
|
|
30
33
|
/**
|
|
31
34
|
* Whether the note offers Show Markdown and Copy Markdown at its top right, editable or not:
|
package/dist/NoteBody.js
CHANGED
|
@@ -34,7 +34,7 @@ function ModeSwitch({ mode, display, toggle }) {
|
|
|
34
34
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
35
35
|
* every piece of notebook data through the props of `NoteHost`.
|
|
36
36
|
*/
|
|
37
|
-
export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeChange, onChange, modeButton = '
|
|
37
|
+
export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeChange, onChange, modeButton = 'icon', actions = true, ...rest }) {
|
|
38
38
|
const [own, setOwn] = useState(defaultMode);
|
|
39
39
|
// The author asked to go back to reading, and the editor is finishing: a save, the toolbar folding.
|
|
40
40
|
const [leaving, setLeaving] = useState(false);
|
package/dist/edit/NoteEditor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { rootStyle } from '../theme.js';
|
|
|
19
19
|
import { ICONS } from './blocks.js';
|
|
20
20
|
import { BLOCKS, command, headingLevel, insertBlock, insertFootnote, insertLink, insertWikilink, setHeading, toggleComment, toggleList, toggleMark, toggleQuote, } from './commands.js';
|
|
21
21
|
import { analysisField, editorEnvironment, livePage } from './live.js';
|
|
22
|
+
import { measureMetrics } from './heights.js';
|
|
22
23
|
import { NoteActions } from '../NoteActions.js';
|
|
23
24
|
import { AttachDialog, webImage } from './AttachDialog.js';
|
|
24
25
|
import { ContextMenu } from './ContextMenu.js';
|
|
@@ -358,7 +359,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
358
359
|
markdown(),
|
|
359
360
|
EditorView.lineWrapping,
|
|
360
361
|
EditorView.contentAttributes.of({ 'aria-multiline': 'true', spellcheck: 'true' }),
|
|
361
|
-
livePage(),
|
|
362
|
+
livePage(measureMetrics(parent.current)),
|
|
362
363
|
autocompletion({ override: [wikilinkCompletion(() => latest.current.host)], icons: false }),
|
|
363
364
|
shortcuts(() => created),
|
|
364
365
|
keymap.of([...markdownKeymap, ...historyKeymap, indentWithTab, ...defaultKeymap]),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How tall a drawn block will be before it is drawn (#91).
|
|
3
|
+
*
|
|
4
|
+
* CodeMirror lays out the blocks it has not drawn yet by the height their widget estimates, and
|
|
5
|
+
* reads that estimate once, when the block's decoration is made; a block drawn taller than its
|
|
6
|
+
* estimate grows the document under the scrollbar as the author scrolls to it. Each block
|
|
7
|
+
* estimates its height from what it draws — its lines wrapped at the editor's width, its rows, its
|
|
8
|
+
* properties — and a block drawn once at a width is remembered at its measured height, so the same
|
|
9
|
+
* block made again, moved by an edit above it or in the next edit of the note, starts right.
|
|
10
|
+
*/
|
|
11
|
+
/** What an estimate reads of the editor: its text column, its line height and its characters. */
|
|
12
|
+
export interface BlockMetrics {
|
|
13
|
+
readonly width: number;
|
|
14
|
+
readonly lineHeight: number;
|
|
15
|
+
readonly charWidth: number;
|
|
16
|
+
}
|
|
17
|
+
/** What an estimate assumes before the editor has been measured, as in a test with no layout. */
|
|
18
|
+
export declare const DEFAULT_METRICS: BlockMetrics;
|
|
19
|
+
/**
|
|
20
|
+
* The metrics of a text column laid out in `parent`, read before the editor exists there, so the
|
|
21
|
+
* first estimates are made at the width the note is drawn at. Falls back to `DEFAULT_METRICS` where
|
|
22
|
+
* nothing is laid out.
|
|
23
|
+
*/
|
|
24
|
+
export declare function measureMetrics(parent: HTMLElement): BlockMetrics;
|
|
25
|
+
/** Metrics as measured, any that could not be read replaced by the default. */
|
|
26
|
+
export declare function withinReason(metrics: BlockMetrics): BlockMetrics;
|
|
27
|
+
/** What a block is, for its estimate. */
|
|
28
|
+
export type BlockKind = 'frontmatter' | 'callout' | 'table' | 'embed' | 'diagram' | 'preview' | 'add-properties';
|
|
29
|
+
/** The height a block of this kind and these lines is drawn at, near enough to lay out the page. */
|
|
30
|
+
export declare function estimateHeight(kind: BlockKind, source: string, metrics: BlockMetrics): number;
|
|
31
|
+
/** The height a block of this content was drawn at at this width, if it was. */
|
|
32
|
+
export declare function measuredHeight(identity: string, metrics: BlockMetrics): number | undefined;
|
|
33
|
+
/** Remembers the height a block was drawn at. */
|
|
34
|
+
export declare function rememberHeight(identity: string, metrics: BlockMetrics, height: number): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How tall a drawn block will be before it is drawn (#91).
|
|
3
|
+
*
|
|
4
|
+
* CodeMirror lays out the blocks it has not drawn yet by the height their widget estimates, and
|
|
5
|
+
* reads that estimate once, when the block's decoration is made; a block drawn taller than its
|
|
6
|
+
* estimate grows the document under the scrollbar as the author scrolls to it. Each block
|
|
7
|
+
* estimates its height from what it draws — its lines wrapped at the editor's width, its rows, its
|
|
8
|
+
* properties — and a block drawn once at a width is remembered at its measured height, so the same
|
|
9
|
+
* block made again, moved by an edit above it or in the next edit of the note, starts right.
|
|
10
|
+
*/
|
|
11
|
+
/** What an estimate assumes before the editor has been measured, as in a test with no layout. */
|
|
12
|
+
export const DEFAULT_METRICS = { width: 700, lineHeight: 24, charWidth: 8 };
|
|
13
|
+
/**
|
|
14
|
+
* The metrics of a text column laid out in `parent`, read before the editor exists there, so the
|
|
15
|
+
* first estimates are made at the width the note is drawn at. Falls back to `DEFAULT_METRICS` where
|
|
16
|
+
* nothing is laid out.
|
|
17
|
+
*/
|
|
18
|
+
export function measureMetrics(parent) {
|
|
19
|
+
const probe = document.createElement('div');
|
|
20
|
+
probe.style.cssText = 'position: absolute; visibility: hidden; white-space: pre; pointer-events: none';
|
|
21
|
+
probe.textContent = 'abcdefghijklmnopqrstuvwxyz';
|
|
22
|
+
parent.appendChild(probe);
|
|
23
|
+
const box = probe.getBoundingClientRect();
|
|
24
|
+
const width = parent.clientWidth;
|
|
25
|
+
probe.remove();
|
|
26
|
+
return withinReason({ width, lineHeight: box.height, charWidth: box.width / 26 });
|
|
27
|
+
}
|
|
28
|
+
/** Metrics as measured, any that could not be read replaced by the default. */
|
|
29
|
+
export function withinReason(metrics) {
|
|
30
|
+
const usable = (value, fallback) => (Number.isFinite(value) && value > 1 ? value : fallback);
|
|
31
|
+
return {
|
|
32
|
+
width: usable(metrics.width, DEFAULT_METRICS.width),
|
|
33
|
+
lineHeight: usable(metrics.lineHeight, DEFAULT_METRICS.lineHeight),
|
|
34
|
+
charWidth: usable(metrics.charWidth, DEFAULT_METRICS.charWidth),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The lines a text takes in a column `width` wide, each of its lines at least one. */
|
|
38
|
+
function wrapped(lines, width, metrics) {
|
|
39
|
+
const perLine = Math.max(8, Math.floor(width / metrics.charWidth));
|
|
40
|
+
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / perLine)), 0);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Below this width the default theme lays a property's value under its key, not beside it; it is
|
|
44
|
+
* the text column of its 560px breakpoint.
|
|
45
|
+
*/
|
|
46
|
+
const STACKED_PROPERTIES = 528;
|
|
47
|
+
/** The height a block of this kind and these lines is drawn at, near enough to lay out the page. */
|
|
48
|
+
export function estimateHeight(kind, source, metrics) {
|
|
49
|
+
const { lineHeight } = metrics;
|
|
50
|
+
const lines = source.split('\n');
|
|
51
|
+
switch (kind) {
|
|
52
|
+
case 'frontmatter': {
|
|
53
|
+
// The name's field, then a row per key of the top level, which a list's items share.
|
|
54
|
+
const keys = lines.filter((line) => /^[^\s#-][^:]*:/.test(line)).length;
|
|
55
|
+
const row = (metrics.width < STACKED_PROPERTIES ? 2.8 : 1.4) * lineHeight;
|
|
56
|
+
return Math.round(2.5 * lineHeight + keys * row);
|
|
57
|
+
}
|
|
58
|
+
case 'callout': {
|
|
59
|
+
// Its title and its body, as text inside its padding and its border.
|
|
60
|
+
const text = lines.map((line) => line.replace(/^(?:\s*>)+\s?/, ''));
|
|
61
|
+
return Math.round(lineHeight + wrapped(text, metrics.width - 32, metrics) * lineHeight);
|
|
62
|
+
}
|
|
63
|
+
case 'table': {
|
|
64
|
+
// A row per line but the delimiter row. The columns share the width by what they hold, so a
|
|
65
|
+
// row wraps as the text of all its cells would in the width their padding leaves.
|
|
66
|
+
const rows = lines.filter((line, index) => index !== 1 || !/^[\s|:-]+$/.test(line));
|
|
67
|
+
const cells = (row) => row.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((cell) => cell.trim());
|
|
68
|
+
const columns = Math.max(1, ...rows.map((row) => cells(row).length));
|
|
69
|
+
const width = metrics.width - 16 * columns;
|
|
70
|
+
const height = rows.reduce((sum, row) => sum + 9 + lineHeight * wrapped([cells(row).join('')], width, metrics), 0);
|
|
71
|
+
return Math.round(0.75 * lineHeight + height);
|
|
72
|
+
}
|
|
73
|
+
case 'embed':
|
|
74
|
+
// What a note or a file embeds is not known before it is drawn: a card's height.
|
|
75
|
+
return Math.round(8 * lineHeight);
|
|
76
|
+
case 'diagram':
|
|
77
|
+
return Math.round(12 * lineHeight);
|
|
78
|
+
case 'preview':
|
|
79
|
+
return Math.round(lineHeight + wrapped(lines, metrics.width, metrics) * lineHeight);
|
|
80
|
+
case 'add-properties':
|
|
81
|
+
return Math.round(1.5 * lineHeight);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** The heights blocks were drawn at, by width and content; the oldest are let go past the limit. */
|
|
85
|
+
const measured = new Map();
|
|
86
|
+
const MEASURED_LIMIT = 2000;
|
|
87
|
+
const measuredKey = (identity, metrics) => `${Math.round(metrics.width)}\u0000${identity}`;
|
|
88
|
+
/** The height a block of this content was drawn at at this width, if it was. */
|
|
89
|
+
export function measuredHeight(identity, metrics) {
|
|
90
|
+
return measured.get(measuredKey(identity, metrics));
|
|
91
|
+
}
|
|
92
|
+
/** Remembers the height a block was drawn at. */
|
|
93
|
+
export function rememberHeight(identity, metrics, height) {
|
|
94
|
+
if (!(height > 0))
|
|
95
|
+
return;
|
|
96
|
+
const key = measuredKey(identity, metrics);
|
|
97
|
+
measured.delete(key);
|
|
98
|
+
measured.set(key, height);
|
|
99
|
+
if (measured.size > MEASURED_LIMIT)
|
|
100
|
+
measured.delete(measured.keys().next().value);
|
|
101
|
+
}
|
package/dist/edit/live.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Facet, StateField, type EditorState, type Extension } from '@codemirror
|
|
|
10
10
|
import { type DecorationSet } from '@codemirror/view';
|
|
11
11
|
import { type ResolvedHost } from '../host.js';
|
|
12
12
|
import { type Analysis } from './analyze.js';
|
|
13
|
+
import { type BlockMetrics } from './heights.js';
|
|
13
14
|
/** What the drawn blocks read: the host the note is edited under. */
|
|
14
15
|
export interface EditorEnvironment {
|
|
15
16
|
readonly host: ResolvedHost;
|
|
@@ -24,5 +25,9 @@ export declare const setFocused: import("@codemirror/state").StateEffectType<boo
|
|
|
24
25
|
/** A grammar a code block names has arrived: its tokens can be coloured now (#84). */
|
|
25
26
|
export declare const grammarArrived: import("@codemirror/state").StateEffectType<string>;
|
|
26
27
|
export declare const decorationsField: StateField<DecorationSet>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The live page: the analysis, the decorations over it, and what the focus and a modifier-click do.
|
|
30
|
+
* `metrics` are the editor's text column measured before the editor is made, which the first drawn
|
|
31
|
+
* blocks estimate their height by; without them they assume `DEFAULT_METRICS` until it is measured.
|
|
32
|
+
*/
|
|
33
|
+
export declare function livePage(metrics?: BlockMetrics): Extension;
|
package/dist/edit/live.js
CHANGED
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
import { EditorSelection, Facet, Prec, StateEffect, StateField } from '@codemirror/state';
|
|
10
10
|
import { Decoration, EditorView, keymap, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
11
11
|
import katex from 'katex';
|
|
12
|
-
import { createElement } from 'react';
|
|
12
|
+
import { Component, createElement } from 'react';
|
|
13
13
|
import { createRoot } from 'react-dom/client';
|
|
14
14
|
import { followable } from '../address.js';
|
|
15
15
|
import { highlightTree, languageToLoad, loadGrammar } from '../highlight.js';
|
|
16
16
|
import { HostProvider, resolveHost } from '../host.js';
|
|
17
|
+
import { readImageAlt } from '../image-dimensions.js';
|
|
17
18
|
import { highlightLanguage } from '../highlight.js';
|
|
18
19
|
import { analyze } from './analyze.js';
|
|
20
|
+
import { DEFAULT_METRICS, estimateHeight, measuredHeight, rememberHeight, withinReason } from './heights.js';
|
|
19
21
|
import { AddProperties, CalloutBlock, PropertiesBlock, reveal, Snippet, TableBlock } from './blocks.js';
|
|
20
22
|
import { LanguageControl } from './LanguageControl.js';
|
|
21
23
|
import { toggleTaskBox } from './commands.js';
|
|
@@ -35,6 +37,42 @@ export const analysisField = StateField.define({
|
|
|
35
37
|
export const setFocused = StateEffect.define();
|
|
36
38
|
/** A grammar a code block names has arrived: its tokens can be coloured now (#84). */
|
|
37
39
|
export const grammarArrived = StateEffect.define();
|
|
40
|
+
/** The editor's metrics changed: the blocks made from now on estimate their height by them (#91). */
|
|
41
|
+
const setBlockMetrics = StateEffect.define();
|
|
42
|
+
/** The metrics of the editor's text column, which a drawn block estimates its height by (#91). */
|
|
43
|
+
const metricsField = StateField.define({
|
|
44
|
+
create: () => DEFAULT_METRICS,
|
|
45
|
+
update: (metrics, transaction) => {
|
|
46
|
+
for (const effect of transaction.effects)
|
|
47
|
+
if (effect.is(setBlockMetrics))
|
|
48
|
+
return effect.value;
|
|
49
|
+
return metrics;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
/** Reads the editor's metrics again whenever its geometry changes, and keeps them when they moved. */
|
|
53
|
+
const metricsWatcher = ViewPlugin.define((view) => {
|
|
54
|
+
let destroyed = false;
|
|
55
|
+
const check = () => view.requestMeasure({
|
|
56
|
+
read: () => withinReason({ width: view.contentDOM.clientWidth, lineHeight: view.defaultLineHeight, charWidth: view.defaultCharacterWidth }),
|
|
57
|
+
write: (next) => {
|
|
58
|
+
const now = view.state.field(metricsField);
|
|
59
|
+
const moved = Math.abs(next.width - now.width) >= 1 || Math.abs(next.lineHeight - now.lineHeight) >= 0.5 || Math.abs(next.charWidth - now.charWidth) >= 0.05;
|
|
60
|
+
// Not inside the measure, where CodeMirror takes no transaction.
|
|
61
|
+
if (moved)
|
|
62
|
+
queueMicrotask(() => destroyed || view.dispatch({ effects: setBlockMetrics.of(next) }));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
check();
|
|
66
|
+
return {
|
|
67
|
+
update: (update) => {
|
|
68
|
+
if (update.geometryChanged)
|
|
69
|
+
check();
|
|
70
|
+
},
|
|
71
|
+
destroy: () => {
|
|
72
|
+
destroyed = true;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
});
|
|
38
76
|
/** Whether the editor has the focus: a construct is shown as source only to an author who is in it. */
|
|
39
77
|
const focusField = StateField.define({
|
|
40
78
|
create: () => false,
|
|
@@ -46,17 +84,45 @@ const focusField = StateField.define({
|
|
|
46
84
|
},
|
|
47
85
|
});
|
|
48
86
|
// ── Widgets ──────────────────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* What a drawn block shows when drawing it threw — a host's `renderLink` that needs the host's own
|
|
89
|
+
* context, which a block's root, outside the host's tree, does not carry (#90): its lines as written,
|
|
90
|
+
* under a notice, so a failure never hides a part of the note. A click on it shows its source.
|
|
91
|
+
*/
|
|
92
|
+
class BlockBoundary extends Component {
|
|
93
|
+
state = { failed: false };
|
|
94
|
+
static getDerivedStateFromError() {
|
|
95
|
+
return { failed: true };
|
|
96
|
+
}
|
|
97
|
+
componentDidCatch(error, info) {
|
|
98
|
+
// React reports a caught error only in development; the host is told in every build.
|
|
99
|
+
console.error('A block of the note could not be drawn:', error, info.componentStack);
|
|
100
|
+
}
|
|
101
|
+
render() {
|
|
102
|
+
if (!this.state.failed)
|
|
103
|
+
return this.props.children;
|
|
104
|
+
const notice = this.props.host.strings.editor.blockFailed;
|
|
105
|
+
if (!this.props.source)
|
|
106
|
+
return createElement('span', { className: 'cm-ms-block-failed', title: notice });
|
|
107
|
+
return createElement('div', { className: 'cm-ms-block-failed', role: 'note' }, createElement('p', { className: 'cm-ms-block-failed-notice' }, notice), createElement('pre', null, this.props.source));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
49
110
|
/** The React roots the widgets mounted, unmounted when CodeMirror lets the widget go. */
|
|
50
111
|
const roots = new WeakMap();
|
|
51
|
-
function mount(view, element, node) {
|
|
112
|
+
function mount(view, element, node, source, measured) {
|
|
52
113
|
const { host } = view.state.facet(editorEnvironment);
|
|
53
114
|
const root = createRoot(element);
|
|
54
115
|
// Drawn on React's own schedule: CodeMirror builds its widgets inside its update, which may run
|
|
55
116
|
// inside a React commit, where React cannot be asked to render synchronously.
|
|
56
|
-
root.render(createElement(HostProvider, { value: host }, node));
|
|
117
|
+
root.render(createElement(HostProvider, { value: host }, createElement(BlockBoundary, { host, source }, node)));
|
|
57
118
|
// A block whose content arrives later — an embed, a diagram — changes its height after it is
|
|
58
119
|
// measured; the editor is told so, or the lines below it would be placed by the old height.
|
|
59
|
-
const observer = typeof ResizeObserver === 'undefined'
|
|
120
|
+
const observer = typeof ResizeObserver === 'undefined'
|
|
121
|
+
? null
|
|
122
|
+
: new ResizeObserver(() => {
|
|
123
|
+
measured(element.getBoundingClientRect().height);
|
|
124
|
+
view.requestMeasure();
|
|
125
|
+
});
|
|
60
126
|
observer?.observe(element);
|
|
61
127
|
roots.set(element, { unmount: () => root.unmount(), observer });
|
|
62
128
|
}
|
|
@@ -75,16 +141,24 @@ class BlockWidget extends WidgetType {
|
|
|
75
141
|
host;
|
|
76
142
|
draw;
|
|
77
143
|
revealsOnClick;
|
|
144
|
+
options;
|
|
78
145
|
inline;
|
|
146
|
+
source;
|
|
79
147
|
constructor(key, host, draw,
|
|
80
148
|
/** Whether a click on it shows its source; a block with controls of its own handles its clicks. */
|
|
81
|
-
revealsOnClick,
|
|
149
|
+
revealsOnClick, options = {}) {
|
|
82
150
|
super();
|
|
83
151
|
this.key = key;
|
|
84
152
|
this.host = host;
|
|
85
153
|
this.draw = draw;
|
|
86
154
|
this.revealsOnClick = revealsOnClick;
|
|
87
|
-
this.
|
|
155
|
+
this.options = options;
|
|
156
|
+
this.inline = options.inline ?? false;
|
|
157
|
+
this.source = options.source ?? '';
|
|
158
|
+
}
|
|
159
|
+
/** What it is remembered by once measured: what it draws, wherever in the note it stands. */
|
|
160
|
+
get identity() {
|
|
161
|
+
return `${this.options.kind ?? 'block'}\u0000${this.source}`;
|
|
88
162
|
}
|
|
89
163
|
eq(other) {
|
|
90
164
|
return other.key === this.key && other.host === this.host;
|
|
@@ -92,7 +166,7 @@ class BlockWidget extends WidgetType {
|
|
|
92
166
|
toDOM(view) {
|
|
93
167
|
const element = document.createElement(this.inline ? 'span' : 'div');
|
|
94
168
|
element.className = this.inline ? 'cm-ms-inline-block' : 'cm-ms-block';
|
|
95
|
-
mount(view, element, this.draw(view));
|
|
169
|
+
mount(view, element, this.draw(view), this.source, (height) => rememberHeight(this.identity, view.state.field(metricsField), height));
|
|
96
170
|
element.addEventListener('mousedown', (event) => {
|
|
97
171
|
const target = event.target;
|
|
98
172
|
if (target.closest('[data-ms-control]'))
|
|
@@ -119,8 +193,16 @@ class BlockWidget extends WidgetType {
|
|
|
119
193
|
ignoreEvent() {
|
|
120
194
|
return true;
|
|
121
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The height CodeMirror lays the block out at until it is drawn, which it reads once, when the
|
|
198
|
+
* block is made: the height it was drawn at before at this width, or its estimate (#91).
|
|
199
|
+
*/
|
|
122
200
|
get estimatedHeight() {
|
|
123
|
-
|
|
201
|
+
if (this.inline)
|
|
202
|
+
return -1;
|
|
203
|
+
const metrics = this.options.metrics ?? DEFAULT_METRICS;
|
|
204
|
+
const kind = this.options.kind;
|
|
205
|
+
return measuredHeight(this.identity, metrics) ?? (kind ? estimateHeight(kind, this.source, metrics) : 2 * metrics.lineHeight);
|
|
124
206
|
}
|
|
125
207
|
}
|
|
126
208
|
/**
|
|
@@ -180,6 +262,10 @@ class TaskWidget extends WidgetType {
|
|
|
180
262
|
return false;
|
|
181
263
|
}
|
|
182
264
|
}
|
|
265
|
+
/** What a rule and a formula's row are drawn at by the default theme, before they are measured (#91). */
|
|
266
|
+
const RULE_HEIGHT = 33;
|
|
267
|
+
const MATH_ROW = 32;
|
|
268
|
+
const MATH_MARGINS = 32;
|
|
183
269
|
class RuleWidget extends WidgetType {
|
|
184
270
|
eq() {
|
|
185
271
|
return true;
|
|
@@ -190,6 +276,9 @@ class RuleWidget extends WidgetType {
|
|
|
190
276
|
wrap.appendChild(document.createElement('hr'));
|
|
191
277
|
return wrap;
|
|
192
278
|
}
|
|
279
|
+
get estimatedHeight() {
|
|
280
|
+
return RULE_HEIGHT;
|
|
281
|
+
}
|
|
193
282
|
}
|
|
194
283
|
class MathWidget extends WidgetType {
|
|
195
284
|
tex;
|
|
@@ -209,21 +298,31 @@ class MathWidget extends WidgetType {
|
|
|
209
298
|
katex.render(this.tex, element, { throwOnError: false, displayMode: this.display, strict: 'ignore' });
|
|
210
299
|
return element;
|
|
211
300
|
}
|
|
301
|
+
/** A formula's rows, each a line of KaTeX's display, within its margins (#91). */
|
|
302
|
+
get estimatedHeight() {
|
|
303
|
+
return this.display ? MATH_MARGINS + MATH_ROW * this.tex.split('\\\\').length : -1;
|
|
304
|
+
}
|
|
212
305
|
ignoreEvent() {
|
|
213
306
|
return false;
|
|
214
307
|
}
|
|
215
308
|
}
|
|
309
|
+
/** What an image declares no height for is drawn at before it loads, near enough (#91). */
|
|
310
|
+
const UNDECLARED_IMAGE_HEIGHT = 200;
|
|
216
311
|
class ImageWidget extends WidgetType {
|
|
217
312
|
source;
|
|
313
|
+
match;
|
|
314
|
+
alt;
|
|
218
315
|
constructor(source) {
|
|
219
316
|
super();
|
|
220
317
|
this.source = source;
|
|
318
|
+
this.match = /^!\[([^\]]*)\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)$/.exec(source);
|
|
319
|
+
this.alt = readImageAlt(this.match?.[1] ?? '');
|
|
221
320
|
}
|
|
222
321
|
eq(other) {
|
|
223
322
|
return other.source === this.source;
|
|
224
323
|
}
|
|
225
324
|
toDOM() {
|
|
226
|
-
const match =
|
|
325
|
+
const match = this.match;
|
|
227
326
|
const address = match ? followable(match[2] ?? '') : '';
|
|
228
327
|
if (!match || !address) {
|
|
229
328
|
const text = document.createElement('span');
|
|
@@ -234,9 +333,22 @@ class ImageWidget extends WidgetType {
|
|
|
234
333
|
const image = document.createElement('img');
|
|
235
334
|
image.className = 'cm-ms-image';
|
|
236
335
|
image.src = address;
|
|
237
|
-
|
|
336
|
+
// Its description and the dimensions it declares, as the page draws it (`SPEC.md` §3.14).
|
|
337
|
+
image.alt = this.alt.description;
|
|
338
|
+
if (this.alt.width !== null)
|
|
339
|
+
image.width = this.alt.width;
|
|
340
|
+
if (this.alt.height !== null)
|
|
341
|
+
image.height = this.alt.height;
|
|
238
342
|
return image;
|
|
239
343
|
}
|
|
344
|
+
/** The line it stands in is as tall as the image: the height it declares, or a guess (#91). */
|
|
345
|
+
get estimatedHeight() {
|
|
346
|
+
if (!this.match)
|
|
347
|
+
return -1;
|
|
348
|
+
if (this.alt.height !== null)
|
|
349
|
+
return this.alt.height;
|
|
350
|
+
return this.alt.width !== null ? Math.round(this.alt.width * 0.75) : UNDECLARED_IMAGE_HEIGHT;
|
|
351
|
+
}
|
|
240
352
|
ignoreEvent() {
|
|
241
353
|
return false;
|
|
242
354
|
}
|
|
@@ -251,6 +363,7 @@ const BLOCK_KINDS = new Set(['frontmatter', 'callout', 'table', 'mathBlock', 'em
|
|
|
251
363
|
function build(state) {
|
|
252
364
|
const { constructs, bodyFrom } = state.field(analysisField);
|
|
253
365
|
const { host } = state.facet(editorEnvironment);
|
|
366
|
+
const metrics = state.field(metricsField);
|
|
254
367
|
const focused = state.field(focusField);
|
|
255
368
|
const doc = state.doc;
|
|
256
369
|
const text = textOf(state);
|
|
@@ -274,17 +387,18 @@ function build(state) {
|
|
|
274
387
|
for (let at = doc.lineAt(lines.from).number; at <= doc.lineAt(lines.to).number; at++)
|
|
275
388
|
out.push(line(className).range(doc.line(at).from));
|
|
276
389
|
};
|
|
277
|
-
const block = (from, to, key, draw, revealsOnClick = true) => {
|
|
390
|
+
const block = (from, to, kind, key, draw, revealsOnClick = true) => {
|
|
278
391
|
const lines = linesOf(from, to);
|
|
279
|
-
|
|
392
|
+
const source = text.slice(lines.from, lines.to);
|
|
393
|
+
out.push(Decoration.replace({ widget: new BlockWidget(key, host, draw, revealsOnClick, { source, kind, metrics }), block: true }).range(lines.from, lines.to));
|
|
280
394
|
covered.push(lines);
|
|
281
395
|
};
|
|
282
|
-
const after = (to, key, draw) => {
|
|
396
|
+
const after = (to, key, draw, source) => {
|
|
283
397
|
const lines = linesOf(to, to);
|
|
284
|
-
out.push(Decoration.widget({ widget: new BlockWidget(key, host, draw, false), block: true, side: 1 }).range(lines.to));
|
|
398
|
+
out.push(Decoration.widget({ widget: new BlockWidget(key, host, draw, false, { source, kind: 'preview', metrics }), block: true, side: 1 }).range(lines.to));
|
|
285
399
|
};
|
|
286
400
|
if (bodyFrom === 0) {
|
|
287
|
-
out.push(Decoration.widget({ widget: new BlockWidget('add-properties', host, (view) => createElement(AddProperties, { view }), false), block: true, side: -1 }).range(0));
|
|
401
|
+
out.push(Decoration.widget({ widget: new BlockWidget('add-properties', host, (view) => createElement(AddProperties, { view }), false, { kind: 'add-properties', metrics }), block: true, side: -1 }).range(0));
|
|
288
402
|
}
|
|
289
403
|
// Blocks first: what a drawn block covers is not decorated again.
|
|
290
404
|
for (const construct of constructs) {
|
|
@@ -299,7 +413,7 @@ function build(state) {
|
|
|
299
413
|
if (open)
|
|
300
414
|
eachLine(from, to, 'cm-ms-yaml');
|
|
301
415
|
else
|
|
302
|
-
block(from, to, `front:${source}`, (view) => createElement(PropertiesBlock, { view, to }), false);
|
|
416
|
+
block(from, to, 'frontmatter', `front:${source}`, (view) => createElement(PropertiesBlock, { view, to }), false);
|
|
303
417
|
break;
|
|
304
418
|
case 'callout':
|
|
305
419
|
if (!alone(from, to))
|
|
@@ -308,7 +422,7 @@ function build(state) {
|
|
|
308
422
|
eachLine(from, to, `cm-ms-callout-source`);
|
|
309
423
|
else {
|
|
310
424
|
const marker = { ...construct.marker, type: construct.type, fold: construct.fold };
|
|
311
|
-
block(from, to, `callout:${from}:${source}`, (view) => createElement(CalloutBlock, { view, text: source, marker }));
|
|
425
|
+
block(from, to, 'callout', `callout:${from}:${source}`, (view) => createElement(CalloutBlock, { view, text: source, marker }));
|
|
312
426
|
}
|
|
313
427
|
break;
|
|
314
428
|
case 'table':
|
|
@@ -317,14 +431,15 @@ function build(state) {
|
|
|
317
431
|
if (open)
|
|
318
432
|
eachLine(from, to, 'cm-ms-table-source');
|
|
319
433
|
else
|
|
320
|
-
block(from, to, `table:${from}:${source}`, (view) => createElement(TableBlock, { view, from, to }), false);
|
|
434
|
+
block(from, to, 'table', `table:${from}:${source}`, (view) => createElement(TableBlock, { view, from, to }), false);
|
|
321
435
|
break;
|
|
322
436
|
case 'mathBlock':
|
|
323
437
|
if (!alone(from, to))
|
|
324
438
|
break;
|
|
325
439
|
if (open) {
|
|
326
440
|
eachLine(from, to, 'cm-ms-code-line');
|
|
327
|
-
|
|
441
|
+
const preview = `$$\n${construct.tex}\n$$`;
|
|
442
|
+
after(to, `math-preview:${construct.tex}`, () => createElement(Snippet, { text: preview }), preview);
|
|
328
443
|
}
|
|
329
444
|
else {
|
|
330
445
|
out.push(Decoration.replace({ widget: new MathWidget(construct.tex, true), block: true }).range(lines.from, lines.to));
|
|
@@ -335,7 +450,7 @@ function build(state) {
|
|
|
335
450
|
if (open)
|
|
336
451
|
out.push(mark('cm-ms-wikilink').range(from, to));
|
|
337
452
|
else
|
|
338
|
-
block(from, to, `embed:${source}`, () => createElement(Snippet, { text: source }));
|
|
453
|
+
block(from, to, 'embed', `embed:${source}`, () => createElement(Snippet, { text: source }));
|
|
339
454
|
break;
|
|
340
455
|
case 'rule':
|
|
341
456
|
if (!alone(from, to))
|
|
@@ -350,7 +465,7 @@ function build(state) {
|
|
|
350
465
|
case 'code': {
|
|
351
466
|
const diagram = construct.language === 'mermaid' && construct.open !== null;
|
|
352
467
|
if (diagram && !open && alone(from, to)) {
|
|
353
|
-
block(from, to, `diagram:${source}`, () => createElement(Snippet, { text: source }));
|
|
468
|
+
block(from, to, 'diagram', `diagram:${source}`, () => createElement(Snippet, { text: source }));
|
|
354
469
|
break;
|
|
355
470
|
}
|
|
356
471
|
codeLines(construct, out, doc, text);
|
|
@@ -359,12 +474,14 @@ function build(state) {
|
|
|
359
474
|
const fence = construct.open.from;
|
|
360
475
|
const language = construct.language;
|
|
361
476
|
out.push(Decoration.widget({
|
|
362
|
-
widget: new BlockWidget(`language:${fence}:${language ?? ''}`, host, (view) => createElement(LanguageControl, { view, from: fence, language }), false, true),
|
|
477
|
+
widget: new BlockWidget(`language:${fence}:${language ?? ''}`, host, (view) => createElement(LanguageControl, { view, from: fence, language }), false, { inline: true }),
|
|
363
478
|
side: 1,
|
|
364
479
|
}).range(construct.open.to));
|
|
365
480
|
}
|
|
366
|
-
if (diagram)
|
|
367
|
-
|
|
481
|
+
if (diagram) {
|
|
482
|
+
const preview = '```mermaid\n' + construct.code + '\n```';
|
|
483
|
+
after(to, `diagram-preview:${construct.code}`, () => createElement(Snippet, { text: preview }), preview);
|
|
484
|
+
}
|
|
368
485
|
covered.push(lines);
|
|
369
486
|
break;
|
|
370
487
|
}
|
|
@@ -420,7 +537,7 @@ function build(state) {
|
|
|
420
537
|
const pending = host.reaches(construct.target) === 0 && !host.keeps(construct.target);
|
|
421
538
|
if (construct.embed && host.keeps(construct.target) && !touches(from, to)) {
|
|
422
539
|
const source = text.slice(from, to);
|
|
423
|
-
out.push(Decoration.replace({ widget: new BlockWidget(`attachment:${source}`, host, () => createElement(Snippet, { text: source }), true, true) }).range(from, to));
|
|
540
|
+
out.push(Decoration.replace({ widget: new BlockWidget(`attachment:${source}`, host, () => createElement(Snippet, { text: source }), true, { inline: true, source }) }).range(from, to));
|
|
424
541
|
break;
|
|
425
542
|
}
|
|
426
543
|
out.push(mark(`cm-ms-wikilink${pending ? ' cm-ms-pending' : ''}`, { 'data-target': construct.target, title: construct.target }).range(construct.label.from, construct.label.to));
|
|
@@ -660,10 +777,16 @@ const grammarLoader = ViewPlugin.fromClass(class {
|
|
|
660
777
|
}
|
|
661
778
|
}
|
|
662
779
|
});
|
|
663
|
-
/**
|
|
664
|
-
|
|
780
|
+
/**
|
|
781
|
+
* The live page: the analysis, the decorations over it, and what the focus and a modifier-click do.
|
|
782
|
+
* `metrics` are the editor's text column measured before the editor is made, which the first drawn
|
|
783
|
+
* blocks estimate their height by; without them they assume `DEFAULT_METRICS` until it is measured.
|
|
784
|
+
*/
|
|
785
|
+
export function livePage(metrics) {
|
|
665
786
|
return [
|
|
666
787
|
analysisField,
|
|
788
|
+
metricsField.init(() => metrics ?? DEFAULT_METRICS),
|
|
789
|
+
metricsWatcher,
|
|
667
790
|
focusField,
|
|
668
791
|
decorationsField,
|
|
669
792
|
EditorView.focusChangeEffect.of((_state, focusing) => setFocused.of(focusing)),
|
package/dist/host.d.ts
CHANGED
|
@@ -92,9 +92,21 @@ export interface NoteHost {
|
|
|
92
92
|
/**
|
|
93
93
|
* Draws a file the notebook keeps where a note embeds it: an image, audio, video, or a card. The
|
|
94
94
|
* dimensions are the ones the embed declared, in CSS pixels, or `null`.
|
|
95
|
+
*
|
|
96
|
+
* In edit mode the node may be drawn outside the host's React tree, in the root of a block the
|
|
97
|
+
* editor draws, so it must not depend on the host's context; a node that throws there leaves the
|
|
98
|
+
* block's Markdown in its place (#90).
|
|
95
99
|
*/
|
|
96
100
|
readonly renderAttachment?: (file: AttachmentRequest) => ReactNode;
|
|
97
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Draws a link to an internal address, through the host's router.
|
|
103
|
+
*
|
|
104
|
+
* In edit mode the node may be drawn outside the host's React tree, in the root of a block the
|
|
105
|
+
* editor draws — a callout, a table, an embed, the properties — so it must not depend on the host's
|
|
106
|
+
* context: a router's `<Link>`, which reads its router from context, throws there. Hand it what it
|
|
107
|
+
* needs (a `navigate` captured where the router is) or draw an anchor whose click the host follows.
|
|
108
|
+
* A node that throws leaves the block's Markdown in its place (#90).
|
|
109
|
+
*/
|
|
98
110
|
readonly renderLink?: (link: LinkRequest) => ReactNode;
|
|
99
111
|
/** The words the page shows, in the reader's language; each one left out is MemorySmith's English. */
|
|
100
112
|
readonly strings?: NoteStringsInput;
|
package/dist/strings.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ export interface EditorWords {
|
|
|
109
109
|
readonly attachFailed: string;
|
|
110
110
|
/** Said after the host's step while more files wait their turn (#88). */
|
|
111
111
|
readonly attachQueued: (count: number) => string;
|
|
112
|
+
/** Said over a drawn block's lines when drawing it failed, so the block is never left empty (#90). */
|
|
113
|
+
readonly blockFailed: string;
|
|
112
114
|
/** A code block's language, chosen at its top right (#84). */
|
|
113
115
|
readonly language: string;
|
|
114
116
|
readonly plainText: string;
|
package/dist/strings.js
CHANGED
|
@@ -113,6 +113,7 @@ export const ENGLISH = {
|
|
|
113
113
|
attaching: 'Attaching…',
|
|
114
114
|
attachFailed: 'The file was not attached',
|
|
115
115
|
attachQueued: (count) => (count === 1 ? '1 more file waiting' : `${count} more files waiting`),
|
|
116
|
+
blockFailed: 'This block could not be drawn; its Markdown is below',
|
|
116
117
|
language: 'Language',
|
|
117
118
|
plainText: 'Plain text',
|
|
118
119
|
mermaidDiagram: 'Mermaid diagram',
|
package/dist/theme.d.ts
CHANGED
|
@@ -315,7 +315,10 @@ export interface Theme {
|
|
|
315
315
|
};
|
|
316
316
|
/** An icon per callout kind, drawn in the title in place of the stylesheet's. */
|
|
317
317
|
readonly icons?: Readonly<Record<string, ReactNode>>;
|
|
318
|
-
/**
|
|
318
|
+
/**
|
|
319
|
+
* The slots the theme fills. In edit mode a slot may be drawn outside the host's React tree, in
|
|
320
|
+
* the root of a block the editor draws, so it must not depend on the host's context (#90).
|
|
321
|
+
*/
|
|
319
322
|
readonly components?: Partial<ThemeComponents>;
|
|
320
323
|
}
|
|
321
324
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorysmithapp/markdown-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"unified": "11.0.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@memorysmithapp/markdown-spec": "0.
|
|
82
|
+
"@memorysmithapp/markdown-spec": "0.6.0",
|
|
83
83
|
"@types/hast": "3.0.5",
|
|
84
84
|
"@types/react": "19.2.18",
|
|
85
85
|
"@types/react-dom": "19.2.4",
|
package/structure.css
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
box-sizing: border-box;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* What is laid out of the flow hangs from the note, never from the page: a
|
|
19
|
+
host that scrolls the note in a pane of its own keeps a page of its own
|
|
20
|
+
height (#93). */
|
|
21
|
+
.markdown {
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
.markdown img {
|
|
19
26
|
max-width: 100%;
|
|
20
27
|
}
|
|
@@ -397,6 +404,12 @@
|
|
|
397
404
|
.markdown .cm-ms-math-block {
|
|
398
405
|
display: flow-root;
|
|
399
406
|
}
|
|
407
|
+
/* A block that could not be drawn shows its lines as they were written (#90). */
|
|
408
|
+
.markdown .cm-ms-block-failed pre {
|
|
409
|
+
margin: 0;
|
|
410
|
+
white-space: pre-wrap;
|
|
411
|
+
overflow-wrap: anywhere;
|
|
412
|
+
}
|
|
400
413
|
/* A table's cell is where a click edits it: it fills its column and keeps a
|
|
401
414
|
size of its own when it is empty, as every cell of a new table is, or it
|
|
402
415
|
would be nothing wide and nothing could be clicked (#68). */
|
package/theme-default.css
CHANGED
|
@@ -1913,6 +1913,20 @@
|
|
|
1913
1913
|
.markdown .cm-ms-block:hover {
|
|
1914
1914
|
box-shadow: 0 0 0 1px var(--ms-border);
|
|
1915
1915
|
}
|
|
1916
|
+
.markdown .cm-ms-block-failed {
|
|
1917
|
+
padding: 0.5rem 0.75rem;
|
|
1918
|
+
border: 1px dashed var(--ms-border);
|
|
1919
|
+
border-radius: var(--ms-radius);
|
|
1920
|
+
}
|
|
1921
|
+
.markdown .cm-ms-block-failed-notice {
|
|
1922
|
+
margin: 0 0 0.35rem;
|
|
1923
|
+
color: var(--ms-text-soft);
|
|
1924
|
+
font-size: 0.85em;
|
|
1925
|
+
}
|
|
1926
|
+
.markdown .cm-ms-block-failed pre {
|
|
1927
|
+
font-family: var(--ms-font-mono);
|
|
1928
|
+
font-size: 0.9em;
|
|
1929
|
+
}
|
|
1916
1930
|
.markdown .edit-block-controls {
|
|
1917
1931
|
opacity: 0;
|
|
1918
1932
|
padding: 0.1rem;
|