@memorysmithapp/markdown-renderer 0.7.1 → 0.8.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 +1 -1
- package/dist/Markdown.d.ts +12 -1
- package/dist/Markdown.js +41 -33
- package/dist/diagnose.js +15 -1
- package/dist/edit/ContextMenu.d.ts +1 -1
- package/dist/edit/ContextMenu.js +1 -1
- package/dist/edit/NoteEditor.js +3 -2
- package/dist/edit/blocks.d.ts +24 -5
- package/dist/edit/blocks.js +384 -56
- package/dist/edit/heights.js +2 -0
- package/dist/edit/image-size.d.ts +26 -10
- package/dist/edit/image-size.js +52 -17
- package/dist/edit/live.js +112 -26
- package/dist/host.d.ts +15 -1
- package/dist/image-dimensions.d.ts +30 -5
- package/dist/image-dimensions.js +39 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kept-image.d.ts +20 -0
- package/dist/kept-image.js +47 -0
- package/dist/markdown.js +2 -4
- package/dist/mode-icons.d.ts +3 -0
- package/dist/mode-icons.js +7 -2
- package/dist/strings.d.ts +16 -1
- package/dist/strings.js +9 -1
- package/dist/theme.d.ts +8 -1
- package/package.json +2 -2
- package/structure.css +46 -8
- package/theme-default.css +48 -7
package/dist/edit/image-size.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The size an image is drawn at, as its notation writes it (#98): the dimensions after the pipe
|
|
3
3
|
* of a Markdown image's description, `` (`SPEC.md` §3.14), or of the
|
|
4
|
-
* embed of a file the notebook keeps, `![[photo.jpg|300]]` (§5.7)
|
|
5
|
-
* rewrites them, and
|
|
4
|
+
* embed of a file the notebook keeps, `![[photo.jpg|300]]` (§5.7) — pixels, a width and a height,
|
|
5
|
+
* or a share of the page (#108). The editor reads them and rewrites them, and the description
|
|
6
|
+
* before the pipe of a Markdown image (#106), and no other byte.
|
|
6
7
|
*/
|
|
7
|
-
import { readDimensions, readImageAlt } from '../image-dimensions.js';
|
|
8
|
+
import { readDimensions, readImageAlt, writeDimensions } from '../image-dimensions.js';
|
|
8
9
|
/** The largest width the notation writes: five digits (`SPEC.md` §3.14). */
|
|
9
10
|
export const MAX_IMAGE_WIDTH = 99999;
|
|
10
|
-
const
|
|
11
|
+
const UNSIZED = { width: null, height: null, percent: null };
|
|
12
|
+
/** The alt text of a Markdown image, a bracket escaped inside it included. */
|
|
13
|
+
const MARKDOWN_IMAGE = /^!\[((?:\\.|[^\]\\])*)\]\(/;
|
|
11
14
|
const EMBED = /^!\[\[([^\]]*)\]\]$/;
|
|
12
15
|
/**
|
|
13
16
|
* The size an image's source declares, and where it is written. `null` for a source that is not an
|
|
@@ -18,7 +21,10 @@ export function readImageSize(source) {
|
|
|
18
21
|
// A block's line may be indented, or end in spaces, around its one embed.
|
|
19
22
|
const lead = source.length - source.trimStart().length;
|
|
20
23
|
const size = readTrimmed(source.trim());
|
|
21
|
-
|
|
24
|
+
if (!size || !lead)
|
|
25
|
+
return size;
|
|
26
|
+
const shift = (span) => ({ ...span, from: span.from + lead, to: span.to + lead });
|
|
27
|
+
return { ...size, at: shift(size.at), description: size.description && shift(size.description) };
|
|
22
28
|
}
|
|
23
29
|
function readTrimmed(source) {
|
|
24
30
|
const image = MARKDOWN_IMAGE.exec(source);
|
|
@@ -26,9 +32,12 @@ function readTrimmed(source) {
|
|
|
26
32
|
const alt = image[1] ?? '';
|
|
27
33
|
const start = 2;
|
|
28
34
|
const read = readImageAlt(alt);
|
|
29
|
-
if (read.width === null)
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if (read.width === null && read.percent === null) {
|
|
36
|
+
const end = start + alt.length;
|
|
37
|
+
return { ...UNSIZED, at: { from: end, to: end }, pipe: '|', description: { text: unescape(alt), from: start, to: end } };
|
|
38
|
+
}
|
|
39
|
+
const pipe = start + alt.lastIndexOf('|');
|
|
40
|
+
return { ...read, at: { from: pipe, to: start + alt.length }, pipe: '|', description: { text: unescape(alt.slice(0, pipe - start)), from: start, to: pipe } };
|
|
32
41
|
}
|
|
33
42
|
const embed = EMBED.exec(source);
|
|
34
43
|
if (embed) {
|
|
@@ -36,26 +45,52 @@ function readTrimmed(source) {
|
|
|
36
45
|
const start = 3;
|
|
37
46
|
const pipe = /\\?\|/.exec(inner);
|
|
38
47
|
if (!pipe)
|
|
39
|
-
return {
|
|
48
|
+
return { ...UNSIZED, at: { from: start + inner.length, to: start + inner.length }, pipe: '|', description: null };
|
|
40
49
|
const measured = readDimensions(inner.slice(pipe.index + pipe[0].length));
|
|
41
50
|
if (!measured)
|
|
42
51
|
return null;
|
|
43
|
-
return {
|
|
52
|
+
return { ...measured, at: { from: start + pipe.index, to: start + inner.length }, pipe: pipe[0], description: null };
|
|
44
53
|
}
|
|
45
54
|
return null;
|
|
46
55
|
}
|
|
56
|
+
/** The text of a description as a reader gets it: a backslash escape is the character it escapes. */
|
|
57
|
+
function unescape(text) {
|
|
58
|
+
return text.replace(/\\([!-/:-@[-`{-~])/g, '$1');
|
|
59
|
+
}
|
|
47
60
|
/**
|
|
48
|
-
* The change that draws the image whose source starts at `from` at
|
|
49
|
-
* or at its own size for `null`: the
|
|
50
|
-
* Empty when the source is not an
|
|
61
|
+
* The change that draws the image whose source starts at `from` at the dimensions asked — a width
|
|
62
|
+
* as a number, its height following, or whole `Dimensions` — or at its own size for `null`: the
|
|
63
|
+
* dimensions written, replaced or taken away, and nothing else. Empty when the source is not an
|
|
64
|
+
* image's notation, already says so, or the dimensions asked are none the notation writes: a
|
|
65
|
+
* value out of its range is refused, never brought into it.
|
|
51
66
|
*/
|
|
52
|
-
export function resizeImage(source, from,
|
|
67
|
+
export function resizeImage(source, from, asked) {
|
|
53
68
|
const size = readImageSize(source);
|
|
54
69
|
if (!size)
|
|
55
70
|
return [];
|
|
56
|
-
const wanted =
|
|
57
|
-
if (wanted
|
|
71
|
+
const wanted = typeof asked === 'number' ? { width: Math.round(asked), height: null, percent: null } : asked;
|
|
72
|
+
if (wanted !== null && !writable(wanted))
|
|
73
|
+
return [];
|
|
74
|
+
const insert = wanted === null ? '' : `${size.pipe}${writeDimensions(wanted)}`;
|
|
75
|
+
if (insert === source.slice(size.at.from, size.at.to))
|
|
58
76
|
return [];
|
|
59
|
-
const insert = wanted === null ? '' : `${size.pipe}${wanted}`;
|
|
60
77
|
return [{ from: from + size.at.from, to: from + size.at.to, insert }];
|
|
61
78
|
}
|
|
79
|
+
/** Whether the notation writes these dimensions (§3.14): five digits from 1, or 1% to 100%. */
|
|
80
|
+
function writable({ width, height, percent }) {
|
|
81
|
+
const pixels = (value) => Number.isInteger(value) && value >= 1 && value <= MAX_IMAGE_WIDTH;
|
|
82
|
+
if (percent !== null)
|
|
83
|
+
return width === null && height === null && Number.isInteger(percent) && percent >= 1 && percent <= 100;
|
|
84
|
+
return width !== null && pixels(width) && (height === null || pixels(height));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The change that writes `text` as the description of the Markdown image whose source starts at
|
|
88
|
+
* `from`, before its pipe, which it keeps (§3.14): a bracket and a backslash escaped, so the image
|
|
89
|
+
* stays one. Empty for an embed, which has no description to write (§5.8), and when nothing changes.
|
|
90
|
+
*/
|
|
91
|
+
export function describeImage(source, from, text) {
|
|
92
|
+
const size = readImageSize(source);
|
|
93
|
+
if (!size?.description || size.description.text === text)
|
|
94
|
+
return [];
|
|
95
|
+
return [{ from: from + size.description.from, to: from + size.description.to, insert: text.replace(/[\\[\]]/g, '\\$&') }];
|
|
96
|
+
}
|
package/dist/edit/live.js
CHANGED
|
@@ -14,7 +14,7 @@ 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 {
|
|
17
|
+
import { MarkdownImage } from '../Markdown.js';
|
|
18
18
|
import { highlightLanguage } from '../highlight.js';
|
|
19
19
|
import { analyze } from './analyze.js';
|
|
20
20
|
import { DEFAULT_METRICS, estimateHeight, lineHeightOf, measureLines, measuredHeight, rememberHeight, withinReason } from './heights.js';
|
|
@@ -256,10 +256,21 @@ class BlockWidget extends WidgetType {
|
|
|
256
256
|
mount(view, element, this.draw(view), this.source, this.identity, measured, held);
|
|
257
257
|
element.addEventListener('mousedown', (event) => {
|
|
258
258
|
const target = event.target;
|
|
259
|
-
|
|
259
|
+
// A control's, or a press in a frame its controls hold busy — a field focused, a handle
|
|
260
|
+
// dragged, the click a touch on them leaves behind — never shows the source (#107).
|
|
261
|
+
if (target.closest('[data-ms-control], [data-ms-busy]'))
|
|
260
262
|
return;
|
|
261
|
-
|
|
263
|
+
// CodeMirror hands the editor no event from inside a drawn block: a link around it is followed here (#112).
|
|
264
|
+
if (event.metaKey || event.ctrlKey) {
|
|
265
|
+
followLinkAround(event, view, element);
|
|
262
266
|
return;
|
|
267
|
+
}
|
|
268
|
+
// A picture sized in place, as a table is edited in place, shows its source from its button or
|
|
269
|
+
// the keyboard, never from a click (#112); kept from placing the caret beside it, which would.
|
|
270
|
+
if (target.closest('[data-ms-in-place]')) {
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
263
274
|
const widget = drawing.get(element) ?? this;
|
|
264
275
|
if (!widget.revealsOnClick)
|
|
265
276
|
return;
|
|
@@ -425,15 +436,22 @@ class MathWidget extends WidgetType {
|
|
|
425
436
|
return false;
|
|
426
437
|
}
|
|
427
438
|
}
|
|
428
|
-
/**
|
|
439
|
+
/**
|
|
440
|
+
* A Markdown image's alt text, address and title, when its address is one the page follows: a
|
|
441
|
+
* bare destination, or one in `<>` that may hold spaces, `<assets/diagram final.png>`.
|
|
442
|
+
*/
|
|
429
443
|
function readImage(source) {
|
|
430
|
-
const match = /^!\[([^\]]*)\]\(\s
|
|
431
|
-
const address = match ? followable(match[2] ?? '') : '';
|
|
432
|
-
return match && address ? { alt:
|
|
444
|
+
const match = /^!\[((?:\\.|[^\]\\])*)\]\(\s*(?:<([^>\n]*)>|([^\s)]+))(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)$/.exec(source);
|
|
445
|
+
const address = match ? followable(match[2] ?? match[3] ?? '') : '';
|
|
446
|
+
return match && address ? { alt: match[1] ?? '', address, title: match[4] ?? match[5] } : null;
|
|
433
447
|
}
|
|
434
|
-
/**
|
|
435
|
-
|
|
436
|
-
|
|
448
|
+
/**
|
|
449
|
+
* An image drawn as the page draws it, by the page's own component: with its description and the
|
|
450
|
+
* dimensions it declares (`SPEC.md` §3.14), and as the kept file it names when its address names
|
|
451
|
+
* no host (§5.8, #104), so edit mode asks nothing of a path under the page either.
|
|
452
|
+
*/
|
|
453
|
+
function drawImage({ alt, address, title }) {
|
|
454
|
+
return createElement(MarkdownImage, { className: 'cm-ms-image', src: address, alt, ...(title === undefined ? {} : { title }) });
|
|
437
455
|
}
|
|
438
456
|
// ── Decorations ──────────────────────────────────────────────────────────────
|
|
439
457
|
const hide = Decoration.replace({});
|
|
@@ -863,29 +881,96 @@ const blockKeys = Prec.high(keymap.of([
|
|
|
863
881
|
{ key: 'ArrowDown', run: enterBlock(true) },
|
|
864
882
|
{ key: 'ArrowUp', run: enterBlock(false) },
|
|
865
883
|
]));
|
|
866
|
-
/**
|
|
867
|
-
const
|
|
884
|
+
/** Where the first press of a gesture fell in the note, read before the press moved the caret. */
|
|
885
|
+
const firstPress = new WeakMap();
|
|
886
|
+
/** The longest a browser counts two presses as one gesture: Windows' default double-click time. */
|
|
887
|
+
const MULTI_CLICK_MS = 500;
|
|
888
|
+
/**
|
|
889
|
+
* Selects the word, or the line, a double or a triple click starts on (#114). The first press puts
|
|
890
|
+
* the caret where it fell, which reveals the markers beside it, or hides the ones it left, and moves
|
|
891
|
+
* the rest of the line under the pointer by their width: the second press, read where it lands,
|
|
892
|
+
* would select the marker or the next word. It is read where the first one fell instead.
|
|
893
|
+
*/
|
|
894
|
+
const multiClick = EditorView.domEventHandlers({
|
|
868
895
|
mousedown(event, view) {
|
|
869
|
-
if (
|
|
896
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
870
897
|
return false;
|
|
871
|
-
|
|
872
|
-
|
|
898
|
+
if (event.detail <= 1) {
|
|
899
|
+
const at = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
900
|
+
if (at === null)
|
|
901
|
+
firstPress.delete(view);
|
|
902
|
+
else
|
|
903
|
+
firstPress.set(view, { at, time: event.timeStamp });
|
|
873
904
|
return false;
|
|
874
|
-
event.preventDefault();
|
|
875
|
-
const { host } = view.state.facet(editorEnvironment);
|
|
876
|
-
const name = target.dataset['target'];
|
|
877
|
-
if (name !== undefined) {
|
|
878
|
-
const address = host.resolve(name);
|
|
879
|
-
if (address !== null)
|
|
880
|
-
host.navigate(address);
|
|
881
|
-
return true;
|
|
882
905
|
}
|
|
883
|
-
const
|
|
884
|
-
if (
|
|
885
|
-
|
|
906
|
+
const first = firstPress.get(view);
|
|
907
|
+
if (!first || event.timeStamp - first.time > MULTI_CLICK_MS * (event.detail - 1))
|
|
908
|
+
return false;
|
|
909
|
+
const { state } = view;
|
|
910
|
+
const line = state.doc.lineAt(first.at);
|
|
911
|
+
const range = event.detail === 2 ? (state.wordAt(first.at) ?? EditorSelection.cursor(first.at)) : EditorSelection.range(line.from, line.to);
|
|
912
|
+
event.preventDefault();
|
|
913
|
+
view.dispatch({ selection: EditorSelection.single(range.from, range.to), userEvent: 'select.pointer' });
|
|
886
914
|
return true;
|
|
887
915
|
},
|
|
888
916
|
});
|
|
917
|
+
/** Follows a link with a modifier held: a wikilink through the host, an address the page follows in a new tab. */
|
|
918
|
+
const follow = EditorView.domEventHandlers({
|
|
919
|
+
mousedown(event, view) {
|
|
920
|
+
if (!(event.metaKey || event.ctrlKey))
|
|
921
|
+
return false;
|
|
922
|
+
return followLink(event, view);
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
/** Follows the link a press with a modifier fell in, if any: `true` when it did. */
|
|
926
|
+
function followLink(event, view) {
|
|
927
|
+
const target = event.target.closest('[data-target], [data-href]');
|
|
928
|
+
if (!target)
|
|
929
|
+
return false;
|
|
930
|
+
event.preventDefault();
|
|
931
|
+
followAddress(view, target.dataset['target'], target.dataset['href']);
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Follows the link a drawn block stands in, pressed with a modifier: CodeMirror draws no mark around
|
|
936
|
+
* a widget, so the link around an image is found among the marks over its source (#112).
|
|
937
|
+
*/
|
|
938
|
+
function followLinkAround(event, view, element) {
|
|
939
|
+
if (followLink(event, view))
|
|
940
|
+
return true;
|
|
941
|
+
const widget = drawing.get(element);
|
|
942
|
+
if (!widget)
|
|
943
|
+
return false;
|
|
944
|
+
const from = view.posAtDOM(element);
|
|
945
|
+
const to = from + widget.source.length;
|
|
946
|
+
const links = [];
|
|
947
|
+
view.state.field(decorationsField).between(from, to, (start, end, decoration) => {
|
|
948
|
+
const attributes = decoration.spec.attributes;
|
|
949
|
+
if (!attributes || start > from || end < to)
|
|
950
|
+
return;
|
|
951
|
+
if (attributes['data-target'] !== undefined || attributes['data-href'] !== undefined)
|
|
952
|
+
links.push({ target: attributes['data-target'], href: attributes['data-href'] });
|
|
953
|
+
});
|
|
954
|
+
const link = links[0];
|
|
955
|
+
if (!link)
|
|
956
|
+
return false;
|
|
957
|
+
event.preventDefault();
|
|
958
|
+
followAddress(view, link.target, link.href);
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
/** Goes to a wikilink's note through the host, or opens an address the page follows in a new tab. */
|
|
962
|
+
function followAddress(view, name, href) {
|
|
963
|
+
const { host } = view.state.facet(editorEnvironment);
|
|
964
|
+
if (name !== undefined) {
|
|
965
|
+
const address = host.resolve(name);
|
|
966
|
+
if (address !== null)
|
|
967
|
+
host.navigate(address);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const address = followable(href ?? '');
|
|
971
|
+
if (address)
|
|
972
|
+
window.open(address, '_blank', 'noreferrer');
|
|
973
|
+
}
|
|
889
974
|
/** How long an embed measured ahead of being drawn may take to load before it is let go (#94). */
|
|
890
975
|
const PREMEASURE_TIMEOUT = 8000;
|
|
891
976
|
/**
|
|
@@ -1027,5 +1112,6 @@ export function livePage(metrics) {
|
|
|
1027
1112
|
embedPremeasure,
|
|
1028
1113
|
blockKeys,
|
|
1029
1114
|
follow,
|
|
1115
|
+
multiClick,
|
|
1030
1116
|
];
|
|
1031
1117
|
}
|
package/dist/host.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export interface NoteHost {
|
|
|
93
93
|
readonly fileOf?: (name: string) => KeptFile | null | PromiseLike<KeptFile | null>;
|
|
94
94
|
/**
|
|
95
95
|
* Draws a file the notebook keeps where a note embeds it: an image, audio, video, or a card. The
|
|
96
|
-
* dimensions are the ones the embed declared, in CSS pixels, or `null`.
|
|
96
|
+
* dimensions are the ones the embed declared, in CSS pixels or as a share of the page, or `null`.
|
|
97
97
|
*
|
|
98
98
|
* In edit mode the node may be drawn outside the host's React tree, in the root of a block the
|
|
99
99
|
* editor draws, so it must not depend on the host's context; a node that throws there leaves the
|
|
@@ -199,8 +199,22 @@ export interface AttachAttributes {
|
|
|
199
199
|
}
|
|
200
200
|
export interface AttachmentRequest {
|
|
201
201
|
readonly name: string;
|
|
202
|
+
/** The width and height the embed declared, in CSS pixels, or `null`. */
|
|
202
203
|
readonly width: number | null;
|
|
203
204
|
readonly height: number | null;
|
|
205
|
+
/**
|
|
206
|
+
* The width it declared as a share of the width the note is read at, from 1 to 100, which it is
|
|
207
|
+
* drawn at most, or `null` (#108). `width` and `height` are then `null`.
|
|
208
|
+
*/
|
|
209
|
+
readonly percent: number | null;
|
|
210
|
+
/**
|
|
211
|
+
* What a Markdown image that names the file wrote before its pipe, ``
|
|
212
|
+
* (`SPEC.md` §5.8, #104): an image's `alt`, a card's text. Absent for an embed, and empty when
|
|
213
|
+
* the image wrote none, when the file's own description is the one to draw.
|
|
214
|
+
*/
|
|
215
|
+
readonly description?: string;
|
|
216
|
+
/** The title that image wrote after its destination, ``, when it wrote one. */
|
|
217
|
+
readonly title?: string;
|
|
204
218
|
}
|
|
205
219
|
/** A link to draw through the host's router. */
|
|
206
220
|
export interface LinkRequest {
|
|
@@ -10,14 +10,26 @@
|
|
|
10
10
|
* **A value that is neither form is not a dimension.** ``
|
|
11
11
|
* keeps `Engelbart|large` as the whole description, because deleting what an
|
|
12
12
|
* author wrote into an accessibility label is the worse of the two failures.
|
|
13
|
+
* A width may be a share of the page, ``, which it is drawn
|
|
14
|
+
* at most (#108).
|
|
15
|
+
*
|
|
13
16
|
* Until the specification declared this form, an imported note displayed the
|
|
14
17
|
* pipe and the digits on screen and mandated the whole string as the label.
|
|
15
18
|
*/
|
|
16
|
-
export interface ImageAlt {
|
|
19
|
+
export interface ImageAlt extends Dimensions {
|
|
17
20
|
/** What a reader hears, and sees when the image does not load. */
|
|
18
21
|
readonly description: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What the pipe declares: a width and maybe a height in CSS pixels, or a width as a share of the
|
|
25
|
+
* page, at most (#108). `percent` and `width` are never both set, and a height goes only with a
|
|
26
|
+
* width in pixels.
|
|
27
|
+
*/
|
|
28
|
+
export interface Dimensions {
|
|
19
29
|
readonly width: number | null;
|
|
20
30
|
readonly height: number | null;
|
|
31
|
+
/** The width as a share of the width the note is read at, from 1 to 100, which it is drawn at most. */
|
|
32
|
+
readonly percent: number | null;
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* What follows a pipe, when it is a dimension and not text.
|
|
@@ -26,8 +38,21 @@ export interface ImageAlt {
|
|
|
26
38
|
* Markdown image does — `![[engelbart.jpg|100x145]]` and
|
|
27
39
|
* `` mean one thing — so both read it here.
|
|
28
40
|
*/
|
|
29
|
-
export declare function readDimensions(value: string):
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
export declare function readDimensions(value: string): Dimensions | null;
|
|
42
|
+
/** The dimensions as the notation writes them after the pipe: `300`, `300x180` or `50%`; empty for none. */
|
|
43
|
+
export declare function writeDimensions({ width, height, percent }: Dimensions): string;
|
|
44
|
+
/**
|
|
45
|
+
* The attributes an element is drawn at for its dimensions: pixels as `width` and `height`, which
|
|
46
|
+
* reserve the box before the bytes arrive, a height as a style too, since the surface gives an
|
|
47
|
+
* image `height: auto`, which two numbers overrule; and a share of the page as a `max-width`, so
|
|
48
|
+
* an image narrower than it keeps its own width and is never blurred (§3.14).
|
|
49
|
+
*/
|
|
50
|
+
export declare function sizedAttributes({ width, height, percent }: Dimensions): {
|
|
51
|
+
width?: number;
|
|
52
|
+
height?: number;
|
|
53
|
+
style?: {
|
|
54
|
+
height?: string;
|
|
55
|
+
maxWidth?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
33
58
|
export declare function readImageAlt(alt: string): ImageAlt;
|
package/dist/image-dimensions.js
CHANGED
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
* **A value that is neither form is not a dimension.** ``
|
|
11
11
|
* keeps `Engelbart|large` as the whole description, because deleting what an
|
|
12
12
|
* author wrote into an accessibility label is the worse of the two failures.
|
|
13
|
+
* A width may be a share of the page, ``, which it is drawn
|
|
14
|
+
* at most (#108).
|
|
15
|
+
*
|
|
13
16
|
* Until the specification declared this form, an imported note displayed the
|
|
14
17
|
* pipe and the digits on screen and mandated the whole string as the label.
|
|
15
18
|
*/
|
|
16
19
|
/** `100` or `100x145`, and nothing else. */
|
|
17
20
|
const DIMENSIONS = /^(\d{1,5})(?:x(\d{1,5}))?$/;
|
|
21
|
+
/** `1%` to `100%`, digits and nothing else: no height, no mixed pair, no fraction, no space (§3.14). */
|
|
22
|
+
const PERCENT = /^(\d{1,3})%$/;
|
|
18
23
|
/**
|
|
19
24
|
* What follows a pipe, when it is a dimension and not text.
|
|
20
25
|
*
|
|
@@ -23,20 +28,50 @@ const DIMENSIONS = /^(\d{1,5})(?:x(\d{1,5}))?$/;
|
|
|
23
28
|
* `` mean one thing — so both read it here.
|
|
24
29
|
*/
|
|
25
30
|
export function readDimensions(value) {
|
|
26
|
-
const
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
const share = PERCENT.exec(trimmed);
|
|
33
|
+
if (share) {
|
|
34
|
+
const percent = Number(share[1]);
|
|
35
|
+
return percent >= 1 && percent <= 100 ? { width: null, height: null, percent } : null;
|
|
36
|
+
}
|
|
37
|
+
const measured = DIMENSIONS.exec(trimmed);
|
|
27
38
|
if (!measured)
|
|
28
39
|
return null;
|
|
29
40
|
return {
|
|
30
41
|
width: Number(measured[1]),
|
|
31
42
|
height: measured[2] === undefined ? null : Number(measured[2]),
|
|
43
|
+
percent: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The dimensions as the notation writes them after the pipe: `300`, `300x180` or `50%`; empty for none. */
|
|
47
|
+
export function writeDimensions({ width, height, percent }) {
|
|
48
|
+
if (percent !== null)
|
|
49
|
+
return `${percent}%`;
|
|
50
|
+
if (width === null)
|
|
51
|
+
return '';
|
|
52
|
+
return height === null ? String(width) : `${width}x${height}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The attributes an element is drawn at for its dimensions: pixels as `width` and `height`, which
|
|
56
|
+
* reserve the box before the bytes arrive, a height as a style too, since the surface gives an
|
|
57
|
+
* image `height: auto`, which two numbers overrule; and a share of the page as a `max-width`, so
|
|
58
|
+
* an image narrower than it keeps its own width and is never blurred (§3.14).
|
|
59
|
+
*/
|
|
60
|
+
export function sizedAttributes({ width, height, percent }) {
|
|
61
|
+
if (percent !== null)
|
|
62
|
+
return { style: { maxWidth: `${percent}%` } };
|
|
63
|
+
return {
|
|
64
|
+
...(width === null ? {} : { width }),
|
|
65
|
+
...(height === null ? {} : { height, style: { height: `${height}px` } }),
|
|
32
66
|
};
|
|
33
67
|
}
|
|
68
|
+
const UNSIZED = { width: null, height: null, percent: null };
|
|
34
69
|
export function readImageAlt(alt) {
|
|
35
70
|
const at = alt.lastIndexOf('|');
|
|
36
71
|
if (at === -1)
|
|
37
|
-
return { description: alt,
|
|
72
|
+
return { description: alt, ...UNSIZED };
|
|
38
73
|
const measured = readDimensions(alt.slice(at + 1));
|
|
39
74
|
if (!measured)
|
|
40
|
-
return { description: alt,
|
|
41
|
-
return { description: alt.slice(0, at).trim(),
|
|
75
|
+
return { description: alt, ...UNSIZED };
|
|
76
|
+
return { description: alt.slice(0, at).trim(), ...measured };
|
|
42
77
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { AttachAttributes, AttachmentRequest, AttachProgress, AttachRefusal
|
|
|
6
6
|
export { ENGLISH, type EditorWords, type NoteStrings, type NoteStringsInput } from './strings.js';
|
|
7
7
|
export { customPropertyOf, DEFAULT_THEME, defineTheme, THEME_PROPERTIES, THEME_SLOTS, type BlockquoteSlotProps, type CalloutSlotProps, type CodeBlockSlotProps, type DiagramSlotProps, type EmbedSlotProps, type FootnotesSlotProps, type HeadingSlotProps, type InlineCodeSlotProps, type ModeSwitchSlotProps, type AttachmentSlotProps, type PropertiesSlotProps, type Scheme, type TableSlotProps, type TaskBoxSlotProps, type Theme, type ThemeComponents, type ThemeProperties, type WikilinkSlotProps, } from './theme.js';
|
|
8
8
|
export { followable } from './address.js';
|
|
9
|
-
export { readDimensions, readImageAlt, type ImageAlt } from './image-dimensions.js';
|
|
9
|
+
export { readDimensions, readImageAlt, writeDimensions, type Dimensions, type ImageAlt } from './image-dimensions.js';
|
|
10
10
|
export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, type SplitDocument, } from './markdown.js';
|
|
11
11
|
export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
|
|
12
12
|
export { readProperties, type NoteProperties, type PropertyEntry, type PropertyShape } from './properties.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { isAttachRefusal } from './host.js';
|
|
|
4
4
|
export { ENGLISH } from './strings.js';
|
|
5
5
|
export { customPropertyOf, DEFAULT_THEME, defineTheme, THEME_PROPERTIES, THEME_SLOTS, } from './theme.js';
|
|
6
6
|
export { followable } from './address.js';
|
|
7
|
-
export { readDimensions, readImageAlt } from './image-dimensions.js';
|
|
7
|
+
export { readDimensions, readImageAlt, writeDimensions } from './image-dimensions.js';
|
|
8
8
|
export { codeRegions, fenceRegions, headingKey, remoteImageHosts, resolveWikilinks, splitFrontmatter, tableRegions, toUnixNewlines, } from './markdown.js';
|
|
9
9
|
export { ordinalAt, taskBoxes, taskCheckedAt, toggleTaskAt } from './tasklist.js';
|
|
10
10
|
export { readProperties } from './properties.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An image whose destination names no host is a file the notebook keeps (`SPEC.md` §5.8, #104):
|
|
3
|
+
* `` is the kept file `curva-2026.png`, drawn at 300 pixels,
|
|
4
|
+
* and never a request to a path under the page. It is the form a vault exported from a desktop
|
|
5
|
+
* editor writes, so every imported note with pictures has it.
|
|
6
|
+
*
|
|
7
|
+
* The page once handed such a destination to `<img>` as it was, and the browser asked for it
|
|
8
|
+
* relative to the page: a 404, and nothing drawn, while `![[curva-2026.png]]` beside it drew the file.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The name of the kept file an image's destination names, or `null` when it names a host or has a
|
|
12
|
+
* scheme — the remote image of §3.14 and §7.10, or an address the page refuses.
|
|
13
|
+
*
|
|
14
|
+
* The name is the last segment of the path, `/` and `\` both separating, after the query and the
|
|
15
|
+
* fragment are dropped, percent-decoded — an escape that is not one stays as written — and in NFC.
|
|
16
|
+
* Nothing else is folded: the comparison is case-exact, and no extension is removed, because an
|
|
17
|
+
* attachment's extension is part of its name. The destination may arrive as the author wrote it or
|
|
18
|
+
* as the parser normalised it, with its spaces and backslashes encoded; both name the same file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function keptImageName(destination: string): string | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An image whose destination names no host is a file the notebook keeps (`SPEC.md` §5.8, #104):
|
|
3
|
+
* `` is the kept file `curva-2026.png`, drawn at 300 pixels,
|
|
4
|
+
* and never a request to a path under the page. It is the form a vault exported from a desktop
|
|
5
|
+
* editor writes, so every imported note with pictures has it.
|
|
6
|
+
*
|
|
7
|
+
* The page once handed such a destination to `<img>` as it was, and the browser asked for it
|
|
8
|
+
* relative to the page: a 404, and nothing drawn, while `![[curva-2026.png]]` beside it drew the file.
|
|
9
|
+
*/
|
|
10
|
+
/** Anything of the shape `word:`, which is what makes an address absolute (§5.2, step 1). */
|
|
11
|
+
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
12
|
+
/**
|
|
13
|
+
* The name of the kept file an image's destination names, or `null` when it names a host or has a
|
|
14
|
+
* scheme — the remote image of §3.14 and §7.10, or an address the page refuses.
|
|
15
|
+
*
|
|
16
|
+
* The name is the last segment of the path, `/` and `\` both separating, after the query and the
|
|
17
|
+
* fragment are dropped, percent-decoded — an escape that is not one stays as written — and in NFC.
|
|
18
|
+
* Nothing else is folded: the comparison is case-exact, and no extension is removed, because an
|
|
19
|
+
* attachment's extension is part of its name. The destination may arrive as the author wrote it or
|
|
20
|
+
* as the parser normalised it, with its spaces and backslashes encoded; both name the same file.
|
|
21
|
+
*/
|
|
22
|
+
export function keptImageName(destination) {
|
|
23
|
+
const trimmed = destination.trim();
|
|
24
|
+
if (trimmed === '' || HAS_SCHEME.test(trimmed) || trimmed.startsWith('//'))
|
|
25
|
+
return null;
|
|
26
|
+
const path = trimmed.split(/[?#]/, 1)[0] ?? '';
|
|
27
|
+
// A backslash the parser encoded separates segments as one written does.
|
|
28
|
+
const segments = path.replace(/%5c/gi, '\\').split(/[/\\]/);
|
|
29
|
+
return decodeLeniently(segments[segments.length - 1] ?? '').normalize('NFC');
|
|
30
|
+
}
|
|
31
|
+
/** Percent-decodes every escape that is one, and leaves the rest as written (§5.2, step 5). */
|
|
32
|
+
function decodeLeniently(text) {
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// A run of escapes that is not UTF-8, or a `%` that opens nothing: each run is decoded on its own.
|
|
38
|
+
return text.replace(/(?:%[0-9a-f]{2})+/gi, (run) => {
|
|
39
|
+
try {
|
|
40
|
+
return decodeURIComponent(run);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return run;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/markdown.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The text helpers of the reading surface: what a note becomes before the parser reads it.
|
|
2
2
|
// Nothing here rewrites the note a host keeps; each function returns a new string for display.
|
|
3
|
-
import { readDimensions } from './image-dimensions.js';
|
|
3
|
+
import { readDimensions, writeDimensions } from './image-dimensions.js';
|
|
4
4
|
/**
|
|
5
5
|
* A note is bytes the notebook wrote, and a notebook written on Windows, or in a
|
|
6
6
|
* desktop editor, or pasted from anywhere, carries CRLF. Every reader in this
|
|
@@ -351,9 +351,7 @@ function resolveWikilinksIn(body, resolve, keeps) {
|
|
|
351
351
|
// Dropping it here is what made `![[picture|120]]` render at full size:
|
|
352
352
|
// the notation was read and then thrown away one line before it was used.
|
|
353
353
|
const measured = label === undefined ? null : readDimensions(label);
|
|
354
|
-
const sized = measured
|
|
355
|
-
? `|${measured.width}${measured.height === null ? '' : `x${measured.height}`}`
|
|
356
|
-
: '';
|
|
354
|
+
const sized = measured ? `|${writeDimensions(measured)}` : '';
|
|
357
355
|
return `[${text}](attachment:${encodeURIComponent(clean)}${sized})`;
|
|
358
356
|
}
|
|
359
357
|
const url = resolve(clean.normalize('NFC'));
|
package/dist/mode-icons.d.ts
CHANGED
|
@@ -5,3 +5,6 @@ export declare const ICON_COPY: import("react").JSX.Element;
|
|
|
5
5
|
export declare const ICON_CHECK: import("react").JSX.Element;
|
|
6
6
|
export declare const ICON_CLOSE: import("react").JSX.Element;
|
|
7
7
|
export declare const ICON_OWN_SIZE: import("react").JSX.Element;
|
|
8
|
+
export declare const ICON_RATIO_LOCKED: import("react").JSX.Element;
|
|
9
|
+
export declare const ICON_RATIO_FREE: import("react").JSX.Element;
|
|
10
|
+
export declare const ICON_RESIZE: import("react").JSX.Element;
|
package/dist/mode-icons.js
CHANGED
|
@@ -9,5 +9,10 @@ export const ICON_MARKDOWN = svg('M10 12.5 8 15l2 2.5', 'm14 12.5 2 2.5-2 2.5',
|
|
|
9
9
|
export const ICON_COPY = svg('M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2z', 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2');
|
|
10
10
|
export const ICON_CHECK = svg('M20 6 9 17l-5-5');
|
|
11
11
|
export const ICON_CLOSE = svg('M18 6 6 18', 'm6 6 12 12');
|
|
12
|
-
// The
|
|
13
|
-
|
|
12
|
+
// The image's controls (#106), Lucide's (ISC): its own size again, rotate-ccw, an undo of the
|
|
13
|
+
// size and not a way out of the page; the lock between width and height, link-2 and unlink-2;
|
|
14
|
+
// and the handle's diagonal arrows, move-diagonal-2.
|
|
15
|
+
export const ICON_OWN_SIZE = svg('M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8', 'M3 3v5h5');
|
|
16
|
+
export const ICON_RATIO_LOCKED = svg('M9 17H7A5 5 0 0 1 7 7h2', 'M15 7h2a5 5 0 1 1 0 10h-2', 'M8 12h8');
|
|
17
|
+
export const ICON_RATIO_FREE = svg('M9 17H7A5 5 0 0 1 7 7', 'M15 7h2a5 5 0 0 1 4 8', 'M8 12h1', 'M2 2l20 20');
|
|
18
|
+
export const ICON_RESIZE = svg('M19 13v6h-6', 'M5 11V5h6', 'm5 5 14 14');
|
package/dist/strings.d.ts
CHANGED
|
@@ -158,12 +158,27 @@ export interface EditorWords {
|
|
|
158
158
|
readonly addItem: string;
|
|
159
159
|
readonly removeItem: string;
|
|
160
160
|
readonly calloutType: string;
|
|
161
|
-
/** The field of an image's width in edit mode, in
|
|
161
|
+
/** The field of an image's width in edit mode, in the unit beside it (#98, #106). */
|
|
162
162
|
readonly imageWidth: string;
|
|
163
|
+
/** The field of an image's height in edit mode, in pixels (#106). */
|
|
164
|
+
readonly imageHeight: string;
|
|
165
|
+
/** The choice of the width's unit: pixels, or a share of the page (#108). */
|
|
166
|
+
readonly imageUnit: string;
|
|
167
|
+
/** The field of a Markdown image's description, the text before its pipe (#106). */
|
|
168
|
+
readonly imageDescription: string;
|
|
169
|
+
/** The lock between the width and the height, locked: the height follows the width (#106). */
|
|
170
|
+
readonly imageKeepRatio: string;
|
|
171
|
+
/** The same lock, open: the width and the height are written apart (#106). */
|
|
172
|
+
readonly imageFreeRatio: string;
|
|
163
173
|
/** The button that draws an image at its own size again (#98). */
|
|
164
174
|
readonly imageOwnSize: string;
|
|
165
175
|
/** The handle at an image's corner that drags its size (#98). */
|
|
166
176
|
readonly imageResize: string;
|
|
177
|
+
/** What the handle does while the proportion is kept, and while it is not (#106). */
|
|
178
|
+
readonly imageResizeKeep: string;
|
|
179
|
+
readonly imageResizeFree: string;
|
|
180
|
+
/** Why a size typed was not written: the range it has to be in, in the unit of its field (#105). */
|
|
181
|
+
readonly imageOutOfRange: (min: number, max: number, unit: string) => string;
|
|
167
182
|
readonly fold: string;
|
|
168
183
|
readonly foldNone: string;
|
|
169
184
|
readonly foldOpen: string;
|
package/dist/strings.js
CHANGED
|
@@ -156,9 +156,17 @@ export const ENGLISH = {
|
|
|
156
156
|
addItem: 'Add',
|
|
157
157
|
removeItem: 'Remove',
|
|
158
158
|
calloutType: 'Type',
|
|
159
|
-
imageWidth: 'Width
|
|
159
|
+
imageWidth: 'Width',
|
|
160
|
+
imageHeight: 'Height',
|
|
161
|
+
imageUnit: 'Unit',
|
|
162
|
+
imageDescription: 'Description',
|
|
163
|
+
imageKeepRatio: 'Keep the proportion',
|
|
164
|
+
imageFreeRatio: 'Width and height apart',
|
|
160
165
|
imageOwnSize: 'Its own size',
|
|
161
166
|
imageResize: 'Drag to resize',
|
|
167
|
+
imageResizeKeep: 'Drag to resize (keeps the proportion)',
|
|
168
|
+
imageResizeFree: 'Drag to resize (free)',
|
|
169
|
+
imageOutOfRange: (min, max, unit) => `Between ${min} and ${max} ${unit}`,
|
|
162
170
|
fold: 'Folding',
|
|
163
171
|
foldNone: 'Not foldable',
|
|
164
172
|
foldOpen: 'Folds, open',
|