@memorysmithapp/markdown-renderer 0.7.1 → 0.8.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 CHANGED
@@ -18,7 +18,7 @@ import 'katex/dist/katex.min.css';
18
18
  <NoteBody
19
19
  markdown={note} // the note as you keep it, frontmatter included
20
20
  resolve={(name) => addressOfTheOneNoteNamed(name)} // or null: the link is pending
21
- keeps={(name) => notebookKeepsAFile(name)} // only an embed reaches a file
21
+ keeps={(name) => notebookKeepsAFile(name)} // reached by an embed, and by an image with no host: ![](../assets/x.png)
22
22
  fileOf={(name) => describe(name)} // { mimeType, bytes, url, description }: drawn by its type, as MemorySmith does
23
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
@@ -1,4 +1,4 @@
1
- import { type AnchorHTMLAttributes, type ReactNode } from 'react';
1
+ import { type AnchorHTMLAttributes, type ImgHTMLAttributes, type ReactNode } from 'react';
2
2
  import { type FootnoteRef } from './footnotes.js';
3
3
  import type { TaskSave } from './task-saves.js';
4
4
  /**
@@ -40,6 +40,17 @@ export interface MarkdownProps {
40
40
  type WithNode<T> = T & {
41
41
  node?: unknown;
42
42
  };
43
+ /**
44
+ * An image, with the dimensions the specification puts in its alt text
45
+ * (§3.14). What precedes the pipe is the description and is never dropped;
46
+ * what follows it is width, or width and height, in CSS pixels, or a width as
47
+ * a share of the page at most (#108), and never appears as text. A value that is neither stays part of the description,
48
+ * because deleting an accessibility label is the worse failure.
49
+ *
50
+ * A destination that names no host is a file the notebook keeps, by the last segment of its path
51
+ * (§5.8, #104): it is drawn as the embed of that file is, and never asked of a path under the page.
52
+ */
53
+ export declare function MarkdownImage({ alt, node: _node, ...rest }: WithNode<ImgHTMLAttributes<HTMLImageElement>>): import("react").JSX.Element;
43
54
  export declare function MarkdownAnchor({ href, children, node: _node, ...rest }: WithNode<AnchorHTMLAttributes<HTMLAnchorElement>>): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
