@memorysmithapp/markdown-renderer 0.3.2 → 0.5.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 +50 -4
- package/dist/Markdown.d.ts +6 -2
- package/dist/Markdown.js +126 -17
- package/dist/NoteActions.d.ts +9 -0
- package/dist/NoteActions.js +68 -0
- package/dist/NoteBody.d.ts +32 -2
- package/dist/NoteBody.js +74 -7
- package/dist/edit/AttachDialog.d.ts +23 -0
- package/dist/edit/AttachDialog.js +126 -0
- package/dist/edit/ContextMenu.d.ts +29 -0
- package/dist/edit/ContextMenu.js +95 -0
- package/dist/edit/LanguageControl.d.ts +19 -0
- package/dist/edit/LanguageControl.js +124 -0
- package/dist/edit/NoteEditor.d.ts +44 -0
- package/dist/edit/NoteEditor.js +520 -0
- package/dist/edit/SaveNoticeDialog.d.ts +15 -0
- package/dist/edit/SaveNoticeDialog.js +41 -0
- package/dist/edit/analyze.d.ts +150 -0
- package/dist/edit/analyze.js +268 -0
- package/dist/edit/blocks.d.ts +97 -0
- package/dist/edit/blocks.js +481 -0
- package/dist/edit/commands.d.ts +78 -0
- package/dist/edit/commands.js +257 -0
- package/dist/edit/frontmatter.d.ts +45 -0
- package/dist/edit/frontmatter.js +142 -0
- package/dist/edit/index.d.ts +5 -0
- package/dist/edit/index.js +7 -0
- package/dist/edit/live.d.ts +28 -0
- package/dist/edit/live.js +675 -0
- package/dist/edit/saves.d.ts +48 -0
- package/dist/edit/saves.js +73 -0
- package/dist/edit/table.d.ts +58 -0
- package/dist/edit/table.js +185 -0
- package/dist/file-types.d.ts +30 -0
- package/dist/file-types.js +70 -0
- package/dist/highlight.d.ts +21 -7
- package/dist/highlight.js +56 -7
- package/dist/host.d.ts +82 -3
- package/dist/host.js +16 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +1 -0
- package/dist/mode-icons.d.ts +6 -0
- package/dist/mode-icons.js +11 -0
- package/dist/prism-grammars.d.ts +18 -0
- package/dist/prism-grammars.js +305 -0
- package/dist/remark-callouts.d.ts +6 -0
- package/dist/remark-callouts.js +2 -2
- package/dist/remark-ring.d.ts +5 -0
- package/dist/remark-ring.js +25 -15
- package/dist/slots.js +17 -1
- package/dist/strings.d.ts +129 -1
- package/dist/strings.js +127 -1
- package/dist/task-saves.d.ts +35 -0
- package/dist/task-saves.js +86 -0
- package/dist/theme.d.ts +47 -0
- package/dist/theme.js +2 -2
- package/package.json +14 -2
- package/structure.css +355 -0
- package/theme-default.css +1099 -0
package/README.md
CHANGED
|
@@ -19,17 +19,17 @@ import 'katex/dist/katex.min.css';
|
|
|
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
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
23
|
renderLink={({ href, className, children }) => <RouterLink to={href} className={className}>{children}</RouterLink>}
|
|
24
24
|
loadNote={(name) => fetchTheBodyOf(name)} // for an embedded note; the package never fetches
|
|
25
25
|
writable
|
|
26
|
-
onToggleTask={(ordinal) => save(toggleTaskAt(
|
|
26
|
+
onToggleTask={(ordinal) => save(toggleTaskAt(latest(), ordinal))} // a promise locks the item under "Updating…" until it settles
|
|
27
27
|
theme="dark"
|
|
28
28
|
strings={{ footnotes: 'Notas' }} // every word left out is MemorySmith's English
|
|
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. 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. 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
|
|
|
@@ -64,7 +64,7 @@ const acme = defineTheme({
|
|
|
64
64
|
|
|
65
65
|
- **Properties** name every value the stylesheet reads — colours, faces, sizes, spacing, headings, quotations, tables, callout hues and icons — and are written on the root as `--markdown-*`, so your CSS may set them too.
|
|
66
66
|
- **Your CSS** is scoped by `.markdown-theme-<name>`, and reaches the note and nothing else.
|
|
67
|
-
- **Slots** — `callout`, `calloutIcon`, `table`, `codeBlock`, `inlineCode`, `heading`, `blockquote`, `embed`, `footnotes`, `taskBox`, `wikilink`, `diagram`, `properties` — are called with the notation already read. A slot decides how a block looks, never what it means: the links it draws go where the host resolved them, a task box reports through the `toggle` it is given, and no raw HTML ever reaches it.
|
|
67
|
+
- **Slots** — `callout`, `calloutIcon`, `table`, `codeBlock`, `inlineCode`, `heading`, `blockquote`, `embed`, `footnotes`, `taskBox`, `wikilink`, `diagram`, `properties`, `modeSwitch` — are called with the notation already read. A slot decides how a block looks, never what it means: the links it draws go where the host resolved them, a task box reports through the `toggle` it is given, and no raw HTML ever reaches it.
|
|
68
68
|
|
|
69
69
|
The frontmatter is the note's too. The default theme draws it as MemorySmith does: `name` as the title, and the other entries in a block that folds, each key with the icon of its value's shape, lists as chips and dates in `strings.locale`, with a `[[wikilink]]`, a Markdown link or a web address in a value drawn as a link. A theme decides otherwise through the `properties` slot, which may draw nothing, as a host that shows properties elsewhere does:
|
|
70
70
|
|
|
@@ -74,6 +74,52 @@ const mine = defineTheme({ name: 'mine', components: { properties: () => null }
|
|
|
74
74
|
|
|
75
75
|
Import `structure.css` instead of `styles.css` for a theme that draws everything itself. A theme names faces; your page loads them.
|
|
76
76
|
|
|
77
|
+
## Edit
|
|
78
|
+
|
|
79
|
+
A note the host makes `editable` carries a button at its top right that switches between reading and editing it on the same page. The author edits what they see: marks, headings, lists, tasks, links and the names of notes after `[[`, callouts, tables cell by cell, code, formulas and diagrams with their drawing live, the properties as a form, footnotes and comments. The note is the bytes you gave, and every change comes back whole through `onChange`, unchanged wherever the author did not touch it — the order and quoting of the frontmatter, a list's marker, a table's spacing, a comment, a line break. The package never saves. The toolbar grows out of the switch, which becomes its last control; its first tool undoes every change since editing began, and a right-click on the text opens a menu of the same tools where you are (Shift keeps the browser's); beside the switch, in both modes, Show Markdown opens the note's Markdown, as you have it or would receive it, in a dialog, and Copy Markdown copies it. Those two are reading, so a note that is not `editable` carries them too, without the switch; pass `actions={false}` to hide them, for a print, a preview or an embed.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<NoteBody
|
|
83
|
+
markdown={note}
|
|
84
|
+
editable // the switch, at the top right
|
|
85
|
+
onChange={(next) => save(next)} // after every change; return the save's promise
|
|
86
|
+
suggest={(typed) => namesOfNotesLike(typed)} // what [[ offers
|
|
87
|
+
attach={(file, { name, description }, progress) => keepInNotebook(file, name, description, progress)} // the name it is kept under
|
|
88
|
+
files={() => filesOfTheNotebook()} // what the attach dialog lists: { name, mimeType, bytes, url, description }
|
|
89
|
+
modeButton="icon" // or "label", the icon and its word
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
When `onChange` returns the promise of its save, the editor shows before the switch an icon for whether the last change is *Saving…*, *Saved* or *Not saved*, the last one's icon trying again when clicked, and going back to reading waits for the save, staying in edit when it failed. Return nothing and no status is shown. When there is something to tell the author, resolve the promise with a `SaveOutcome` (the save went through, and they have a choice to make) or reject it with a `SaveRefusal` (it did not, and why): each is a `title`, a `message` and the `actions` to choose from, in your words. The icon then opens a dialog with them; an action's promise is followed like a save, and the author can close the dialog and come back to it:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
onChange={async (next) => {
|
|
97
|
+
const saved = await save(next);
|
|
98
|
+
if (saved.conflict) throw { title: 'Somebody else wrote this note', message: 'Your edit was not saved.', actions: [
|
|
99
|
+
{ label: 'Discard my edit and reload', run: () => reload() },
|
|
100
|
+
{ label: 'Save anyway', run: () => save(next, { force: true }) },
|
|
101
|
+
] } satisfies SaveRefusal;
|
|
102
|
+
if (saved.renamed) return { title: 'Note renamed', message: `${saved.links} links reached the old name.`, actions: [
|
|
103
|
+
{ label: 'Keep the links broken', run: () => undefined },
|
|
104
|
+
{ label: 'Keep the old name as an alias', run: () => addAlias() },
|
|
105
|
+
] } satisfies SaveOutcome;
|
|
106
|
+
}}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
While `attach` keeps a file, call `progress({ message, fraction })` with each step in your words — *Sending part 12 of 40*, and `0.3` — and the paperclip shows it, a bar filling when you give the fraction. Reject with `{ message }` (an `AttachRefusal`, not an `Error`) to tell the author why the file was refused; the paperclip says it until they open it again. An `Error` keeps the paperclip's own *The file was not attached*.
|
|
110
|
+
|
|
111
|
+
Pass the saved note back as `markdown` whenever you like: a text the editor already moved past is not applied over what the author typed since.
|
|
112
|
+
|
|
113
|
+
`mode` and `onModeChange` let you control the mode yourself, and the words come from `strings` (`edit`, `view` and `editor`). The editor is CodeMirror, loaded the first time a note is edited, so a page that only draws never loads it. To edit without the switch, import it from its own entry point:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { NoteEditor } from '@memorysmithapp/markdown-renderer/edit';
|
|
117
|
+
|
|
118
|
+
<NoteEditor markdown={note} onChange={setNote} {...host} />;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A note that mixes line breaks (CRLF and LF) opens read-only, because the editor could not give it back byte for byte.
|
|
122
|
+
|
|
77
123
|
## Diagnose
|
|
78
124
|
|
|
79
125
|
Every note is valid, and nothing here refuses one. What `diagnose` tells a host is where the page will not show what was most likely typed, and what the note lacks that the specification reserves a place for: the notices of [`SPEC.md` §7.13](https://github.com/memorysmithapp/memorysmithapp-markdown/blob/main/packages/markdown-spec/SPEC.md#713-notices). It imports no React and needs no DOM, so it runs in an editor, on a server, or in a tool that writes a note.
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AnchorHTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { type FootnoteRef } from './footnotes.js';
|
|
3
|
+
import type { TaskSave } from './task-saves.js';
|
|
3
4
|
/**
|
|
4
5
|
* One run of a note, parsed and drawn: the pipeline of MemorySmith's reading page.
|
|
5
6
|
*
|
|
@@ -16,7 +17,10 @@ export interface MarkdownProps {
|
|
|
16
17
|
* the two disagree on how many there are.
|
|
17
18
|
*/
|
|
18
19
|
readonly source?: string;
|
|
19
|
-
|
|
20
|
+
/** A box of this run was ticked: its ordinal in the run, and the state it is being set to. */
|
|
21
|
+
readonly onToggleTask?: (ordinal: number, checked: boolean) => void;
|
|
22
|
+
/** Where the save of a box of this run stands, by its ordinal in the run (`task-saves.ts`). */
|
|
23
|
+
readonly taskSave?: (ordinal: number) => TaskSave | null;
|
|
20
24
|
readonly writable?: boolean;
|
|
21
25
|
/**
|
|
22
26
|
* How this run takes part in the footnotes of a note drawn in several runs
|
|
@@ -37,5 +41,5 @@ type WithNode<T> = T & {
|
|
|
37
41
|
node?: unknown;
|
|
38
42
|
};
|
|
39
43
|
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;
|
|
40
|
-
export declare function Markdown({ children, source, onToggleTask, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
|
|
44
|
+
export declare function Markdown({ children, source, onToggleTask, taskSave, writable, footnotes }: MarkdownProps): import("react").JSX.Element;
|
|
41
45
|
export {};
|
package/dist/Markdown.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
|
3
|
-
import { isValidElement, } from 'react';
|
|
3
|
+
import { isValidElement, useEffect, useMemo, useReducer, useState, } from 'react';
|
|
4
4
|
import { Fragment, jsx, jsxs } from 'react/jsx-runtime';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import rehypeKatex from 'rehype-katex';
|
|
7
7
|
import { followable } from './address.js';
|
|
8
8
|
import { CopyButton } from './CopyButton.js';
|
|
9
|
-
import {
|
|
9
|
+
import { fileAddress, fileKind, formatBytes, opensInBrowser, rendersAs } from './file-types.js';
|
|
10
|
+
import { highlightLanguage, highlightTree, languageToLoad, loadGrammar } from './highlight.js';
|
|
10
11
|
import { useHost } from './host.js';
|
|
11
12
|
import { readDimensions, readImageAlt } from './image-dimensions.js';
|
|
12
13
|
import { choiceTargetOf, LinkChoice } from './LinkChoice.js';
|
|
@@ -56,19 +57,107 @@ function InPageAnchor({ href, children, ...rest }) {
|
|
|
56
57
|
return (_jsx("a", { href: href, onClick: follow, ...rest, children: children }));
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
* A file the notebook keeps, where a note embedded it
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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).
|
|
63
65
|
*/
|
|
64
66
|
function Attachment({ name, width, height }) {
|
|
65
67
|
const host = useHost();
|
|
66
|
-
if (!host.keeps(name))
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (!host.keeps(name))
|
|
69
|
+
return _jsx(AttachmentMissing, { name: name });
|
|
70
|
+
if (host.renderAttachment)
|
|
71
|
+
return _jsx(_Fragment, { children: host.renderAttachment({ name, width, height }) });
|
|
72
|
+
if (!host.fileOf)
|
|
70
73
|
return _jsx("span", { className: "attachment-loading", children: name });
|
|
71
|
-
return _jsx(
|
|
74
|
+
return _jsx(KeptAttachment, { name: name, width: width, height: height });
|
|
75
|
+
}
|
|
76
|
+
function AttachmentMissing({ name }) {
|
|
77
|
+
const host = useHost();
|
|
78
|
+
return (_jsx("span", { className: "attachment-missing", title: host.strings.attachmentMissing, children: name }));
|
|
79
|
+
}
|
|
80
|
+
function isThenable(value) {
|
|
81
|
+
return typeof value === 'object' && value !== null && typeof value.then === 'function';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What the host's `fileOf` says of a name: the file, `null` for none, or `undefined` while a
|
|
85
|
+
* promise is on its way. An answer given at once is drawn at once, a server's render included.
|
|
86
|
+
*/
|
|
87
|
+
function useKeptFile(name) {
|
|
88
|
+
const { fileOf } = useHost();
|
|
89
|
+
const answer = useMemo(() => fileOf?.(name) ?? null, [fileOf, name]);
|
|
90
|
+
const [settled, setSettled] = useState(null);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!isThenable(answer))
|
|
93
|
+
return;
|
|
94
|
+
let live = true;
|
|
95
|
+
answer.then((file) => live && setSettled({ answer, file }), () => live && setSettled({ answer, file: null }));
|
|
96
|
+
return () => {
|
|
97
|
+
live = false;
|
|
98
|
+
};
|
|
99
|
+
}, [answer]);
|
|
100
|
+
if (!isThenable(answer))
|
|
101
|
+
return answer;
|
|
102
|
+
return settled?.answer === answer ? settled.file : undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Opens a file kept as `data:` in a new tab as the `blob:` of its bytes: a browser navigates no tab
|
|
106
|
+
* to a `data:` address, so the link alone would do nothing (#79). Any other address is the link's.
|
|
107
|
+
*/
|
|
108
|
+
function openInTab(event, url) {
|
|
109
|
+
if (!/^data:/i.test(url.trim()) || typeof URL.createObjectURL !== 'function')
|
|
110
|
+
return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
const tab = window.open('', '_blank');
|
|
113
|
+
void fetch(url)
|
|
114
|
+
.then((response) => response.blob())
|
|
115
|
+
.then((blob) => {
|
|
116
|
+
const address = URL.createObjectURL(blob);
|
|
117
|
+
if (tab)
|
|
118
|
+
tab.location.href = address;
|
|
119
|
+
else
|
|
120
|
+
window.open(address, '_blank', 'noreferrer');
|
|
121
|
+
// The tab has the bytes once it loaded them; the address is let go after a while.
|
|
122
|
+
setTimeout(() => URL.revokeObjectURL(address), 60_000);
|
|
123
|
+
})
|
|
124
|
+
.catch(() => tab?.close());
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A kept file drawn as MemorySmith's `Attachment` draws it (*observed* at `4b67c7e`): an image with
|
|
128
|
+
* the dimensions the embed declared, an audio or a video player, and everything else — the PDF
|
|
129
|
+
* included — a card with what it is, its size, and the ways out of the page: Open only for what a
|
|
130
|
+
* browser shows, Download always. A theme may draw it through the `attachment` slot.
|
|
131
|
+
*/
|
|
132
|
+
function KeptAttachment({ name, width, height }) {
|
|
133
|
+
const host = useHost();
|
|
134
|
+
const slot = useSlot('attachment');
|
|
135
|
+
const described = useKeptFile(name);
|
|
136
|
+
if (described === undefined)
|
|
137
|
+
return _jsx("span", { className: "attachment-loading", children: name });
|
|
138
|
+
const url = described ? fileAddress(described.url) : '';
|
|
139
|
+
if (!described || url === '')
|
|
140
|
+
return _jsx(AttachmentMissing, { name: name });
|
|
141
|
+
const file = { ...described, url, downloadUrl: fileAddress(described.downloadUrl ?? described.url) || url };
|
|
142
|
+
const shape = rendersAs(file.mimeType);
|
|
143
|
+
const kind = fileKind(file.mimeType, host.strings.fileKinds);
|
|
144
|
+
const size = formatBytes(file.bytes, host.strings.locale);
|
|
145
|
+
const opens = opensInBrowser(file.mimeType);
|
|
146
|
+
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
|
+
if (shape === 'audio')
|
|
157
|
+
return _jsx("audio", { className: "attachment-player", controls: true, src: file.url });
|
|
158
|
+
if (shape === 'video')
|
|
159
|
+
return _jsx("video", { className: "attachment-player", controls: true, src: file.url, ...sized });
|
|
160
|
+
return (_jsxs("span", { className: "attachment-card", children: [_jsxs("span", { className: "attachment-card-body", children: [_jsx("span", { className: "attachment-card-name", children: name }), _jsxs("span", { className: "attachment-card-note", title: file.mimeType, children: [kind, " \u00B7 ", size] }), file.description && _jsx("span", { className: "attachment-card-note", children: file.description })] }), _jsxs("span", { className: "attachment-card-actions", children: [opens && (_jsx("a", { href: file.url, target: "_blank", rel: "noreferrer", onClick: (event) => openInTab(event, file.url), children: host.strings.attachmentOpen })), _jsx("a", { href: file.downloadUrl, download: name, children: host.strings.attachmentDownload })] })] }));
|
|
72
161
|
}
|
|
73
162
|
export function MarkdownAnchor({ href, children, node: _node, ...rest }) {
|
|
74
163
|
const host = useHost();
|
|
@@ -141,6 +230,19 @@ function MarkdownCode({ className, children, node: _node, ...rest }) {
|
|
|
141
230
|
const { scheme } = useHost();
|
|
142
231
|
const inBlock = useInBlock();
|
|
143
232
|
const inlineSlot = useSlot('inlineCode');
|
|
233
|
+
// A grammar not among the fifteen is fetched the first time a fence names it, and the block
|
|
234
|
+
// drawn again highlighted once it is there (#84); until then it is plain code.
|
|
235
|
+
const toLoad = inBlock ? languageToLoad(className) : null;
|
|
236
|
+
const [, arrived] = useReducer((count) => count + 1, 0);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (!toLoad)
|
|
239
|
+
return;
|
|
240
|
+
let live = true;
|
|
241
|
+
void loadGrammar(toLoad).then((loaded) => live && loaded && arrived());
|
|
242
|
+
return () => {
|
|
243
|
+
live = false;
|
|
244
|
+
};
|
|
245
|
+
}, [toLoad]);
|
|
144
246
|
if (className?.includes('language-mermaid')) {
|
|
145
247
|
return _jsx(MermaidDiagram, { code: String(children ?? '').trim(), scheme: scheme });
|
|
146
248
|
}
|
|
@@ -219,8 +321,9 @@ function taskState(node) {
|
|
|
219
321
|
* lets a click become a one-character change in the original text — made by
|
|
220
322
|
* the host, never by the package.
|
|
221
323
|
*/
|
|
222
|
-
function TaskItem({ node, children, className, source, toggleTask, writable, ...rest }) {
|
|
324
|
+
function TaskItem({ node, children, className, source, toggleTask, taskSave, writable, ...rest }) {
|
|
223
325
|
const boxSlot = useSlot('taskBox');
|
|
326
|
+
const { strings } = useHost();
|
|
224
327
|
const element = node;
|
|
225
328
|
const checked = taskState(element);
|
|
226
329
|
if (checked === null) {
|
|
@@ -228,15 +331,21 @@ function TaskItem({ node, children, className, source, toggleTask, writable, ...
|
|
|
228
331
|
}
|
|
229
332
|
const offset = element?.position?.start?.offset;
|
|
230
333
|
const ordinal = offset === undefined ? -1 : ordinalAt(source, offset);
|
|
231
|
-
|
|
334
|
+
// While the host saves a tick the box shows the state being written and answers no click; once a
|
|
335
|
+
// save failed it is back to what its source holds, which is `checked`.
|
|
336
|
+
const save = ordinal >= 0 ? (taskSave?.(ordinal) ?? null) : null;
|
|
337
|
+
const pending = save?.kind === 'pending';
|
|
338
|
+
const failed = save?.kind === 'failed';
|
|
339
|
+
const shown = save?.kind === 'pending' ? save.checked : checked;
|
|
340
|
+
const enabled = writable && toggleTask !== undefined && ordinal >= 0 && !pending;
|
|
232
341
|
const toggle = () => {
|
|
233
342
|
if (enabled)
|
|
234
|
-
toggleTask(ordinal);
|
|
343
|
+
toggleTask(ordinal, !checked);
|
|
235
344
|
};
|
|
236
345
|
return (
|
|
237
346
|
// Our class comes LAST, because the one react-markdown passes in is on
|
|
238
347
|
// `className` and spreading it after would silently take the item back.
|
|
239
|
-
_jsxs("li", { ...rest, className: `${className ?? ''} task-item`.trim(), children: [boxSlot ? (drawSlot(boxSlot, { checked, enabled, toggle })) : (_jsx("input", { type: "checkbox", checked:
|
|
348
|
+
_jsxs("li", { ...rest, className: `${className ?? ''} task-item`.trim(), "data-save": pending ? 'pending' : failed ? 'failed' : undefined, "aria-busy": pending || undefined, children: [boxSlot ? (drawSlot(boxSlot, { checked: shown, enabled, toggle, pending, failed })) : (_jsx("input", { type: "checkbox", checked: shown, disabled: !enabled, onChange: toggle })), dropBox(children), pending && (_jsx("span", { className: "task-lock", "aria-hidden": "true", children: _jsx("span", { className: "task-lock-label", children: strings.updating }) })), failed && (_jsx("span", { className: "task-notice", "aria-hidden": "true", children: strings.notSaved }))] }));
|
|
240
349
|
}
|
|
241
350
|
/**
|
|
242
351
|
* The children of the item without the checkbox GFM put in front of them.
|
|
@@ -249,7 +358,7 @@ function dropBox(children) {
|
|
|
249
358
|
return children;
|
|
250
359
|
return children.filter((child) => !(isValidElement(child) && child.type === 'input'));
|
|
251
360
|
}
|
|
252
|
-
export function Markdown({ children, source, onToggleTask, writable = false, footnotes }) {
|
|
361
|
+
export function Markdown({ children, source, onToggleTask, taskSave, writable = false, footnotes }) {
|
|
253
362
|
const text = toUnixNewlines(children);
|
|
254
363
|
const { strings } = useHost();
|
|
255
364
|
const calloutTitle = (kind) => Object.hasOwn(strings.callouts, kind) ? strings.callouts[kind] : undefined;
|
|
@@ -293,6 +402,6 @@ export function Markdown({ children, source, onToggleTask, writable = false, foo
|
|
|
293
402
|
blockquote: MarkdownBlockquote,
|
|
294
403
|
section: MarkdownSection,
|
|
295
404
|
...headings,
|
|
296
|
-
li: (props) => (_jsx(TaskItem, { ...props, source: source ?? text, toggleTask: onToggleTask, writable: writable })),
|
|
405
|
+
li: (props) => (_jsx(TaskItem, { ...props, source: source ?? text, toggleTask: onToggleTask, taskSave: taskSave, writable: writable })),
|
|
297
406
|
}, children: text }) }));
|
|
298
407
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What stands beside the switch between reading and editing, in both modes (#76): a button that
|
|
3
|
+
* shows the note's Markdown in a dialog (#63), and one that copies it. Apart from the editor, so a
|
|
4
|
+
* note that is only read offers them without loading it.
|
|
5
|
+
*/
|
|
6
|
+
/** Show Markdown and Copy Markdown; `text` answers the bytes when a button is used. */
|
|
7
|
+
export declare function NoteActions({ text }: {
|
|
8
|
+
text: () => string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What stands beside the switch between reading and editing, in both modes (#76): a button that
|
|
4
|
+
* shows the note's Markdown in a dialog (#63), and one that copies it. Apart from the editor, so a
|
|
5
|
+
* note that is only read offers them without loading it.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { useHost } from './host.js';
|
|
9
|
+
import { ICON_CHECK, ICON_CLOSE, ICON_COPY, ICON_MARKDOWN } from './mode-icons.js';
|
|
10
|
+
/** Puts the text on the clipboard; answers whether it got there. */
|
|
11
|
+
function copyText(text) {
|
|
12
|
+
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
|
|
13
|
+
if (!clipboard)
|
|
14
|
+
return Promise.resolve(false);
|
|
15
|
+
return clipboard.writeText(text).then(() => true, () => false);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The note's Markdown, read-only, exactly as the host has it or would receive it: a modal dialog,
|
|
19
|
+
* closed by Escape, its button or a click outside it, the focus then back on the button that
|
|
20
|
+
* opened it.
|
|
21
|
+
*/
|
|
22
|
+
function SourceDialog({ text, onClose, onClosed }) {
|
|
23
|
+
const host = useHost();
|
|
24
|
+
const words = host.strings.editor;
|
|
25
|
+
const dialog = useRef(null);
|
|
26
|
+
const [copied, setCopied] = useState(null);
|
|
27
|
+
const closed = useRef(onClosed);
|
|
28
|
+
closed.current = onClosed;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const element = dialog.current;
|
|
31
|
+
if (!element)
|
|
32
|
+
return;
|
|
33
|
+
if (typeof element.showModal === 'function')
|
|
34
|
+
element.showModal();
|
|
35
|
+
else
|
|
36
|
+
element.setAttribute('open', '');
|
|
37
|
+
element.querySelector('.edit-source-close')?.focus();
|
|
38
|
+
return () => {
|
|
39
|
+
if (element.open && typeof element.close === 'function')
|
|
40
|
+
element.close();
|
|
41
|
+
// Only once the dialog is closed may the page behind it take the focus.
|
|
42
|
+
closed.current();
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
return (_jsx("dialog", { ref: dialog, className: "edit-source", "aria-modal": "true", "aria-labelledby": "edit-source-title", onCancel: (event) => {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
onClose();
|
|
48
|
+
},
|
|
49
|
+
// The dialog has no padding of its own, so a click on it and not on its content is outside.
|
|
50
|
+
onClick: (event) => event.target === dialog.current && onClose(), children: _jsxs("div", { className: "edit-source-frame", children: [_jsxs("header", { className: "edit-source-header", children: [_jsx("h2", { id: "edit-source-title", className: "edit-source-title", children: words.sourceTitle }), _jsx("span", { className: "edit-source-copied", role: "status", children: copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : '' }), _jsx("button", { type: "button", className: "edit-control edit-control-labelled", onClick: () => void copyText(text).then((done) => setCopied(done ? 'copied' : 'failed')), children: words.copy }), _jsx("button", { type: "button", className: "edit-control edit-source-close", "aria-label": host.strings.close, title: host.strings.close, onClick: onClose, children: ICON_CLOSE })] }), _jsx("pre", { className: "edit-source-text", tabIndex: 0, children: text })] }) }));
|
|
51
|
+
}
|
|
52
|
+
/** How long Copy Markdown shows its check, in milliseconds. */
|
|
53
|
+
const COPIED_MS = 1600;
|
|
54
|
+
/** Show Markdown and Copy Markdown; `text` answers the bytes when a button is used. */
|
|
55
|
+
export function NoteActions({ text }) {
|
|
56
|
+
const words = useHost().strings.editor;
|
|
57
|
+
const show = useRef(null);
|
|
58
|
+
const [source, setSource] = useState(null);
|
|
59
|
+
const [copied, setCopied] = useState(null);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!copied)
|
|
62
|
+
return;
|
|
63
|
+
const timer = setTimeout(() => setCopied(null), COPIED_MS);
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}, [copied]);
|
|
66
|
+
const copyLabel = copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : words.copyMarkdown;
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: show, type: "button", className: "note-action", title: words.showMarkdown, "aria-label": words.showMarkdown, "aria-haspopup": "dialog", onClick: () => setSource(text()), children: ICON_MARKDOWN }), _jsx("button", { type: "button", className: "note-action", "data-copied": copied ?? undefined, title: copyLabel, "aria-label": words.copyMarkdown, onClick: () => void copyText(text()).then((done) => setCopied(done ? 'copied' : 'failed')), children: copied === 'copied' ? ICON_CHECK : ICON_COPY }), _jsx("span", { className: "visually-hidden", role: "status", children: copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : '' }), source !== null && _jsx(SourceDialog, { text: source, onClose: () => setSource(null), onClosed: () => show.current?.focus() })] }));
|
|
68
|
+
}
|
package/dist/NoteBody.d.ts
CHANGED
|
@@ -2,14 +2,44 @@ import { type NoteHost } from './host.js';
|
|
|
2
2
|
export interface NoteBodyProps extends NoteHost {
|
|
3
3
|
/**
|
|
4
4
|
* The note as the host keeps it, frontmatter included. It is drawn from these bytes and never
|
|
5
|
-
* rewritten; the frontmatter is
|
|
5
|
+
* rewritten; the frontmatter is drawn as the theme decides.
|
|
6
6
|
*/
|
|
7
7
|
readonly markdown: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the note offers the switch to edit it, at its top right (#51). Defaults to `false`:
|
|
10
|
+
* no switch, the note only read, with Show and Copy Markdown as `actions` says.
|
|
11
|
+
*/
|
|
12
|
+
readonly editable?: boolean;
|
|
13
|
+
/** The mode, for a host that controls it; left out, the note keeps its own, starting from `defaultMode`. */
|
|
14
|
+
readonly mode?: NoteMode;
|
|
15
|
+
/** The mode a note that keeps its own starts in. Defaults to `view`. */
|
|
16
|
+
readonly defaultMode?: NoteMode;
|
|
17
|
+
/** The switch was used: the mode the reader asked for. */
|
|
18
|
+
readonly onModeChange?: (mode: NoteMode) => void;
|
|
19
|
+
/**
|
|
20
|
+
* The note after each change made in edit mode, byte for byte the original wherever it was not
|
|
21
|
+
* edited. The package never saves it; a host that answers with the promise of its save has the
|
|
22
|
+
* editor say whether it was saved, and going back to reading wait for it (#64). The promise may
|
|
23
|
+
* resolve with a `SaveOutcome`, a save that succeeded with something the author must decide, or
|
|
24
|
+
* reject with a `SaveRefusal`, the reason it was refused: either opens, from the save's status,
|
|
25
|
+
* a dialog with the host's words and its actions (#86).
|
|
26
|
+
*/
|
|
27
|
+
readonly onChange?: (markdown: string) => unknown;
|
|
28
|
+
/** Whether the switch shows its icon alone or its icon and its word. Defaults to `label`. */
|
|
29
|
+
readonly modeButton?: 'icon' | 'label';
|
|
30
|
+
/**
|
|
31
|
+
* Whether the note offers Show Markdown and Copy Markdown at its top right, editable or not:
|
|
32
|
+
* reading a note's Markdown and copying it are reading (#87). Defaults to `true`; `false` hides
|
|
33
|
+
* them, for a print, a preview or an embed.
|
|
34
|
+
*/
|
|
35
|
+
readonly actions?: boolean;
|
|
8
36
|
}
|
|
37
|
+
/** Whether a note is read or edited. */
|
|
38
|
+
export type NoteMode = 'view' | 'edit';
|
|
9
39
|
/**
|
|
10
40
|
* The body of a note, drawn as MemorySmith's reading page draws it.
|
|
11
41
|
*
|
|
12
42
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
13
43
|
* every piece of notebook data through the props of `NoteHost`.
|
|
14
44
|
*/
|
|
15
|
-
export declare function NoteBody({
|
|
45
|
+
export declare function NoteBody({ editable, mode, defaultMode, onModeChange, onChange, modeButton, actions, ...rest }: NoteBodyProps): import("react").JSX.Element;
|
package/dist/NoteBody.js
CHANGED
|
@@ -1,29 +1,96 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from 'react';
|
|
3
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { lazy, Suspense, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { ICON_EDIT, ICON_VIEW } from './mode-icons.js';
|
|
4
|
+
import { NoteActions } from './NoteActions.js';
|
|
5
|
+
import { drawSlot } from './slots.js';
|
|
6
|
+
import { HOST_KEYS, HostProvider, resolveHost, useHost } from './host.js';
|
|
4
7
|
import { planFootnotes } from './footnotes.js';
|
|
5
8
|
import { Markdown } from './Markdown.js';
|
|
6
9
|
import { readingPlugins } from './reading.js';
|
|
7
10
|
import { bodyStart, hideComments, remoteImageHosts, resolveWikilinks } from './markdown.js';
|
|
8
11
|
import { readProperties } from './properties.js';
|
|
9
12
|
import { Properties } from './PropertiesBlock.js';
|
|
13
|
+
import { useTaskSaves } from './task-saves.js';
|
|
10
14
|
import { taskBoxes } from './tasklist.js';
|
|
11
15
|
import { rootStyle } from './theme.js';
|
|
12
16
|
import { Transclusion } from './Transclusion.js';
|
|
13
17
|
import { splitEmbeds } from './transclusion.js';
|
|
14
18
|
import { wikilinkAddress } from './wikilink-address.js';
|
|
19
|
+
/** The editor, loaded the first time a note is edited, so a host that only draws never loads it. */
|
|
20
|
+
const NoteEditor = lazy(() => import('./edit/NoteEditor.js').then((module) => ({ default: module.NoteEditor })));
|
|
21
|
+
/** The switch between reading and editing, drawn by the theme's slot or as the default theme draws it. */
|
|
22
|
+
function ModeSwitch({ mode, display, toggle }) {
|
|
23
|
+
const host = useHost();
|
|
24
|
+
const slot = host.theme.components?.modeSwitch;
|
|
25
|
+
const label = mode === 'view' ? host.strings.edit : host.strings.view;
|
|
26
|
+
const icon = mode === 'view' ? ICON_EDIT : ICON_VIEW;
|
|
27
|
+
if (slot)
|
|
28
|
+
return _jsx(_Fragment, { children: drawSlot(slot, { mode, label, icon, display, toggle }) });
|
|
29
|
+
return (_jsxs("button", { type: "button", className: `mode-switch mode-switch-${display}`, "data-mode": mode, title: label, "aria-label": display === 'icon' ? label : undefined, onClick: toggle, children: [icon, display === 'label' && _jsx("span", { className: "mode-switch-label", children: label })] }));
|
|
30
|
+
}
|
|
15
31
|
/**
|
|
16
32
|
* The body of a note, drawn as MemorySmith's reading page draws it.
|
|
17
33
|
*
|
|
18
34
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
19
35
|
* every piece of notebook data through the props of `NoteHost`.
|
|
20
36
|
*/
|
|
21
|
-
export function NoteBody({
|
|
37
|
+
export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeChange, onChange, modeButton = 'label', actions = true, ...rest }) {
|
|
38
|
+
const [own, setOwn] = useState(defaultMode);
|
|
39
|
+
// The author asked to go back to reading, and the editor is finishing: a save, the toolbar folding.
|
|
40
|
+
const [leaving, setLeaving] = useState(false);
|
|
41
|
+
// The last text handed to the host in edit mode, which Show and Copy Markdown give there (#76).
|
|
42
|
+
const handed = useRef(null);
|
|
43
|
+
const current = editable ? (mode ?? own) : 'view';
|
|
44
|
+
if (!editable && !actions)
|
|
45
|
+
return _jsx(NoteView, { ...rest });
|
|
46
|
+
const go = (next) => {
|
|
47
|
+
if (mode === undefined)
|
|
48
|
+
setOwn(next);
|
|
49
|
+
onModeChange?.(next);
|
|
50
|
+
};
|
|
51
|
+
const toggle = () => {
|
|
52
|
+
if (current === 'view') {
|
|
53
|
+
handed.current = null;
|
|
54
|
+
go('edit');
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
setLeaving(true);
|
|
58
|
+
};
|
|
59
|
+
const hand = (text) => {
|
|
60
|
+
handed.current = text;
|
|
61
|
+
return onChange?.(text);
|
|
62
|
+
};
|
|
63
|
+
const onLeave = (left) => {
|
|
64
|
+
setLeaving(false);
|
|
65
|
+
if (left)
|
|
66
|
+
go('view');
|
|
67
|
+
};
|
|
68
|
+
const { writable: _writable, onToggleTask: _toggle, ...host } = rest;
|
|
69
|
+
return (_jsx(HostOnly, { host: rest, children: (resolved) => {
|
|
70
|
+
// Show and Copy Markdown for whoever reads the note, in both modes (#76, #87), and the
|
|
71
|
+
// switch only where the note is editable.
|
|
72
|
+
const corner = (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: "note-corner", children: [actions && _jsx(NoteActions, { text: () => (current === 'edit' ? (handed.current ?? rest.markdown) : rest.markdown) }), editable && _jsx(ModeSwitch, { mode: current, display: modeButton, toggle: toggle })] }) }));
|
|
73
|
+
if (current === 'view')
|
|
74
|
+
return _jsx(NoteView, { ...rest, corner: corner, switched: editable });
|
|
75
|
+
return (
|
|
76
|
+
// While the editor loads, the note stays on the page as it was.
|
|
77
|
+
_jsx(Suspense, { fallback: _jsx(NoteView, { ...rest, corner: corner, switched: true }), children: _jsx(NoteEditor, { ...host, onChange: hand, corner: corner, autoFocus: true, leaving: leaving, onLeave: onLeave }) }));
|
|
78
|
+
} }));
|
|
79
|
+
}
|
|
80
|
+
/** The host resolved once, for what is drawn around the note. */
|
|
81
|
+
function HostOnly({ host, children }) {
|
|
82
|
+
const resolved = useMemo(() => resolveHost(host), HOST_KEYS.map((key) => host[key]));
|
|
83
|
+
return _jsx(_Fragment, { children: children(resolved) });
|
|
84
|
+
}
|
|
85
|
+
/** The note drawn for reading: the page MemorySmith's reading page draws. */
|
|
86
|
+
function NoteView({ markdown, writable = false, onToggleTask, corner, switched = false, ...host }) {
|
|
22
87
|
const resolved = useMemo(() => resolveHost(host),
|
|
23
88
|
// Every member of the contract, so a host that passes the same functions keeps the same host.
|
|
24
89
|
HOST_KEYS.map((key) => host[key]));
|
|
25
90
|
// The frontmatter is drawn apart from the body, as the theme decides, so the
|
|
26
91
|
// runs start after it. Everything below counts task items from there.
|
|
92
|
+
// Each tick the host answers with a promise locks its item until the save settles.
|
|
93
|
+
const saves = useTaskSaves(onToggleTask);
|
|
27
94
|
const split = bodyStart(markdown);
|
|
28
95
|
const properties = useMemo(() => readProperties(markdown), [markdown]);
|
|
29
96
|
// What a comment holds is never read (§7.6): it leaves before the embeds and the links are found.
|
|
@@ -80,7 +147,7 @@ export function NoteBody({ markdown, writable = false, onToggleTask, ...host })
|
|
|
80
147
|
// The runs and whether an embed cut them are the whole input.
|
|
81
148
|
[runs.join('\u0000'), runs.length < segments.length]);
|
|
82
149
|
let run = 0;
|
|
83
|
-
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name}`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [_jsx(Properties, { properties: properties }), segments.map((segment, index) => {
|
|
150
|
+
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name}${corner ? ' has-note-corner' : ''}${switched ? ' has-mode-switch' : ''}`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [corner, _jsx(Properties, { properties: properties }), segments.map((segment, index) => {
|
|
84
151
|
if (!('rendered' in segment)) {
|
|
85
152
|
return _jsx(Transclusion, { target: segment.target, anchor: segment.anchor }, index);
|
|
86
153
|
}
|
|
@@ -93,6 +160,6 @@ export function NoteBody({ markdown, writable = false, onToggleTask, ...host })
|
|
|
93
160
|
// `mappable` above is what makes it safe to write back through.
|
|
94
161
|
// A planned run is the same text with definitions appended after
|
|
95
162
|
// it, so every box keeps the offset it had.
|
|
96
|
-
source: planned?.text ?? segment.rendered, onToggleTask: onToggleTask && ((ordinal) =>
|
|
97
|
-
}), footnotes && _jsx(Markdown, { footnotes: "section", children: footnotes.section }), hosts.length > 0 && _jsx("p", { className: "remote-notice", children: resolved.strings.remoteImages(hosts) })] }) }));
|
|
163
|
+
source: planned?.text ?? segment.rendered, onToggleTask: onToggleTask && ((ordinal, checked) => saves.toggle(base + ordinal, checked)), taskSave: (ordinal) => saves.stateOf(base + ordinal), writable: writable && mappable, ...(planned ? { footnotes: { refs: planned.refs } } : {}), children: planned?.text ?? segment.rendered }, index));
|
|
164
|
+
}), footnotes && _jsx(Markdown, { footnotes: "section", children: footnotes.section }), hosts.length > 0 && _jsx("p", { className: "remote-notice", children: resolved.strings.remoteImages(hosts) }), writable && onToggleTask && (_jsx("p", { className: "visually-hidden", role: "status", "aria-live": "polite", children: saves.announcement && resolved.strings[saves.announcement] }))] }) }));
|
|
98
165
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attach dialog (#78): the three ways a file or an image comes into a note. From the computer —
|
|
3
|
+
* one or more files, each kept by the host's `attach` under the name and with the description the
|
|
4
|
+
* author gives; from the notebook — a file it already keeps, listed by the host's `files`, found by
|
|
5
|
+
* name or description; from the web — an image at a public address, written as ``.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChosenFile {
|
|
8
|
+
readonly file: File;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
}
|
|
12
|
+
/** The Markdown of an image at a web address, its name the alternative text (§3.14). */
|
|
13
|
+
export declare function webImage(name: string, address: string): string;
|
|
14
|
+
export declare function AttachDialog({ refusal, onClose, onClosed, onAttach, onEmbed, onLink, }: {
|
|
15
|
+
/** Why the host refused the last file, said at the top of the dialog the author opened after it (#88). */
|
|
16
|
+
refusal?: string | null;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
/** Once the dialog is closed: whether something was put in the note, which then has the focus. */
|
|
19
|
+
onClosed: (inserted: boolean) => void;
|
|
20
|
+
onAttach: (files: readonly ChosenFile[]) => void;
|
|
21
|
+
onEmbed: (name: string) => void;
|
|
22
|
+
onLink: (name: string, address: string) => void;
|
|
23
|
+
}): import("react").JSX.Element;
|