44
55
  export declare function Markdown({ children, source, onToggleTask, taskSave, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
45
56
  export {};
package/dist/Markdown.js CHANGED
@@ -7,9 +7,10 @@ import rehypeKatex from 'rehype-katex';
7
7
  import { followable } from './address.js';
8
8
  import { CopyButton } from './CopyButton.js';
9
9
  import { fileAddress, fileKind, formatBytes, opensInBrowser, rendersAs } from './file-types.js';
10
+ import { keptImageName } from './kept-image.js';
10
11
  import { highlightLanguage, highlightTree, languageToLoad, loadGrammar } from './highlight.js';
11
12
  import { useHost } from './host.js';
12
- import { readDimensions, readImageAlt } from './image-dimensions.js';
13
+ import { readDimensions, readImageAlt, sizedAttributes } from './image-dimensions.js';
13
14
  import { choiceTargetOf, LinkChoice } from './LinkChoice.js';
14
15
  import { toUnixNewlines } from './markdown.js';
15
16
  import { MermaidDiagram } from './MermaidDiagram.js';
@@ -20,13 +21,21 @@ import { ordinalAt } from './tasklist.js';
20
21
  /**
21
22
  * An image, with the dimensions the specification puts in its alt text
22
23
  * (§3.14). What precedes the pipe is the description and is never dropped;
23
- * what follows it is width, or width and height, in CSS pixels, and never
24
- * appears as text. A value that is neither stays part of the description,
24
+ * what follows it is width, or width and height, in CSS pixels, or a width as
25
+ * a share of the page at most (#108), and never appears as text. A value that is neither stays part of the description,
25
26
  * because deleting an accessibility label is the worse failure.
27
+ *
28
+ * A destination that names no host is a file the notebook keeps, by the last segment of its path
29
+ * (§5.8, #104): it is drawn as the embed of that file is, and never asked of a path under the page.
26
30
  */
27
- function MarkdownImage({ alt, node: _node, ...rest }) {
31
+ export function MarkdownImage({ alt, node: _node, ...rest }) {
28
32
  const read = readImageAlt(alt ?? '');
29
- return (_jsx("img", { ...rest, alt: read.description, ...(read.width === null ? {} : { width: read.width }), ...(read.height === null ? {} : { height: read.height }) }));
33
+ const name = typeof rest.src === 'string' ? keptImageName(rest.src) : null;
34
+ if (name !== null)
35
+ return _jsx(Attachment, { name: name, size: read, description: read.description, title: rest.title });
36
+ // The description is the hover hint too, so a person who sees the picture can read it (#109); a title written wins.
37
+ const title = rest.title || read.description || undefined;
38
+ return _jsx("img", { ...rest, alt: read.description, title: title, ...sizedAttributes(read) });
30
39
  }
31
40
  /**
32
41
  * A link to a place in the same note: a footnote and its way back (§7.12),
@@ -56,26 +65,19 @@ function InPageAnchor({ href, children, ...rest }) {
56
65
  };
57
66
  return (_jsx("a", { href: href, onClick: follow, ...rest, children: children }));
58
67
  }
59
- /**
60
- * A file the notebook keeps, where a note embedded it, drawn by its type — an
61
- * image, audio, video, or a card — by the host's `renderAttachment`, else from
62
- * what its `fileOf` says (#77). A name the notebook keeps nothing under is
63
- * reported the way a pending link is: a notebook is read most while it is
64
- * being written (§5.8).
65
- */
66
- function Attachment({ name, width, height }) {
68
+ function Attachment({ name, size, ...written }) {
67
69
  const host = useHost();
68
70
  if (!host.keeps(name))
69
- return _jsx(AttachmentMissing, { name: name });
71
+ return _jsx(AttachmentMissing, { name: name, label: written.description });
70
72
  if (host.renderAttachment)
71
- return _jsx(_Fragment, { children: host.renderAttachment({ name, width, height }) });
73
+ return _jsx(_Fragment, { children: host.renderAttachment({ name, ...size, ...written }) });
72
74
  if (!host.fileOf)
73
- return _jsx("span", { className: "attachment-loading", children: name });
74
- return _jsx(KeptAttachment, { name: name, width: width, height: height });
75
+ return _jsx("span", { className: "attachment-loading", children: written.description || name });
76
+ return _jsx(KeptAttachment, { name: name, size: size, ...written });
75
77
  }
76
- function AttachmentMissing({ name }) {
78
+ function AttachmentMissing({ name, label }) {
77
79
  const host = useHost();
78
- return (_jsx("span", { className: "attachment-missing", title: host.strings.attachmentMissing, children: name }));
80
+ return (_jsx("span", { className: "attachment-missing", title: host.strings.attachmentMissing, "data-name": label ? name : undefined, children: label || name }));
79
81
  }
80
82
  function isThenable(value) {
81
83
  return typeof value === 'object' && value !== null && typeof value.then === 'function';
@@ -129,30 +131,36 @@ function openInTab(event, url) {
129
131
  * included — a card with what it is, its size, and the ways out of the page: Open only for what a
130
132
  * browser shows, Download always. A theme may draw it through the `attachment` slot.
131
133
  */
132
- function KeptAttachment({ name, width, height }) {
134
+ function KeptAttachment({ name, size: declared, ...written }) {
133
135
  const host = useHost();
134
136
  const slot = useSlot('attachment');
135
137
  const described = useKeptFile(name);
136
138
  if (described === undefined)
137
- return _jsx("span", { className: "attachment-loading", children: name });
139
+ return _jsx("span", { className: "attachment-loading", children: written.description || name });
138
140
  const url = described ? fileAddress(described.url) : '';
139
141
  if (!described || url === '')
140
- return _jsx(AttachmentMissing, { name: name });
141
- const file = { ...described, url, downloadUrl: fileAddress(described.downloadUrl ?? described.url) || url };
142
+ return _jsx(AttachmentMissing, { name: name, label: written.description });
143
+ // What the note wrote of the file is what the reader gets of it; the file's own when it wrote nothing.
144
+ const description = written.description || described.description;
145
+ const file = {
146
+ ...described,
147
+ url,
148
+ downloadUrl: fileAddress(described.downloadUrl ?? described.url) || url,
149
+ ...(description ? { description } : {}),
150
+ };
142
151
  const shape = rendersAs(file.mimeType);
143
152
  const kind = fileKind(file.mimeType, host.strings.fileKinds);
144
153
  const size = formatBytes(file.bytes, host.strings.locale);
145
154
  const opens = opensInBrowser(file.mimeType);
146
155
  if (slot)
147
- return _jsx(_Fragment, { children: drawSlot(slot, { name, file, shape, kind, size, opens, width, height }) });
148
- // The attributes reserve the box before the bytes arrive; a height is a style too, since the
149
- // surface gives an image `height: auto`, which two numbers overrule (§3.14).
150
- const sized = {
151
- ...(width === null ? {} : { width }),
152
- ...(height === null ? {} : { height, style: { height: `${height}px` } }),
153
- };
154
- if (shape === 'image')
155
- return _jsx("img", { className: "attachment-image", src: file.url, alt: file.description || name, ...sized });
156
+ return _jsx(_Fragment, { children: drawSlot(slot, { name, file, shape, kind, size, opens, ...declared, title: written.title ?? null }) });
157
+ const sized = sizedAttributes(declared);
158
+ if (shape === 'image') {
159
+ // Described, its description is also the hover hint, a title written winning (#109); a picture
160
+ // with none takes its name as its `alt` and no hint, which would only say the name again.
161
+ const title = written.title || file.description;
162
+ return _jsx("img", { className: "attachment-image", src: file.url, alt: file.description || name, ...(title ? { title } : {}), ...sized });
163
+ }
156
164
  if (shape === 'audio')
157
165
  return _jsx("audio", { className: "attachment-player", controls: true, src: file.url });
158
166
  if (shape === 'video')
@@ -178,7 +186,7 @@ export function MarkdownAnchor({ href, children, node: _node, ...rest }) {
178
186
  const at = address.lastIndexOf('|');
179
187
  const measured = at === -1 ? null : readDimensions(address.slice(at + 1));
180
188
  const name = measured ? address.slice(0, at) : address;
181
- return _jsx(Attachment, { name: name, width: measured?.width ?? null, height: measured?.height ?? null });
189
+ return _jsx(Attachment, { name: name, size: measured ?? { width: null, height: null, percent: null } });
182
190
  }
183
191
  if (href?.startsWith('pending:')) {
184
192
  const pending = (label) => (_jsx("span", { className: "wikilink-pending", title: host.strings.pendingLink, children: label }));
package/dist/diagnose.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * note, or beside an editor. KaTeX checks a formula as `rehype-katex` would typeset it, and Mermaid
12
12
  * reads a diagram as the page would draw it, loaded only when a note carries one.
13
13
  */
14
+ import { keptImageName } from './kept-image.js';
14
15
  import katex from 'katex';
15
16
  import remarkParse from 'remark-parse';
16
17
  import { unified } from 'unified';
@@ -36,7 +37,7 @@ export const NOTICES = {
36
37
  'raw-html': { severity: 'notice', section: '7.9', message: 'Raw HTML is drawn as text, tags and all.' },
37
38
  'strikethrough-single-tilde': { severity: 'notice', section: '4.3', message: 'One tilde strikes nothing: the tildes stay on the page. Strikethrough is ~~two~~.' },
38
39
  'link-pending': { severity: 'hint', section: '7.4', message: 'No note answers this link yet, so it is drawn as pending.' },
39
- 'embed-pending': { severity: 'hint', section: '7.3', message: 'No note and no kept file answers this embed, so it is drawn as a pending link.' },
40
+ 'embed-pending': { severity: 'hint', section: '7.3', message: 'No note and no kept file answers this embed, or no kept file this image, so it is drawn as pending.' },
40
41
  };
41
42
  /** A footnote reference left as text, which is one with no definition (§7.12). */
42
43
  const UNDEFINED_REFERENCE = /\[\^[^\]\s]+\]/g;
@@ -106,6 +107,7 @@ export async function diagnose(markdown, options = {}) {
106
107
  const diagrams = [];
107
108
  const referenced = new Set();
108
109
  const definitions = [];
110
+ const images = [];
109
111
  walk(tree, (node) => {
110
112
  const at = span(node);
111
113
  if (!at)
@@ -136,6 +138,11 @@ export async function diagnose(markdown, options = {}) {
136
138
  else if (node.type === 'footnoteDefinition') {
137
139
  definitions.push(node);
138
140
  }
141
+ else if (node.type === 'image') {
142
+ const name = keptImageName(node.url ?? '');
143
+ if (name !== null)
144
+ images.push({ name, at });
145
+ }
139
146
  });
140
147
  for (const definition of definitions) {
141
148
  const at = span(definition);
@@ -164,6 +171,13 @@ export async function diagnose(markdown, options = {}) {
164
171
  }
165
172
  });
166
173
  pending(hidden, split, options, report);
174
+ // An image whose destination names no host names a kept file, and only a kept file (§5.8, #104).
175
+ if (options.resolve || options.reaches) {
176
+ const keeps = options.keeps ?? (() => false);
177
+ for (const image of images)
178
+ if (!keeps(image.name))
179
+ report('embed-pending', image.at[0], image.at[1], image.name);
180
+ }
167
181
  if (diagrams.length > 0 && options.diagrams !== false) {
168
182
  const mermaid = (await import('mermaid')).default;
169
183
  for (const diagram of diagrams) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The editor's own menu on a right-click in edit mode (#81): what the toolbar formats and inserts,
3
- * where the author is, with the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
3
+ * where the author is, after the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
4
4
  * kept inside the window, and closed by Escape, a click outside it, a scroll, or an entry run.
5
5
  */
6
6
  import { type ReactNode } from 'react';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The editor's own menu on a right-click in edit mode (#81): what the toolbar formats and inserts,
4
- * where the author is, with the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
4
+ * where the author is, after the clipboard's three verbs. It is a `role="menu"` reached by keyboard,
5
5
  * kept inside the window, and closed by Escape, a click outside it, a scroll, or an entry run.
6
6
  */
7
7
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
@@ -261,7 +261,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
261
261
  /** Where the editing menu is open, in the window's coordinates (#81). */
262
262
  const [menu, setMenu] = useState(null);
263
263
  const openMenu = useRef((x, y) => setMenu({ x, y }));
264
- /** The menu's groups, from the same commands as the toolbar: Format with a selection, Insert, Edit. */
264
+ /** The menu's groups, from the same commands as the toolbar: Edit first (#110), Format with a selection, Insert. */
265
265
  const menuGroups = (target) => {
266
266
  const words = resolved.strings.editor;
267
267
  const item = (name, action, shortcut) => ({
@@ -308,7 +308,8 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
308
308
  { name: 'attach', label: words.attach, icon: ICONS.attach, run: () => setAttachOpen(true) },
309
309
  ],
310
310
  });
311
- groups.push({
311
+ // The clipboard's verbs open the menu, as they open every other one a person knows (#110).
312
+ groups.unshift({
312
313
  label: words.menuEdit,
313
314
  items: [
314
315
  {
@@ -29,12 +29,28 @@ export declare function CalloutBlock({ view, text, marker }: {
29
29
  fold: '+' | '-' | null;
30
30
  };
31
31
  }): import("react").JSX.Element;
32
+ /** The narrowest an image is sized to, by a field or the handle: something a reader still sees (#105). */
33
+ export declare const MIN_IMAGE_SIZE = 16;
32
34
  /**
33
- * An image drawn in edit mode with the size it is drawn at set in place (#98): a width in pixels,
34
- * its height following, a handle at its corner that drags it, and a button back to its own size.
35
- * Each writes the dimensions of its notation — `![description|300](…)` or `![[photo.jpg|300]]` —
36
- * and no other byte. The controls appear once an image is drawn inside it, so an embed of a file
37
- * that is not an image, or a host's own drawing without one, offers none.
35
+ * An image drawn in edit mode with what its notation writes set in place (#98, #106): its width and
36
+ * its height with a lock between them — locked, the height follows and only the width is written,
37
+ * `|300`; open, both are, `|300x180` — the width's unit, pixels or a share of the page (`|50%`,
38
+ * #108), its description for a Markdown image, a button back to its own size, and a handle at its
39
+ * corner that drags it, keeping the proportion or, with the lock open or Shift held, not. Each
40
+ * writes the dimensions or the description of its notation, and no other byte.
41
+ *
42
+ * A field selects what it holds when it takes the focus, so typing replaces it, and writes on Enter
43
+ * or when it is left; Escape leaves the note as it is. A value outside the range a reader can see
44
+ * — from 16 pixels to the column or the image's own width, the wider — is refused where it was
45
+ * typed, never brought into the range and written (#105).
46
+ *
47
+ * The controls stand under the picture, never over it, and nothing that starts on them or in the
48
+ * moment after reaches the click that shows the image's source (#107): they are all marked as
49
+ * controls, gaps included, and the frame is marked busy while one of them has the focus, while the
50
+ * handle is dragged and for a moment after a pointer or a touch on them.
51
+ *
52
+ * The controls appear once an image is drawn inside it, so an embed of a file that is not an image,
53
+ * or a host's own drawing without one, offers none.
38
54
  */
39
55
  export declare function ImageBlock({ view, source, children }: {
40
56
  view: EditorView;