@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.
Files changed (59) hide show
  1. package/README.md +50 -4
  2. package/dist/Markdown.d.ts +6 -2
  3. package/dist/Markdown.js +126 -17
  4. package/dist/NoteActions.d.ts +9 -0
  5. package/dist/NoteActions.js +68 -0
  6. package/dist/NoteBody.d.ts +32 -2
  7. package/dist/NoteBody.js +74 -7
  8. package/dist/edit/AttachDialog.d.ts +23 -0
  9. package/dist/edit/AttachDialog.js +126 -0
  10. package/dist/edit/ContextMenu.d.ts +29 -0
  11. package/dist/edit/ContextMenu.js +95 -0
  12. package/dist/edit/LanguageControl.d.ts +19 -0
  13. package/dist/edit/LanguageControl.js +124 -0
  14. package/dist/edit/NoteEditor.d.ts +44 -0
  15. package/dist/edit/NoteEditor.js +520 -0
  16. package/dist/edit/SaveNoticeDialog.d.ts +15 -0
  17. package/dist/edit/SaveNoticeDialog.js +41 -0
  18. package/dist/edit/analyze.d.ts +150 -0
  19. package/dist/edit/analyze.js +268 -0
  20. package/dist/edit/blocks.d.ts +97 -0
  21. package/dist/edit/blocks.js +481 -0
  22. package/dist/edit/commands.d.ts +78 -0
  23. package/dist/edit/commands.js +257 -0
  24. package/dist/edit/frontmatter.d.ts +45 -0
  25. package/dist/edit/frontmatter.js +142 -0
  26. package/dist/edit/index.d.ts +5 -0
  27. package/dist/edit/index.js +7 -0
  28. package/dist/edit/live.d.ts +28 -0
  29. package/dist/edit/live.js +675 -0
  30. package/dist/edit/saves.d.ts +48 -0
  31. package/dist/edit/saves.js +73 -0
  32. package/dist/edit/table.d.ts +58 -0
  33. package/dist/edit/table.js +185 -0
  34. package/dist/file-types.d.ts +30 -0
  35. package/dist/file-types.js +70 -0
  36. package/dist/highlight.d.ts +21 -7
  37. package/dist/highlight.js +56 -7
  38. package/dist/host.d.ts +82 -3
  39. package/dist/host.js +16 -0
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +1 -0
  42. package/dist/mode-icons.d.ts +6 -0
  43. package/dist/mode-icons.js +11 -0
  44. package/dist/prism-grammars.d.ts +18 -0
  45. package/dist/prism-grammars.js +305 -0
  46. package/dist/remark-callouts.d.ts +6 -0
  47. package/dist/remark-callouts.js +2 -2
  48. package/dist/remark-ring.d.ts +5 -0
  49. package/dist/remark-ring.js +25 -15
  50. package/dist/slots.js +17 -1
  51. package/dist/strings.d.ts +129 -1
  52. package/dist/strings.js +127 -1
  53. package/dist/task-saves.d.ts +35 -0
  54. package/dist/task-saves.js +86 -0
  55. package/dist/theme.d.ts +47 -0
  56. package/dist/theme.js +2 -2
  57. package/package.json +14 -2
  58. package/structure.css +355 -0
  59. package/theme-default.css +1099 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * What the toolbar and the shortcuts of the editor do to a note (#52, #53, #57, #59).
3
+ *
4
+ * Each command is a change to the bytes around the selection and nothing else: a mark is the
5
+ * delimiters written on either side of what is selected, a heading the `#` at the start of its
6
+ * line, a list the marker of each line selected. The page the author sees follows from the text,
7
+ * because the editor draws the text.
8
+ */
9
+ import { EditorSelection } from '@codemirror/state';
10
+ /** A command from a function of the state, which answers `null` when it does not apply. */
11
+ export function command(spec) {
12
+ return (view) => {
13
+ if (view.state.readOnly)
14
+ return false;
15
+ const transaction = spec(view.state);
16
+ if (!transaction)
17
+ return false;
18
+ view.dispatch({ scrollIntoView: true, userEvent: 'input.format', ...transaction });
19
+ view.focus();
20
+ return true;
21
+ };
22
+ }
23
+ /**
24
+ * Writes or takes away a pair of delimiters around each selection: `**` bold, `_` italic, `~~`
25
+ * struck, `==` marked, `` ` `` code. A selection that the delimiters already wrap, inside it or
26
+ * just outside it, loses them; an empty one gains the pair with the cursor between.
27
+ */
28
+ export function toggleMark(state, delimiter) {
29
+ const size = delimiter.length;
30
+ return state.changeByRange((range) => {
31
+ const before = state.sliceDoc(range.from - size, range.from);
32
+ const after = state.sliceDoc(range.to, range.to + size);
33
+ if (before === delimiter && after === delimiter) {
34
+ return {
35
+ changes: [
36
+ { from: range.from - size, to: range.from },
37
+ { from: range.to, to: range.to + size },
38
+ ],
39
+ range: EditorSelection.range(range.from - size, range.to - size),
40
+ };
41
+ }
42
+ const inside = state.sliceDoc(range.from, range.to);
43
+ if (inside.length >= size * 2 && inside.startsWith(delimiter) && inside.endsWith(delimiter)) {
44
+ return {
45
+ changes: { from: range.from, to: range.to, insert: inside.slice(size, -size) },
46
+ range: EditorSelection.range(range.from, range.to - size * 2),
47
+ };
48
+ }
49
+ return {
50
+ changes: [
51
+ { from: range.from, insert: delimiter },
52
+ { from: range.to, insert: delimiter },
53
+ ],
54
+ range: EditorSelection.range(range.from + size, range.to + size),
55
+ };
56
+ });
57
+ }
58
+ /**
59
+ * Changes to the start of lines, with the selection after what they insert: a cursor between a
60
+ * line's start and where its marker goes — before a quotation's `>` a list marker follows — is after
61
+ * the marker, so what is typed next is in it (#82). Any other cursor keeps its place in the text.
62
+ */
63
+ function atLineStarts(state, changes) {
64
+ const set = state.changes([...changes]);
65
+ const selection = EditorSelection.create(state.selection.ranges.map((range) => {
66
+ if (!range.empty)
67
+ return range.map(set, 1);
68
+ const line = state.doc.lineAt(range.head);
69
+ const change = changes.find((each) => each.from >= line.from && each.from <= line.to && range.head <= each.to);
70
+ return change ? EditorSelection.cursor(set.mapPos(change.to, 1)) : range.map(set, 1);
71
+ }), state.selection.mainIndex);
72
+ return { changes: set, selection };
73
+ }
74
+ /** The lines the selection touches, each once. */
75
+ function selectedLines(state) {
76
+ const seen = new Set();
77
+ const lines = [];
78
+ for (const range of state.selection.ranges) {
79
+ for (let at = state.doc.lineAt(range.from).number; at <= state.doc.lineAt(range.to).number; at++) {
80
+ if (seen.has(at))
81
+ continue;
82
+ seen.add(at);
83
+ const line = state.doc.line(at);
84
+ lines.push({ from: line.from, to: line.to, text: line.text, number: at });
85
+ }
86
+ }
87
+ return lines;
88
+ }
89
+ const QUOTE = /^(?:[ \t]{0,3}>[ \t]?)*/;
90
+ const HEADING = /^#{1,6}(?:[ \t]+|$)/;
91
+ const LIST = /^([ \t]*)(?:([-*+]|\d{1,9}[.)])[ \t]+(\[[ xX]\][ \t]+)?)?/;
92
+ /** Makes each selected line a heading of `level`, or a paragraph at 0; a quotation's markers stay. */
93
+ export function setHeading(state, level) {
94
+ const changes = selectedLines(state).map((line) => {
95
+ const quote = QUOTE.exec(line.text)[0];
96
+ const rest = line.text.slice(quote.length);
97
+ const marker = HEADING.exec(rest)?.[0] ?? '';
98
+ return { from: line.from + quote.length, to: line.from + quote.length + marker.length, insert: level > 0 ? `${'#'.repeat(level)} ` : '' };
99
+ });
100
+ return atLineStarts(state, changes);
101
+ }
102
+ /** The heading level of the line the cursor is on, or 0. */
103
+ export function headingLevel(state) {
104
+ const text = state.doc.lineAt(state.selection.main.head).text;
105
+ const marker = HEADING.exec(text.slice(QUOTE.exec(text)[0].length));
106
+ return marker ? marker[0].trim().length : 0;
107
+ }
108
+ function listKindOf(text) {
109
+ const match = LIST.exec(text.slice(QUOTE.exec(text)[0].length));
110
+ if (!match?.[2])
111
+ return null;
112
+ if (match[3])
113
+ return 'task';
114
+ return /\d/.test(match[2]) ? 'ordered' : 'bullet';
115
+ }
116
+ /**
117
+ * Makes each selected line an item of a list of `kind`, renumbering an ordered one from 1; when
118
+ * every line already is one, they become paragraphs again. Blank lines among them are left alone;
119
+ * a blank line alone starts a list.
120
+ */
121
+ export function toggleList(state, kind) {
122
+ const selected = selectedLines(state);
123
+ const marker = kind === 'bullet' ? '- ' : kind === 'task' ? '- [ ] ' : '1. ';
124
+ // On a blank line a list starts: its marker, the cursor after it for the first item (#74).
125
+ if (selected.every((line) => line.text.trim().length === 0)) {
126
+ const line = state.doc.lineAt(state.selection.main.head);
127
+ const quote = QUOTE.exec(line.text)[0];
128
+ return { changes: { from: line.from + quote.length, to: line.to, insert: marker }, selection: { anchor: line.from + quote.length + marker.length } };
129
+ }
130
+ const lines = selected.filter((line) => line.text.trim().length > 0);
131
+ const remove = lines.length > 0 && lines.every((line) => listKindOf(line.text) === kind);
132
+ let number = 1;
133
+ const changes = lines.map((line) => {
134
+ const quote = QUOTE.exec(line.text)[0];
135
+ const match = LIST.exec(line.text.slice(quote.length));
136
+ const from = line.from + quote.length + match[1].length;
137
+ const to = line.from + quote.length + match[0].length;
138
+ const marker = remove ? '' : kind === 'bullet' ? '- ' : kind === 'task' ? '- [ ] ' : `${number++}. `;
139
+ return { from, to, insert: marker };
140
+ });
141
+ return atLineStarts(state, changes);
142
+ }
143
+ /** Quotes each selected line, or unquotes them when every one is quoted. */
144
+ export function toggleQuote(state) {
145
+ const lines = selectedLines(state);
146
+ const remove = lines.every((line) => /^[ \t]{0,3}>/.test(line.text));
147
+ const changes = lines.map((line) => {
148
+ if (!remove)
149
+ return { from: line.from, to: line.from, insert: '> ' };
150
+ const marker = /^[ \t]{0,3}>[ \t]?/.exec(line.text)[0];
151
+ return { from: line.from, to: line.from + marker.length, insert: '' };
152
+ });
153
+ return atLineStarts(state, changes);
154
+ }
155
+ /**
156
+ * Inserts a block on lines of its own: in place of the current line when it is blank, else after
157
+ * the block the cursor is in, with a blank line on either side. `cursor` is where in the block the
158
+ * cursor goes, from its start; past its end, the cursor goes to the line after it.
159
+ */
160
+ export function insertBlock(state, block, cursor) {
161
+ const line = state.doc.lineAt(state.selection.main.head);
162
+ // A cursor past the block lands at the start of the line after it, made when there is none.
163
+ const past = cursor > block.length;
164
+ if (line.text.trim().length === 0) {
165
+ const before = line.number > 1 && state.doc.line(line.number - 1).text.trim().length > 0 ? '\n' : '';
166
+ const next = line.number < state.doc.lines && state.doc.line(line.number + 1).text.trim().length > 0;
167
+ const last = line.number === state.doc.lines;
168
+ const insert = `${before}${block}${next || (past && last) ? '\n' : ''}`;
169
+ const start = line.from + before.length;
170
+ return { changes: { from: line.from, to: line.to, insert }, selection: { anchor: past ? start + block.length + 1 : start + cursor } };
171
+ }
172
+ // After the paragraph the cursor is in: before the blank line that ends it, or at the end of the note.
173
+ let end = line;
174
+ while (end.number < state.doc.lines && state.doc.line(end.number + 1).text.trim().length > 0)
175
+ end = state.doc.line(end.number + 1);
176
+ const last = end.number === state.doc.lines;
177
+ const start = end.to + 2;
178
+ return {
179
+ changes: { from: end.to, insert: `\n\n${block}${past && last ? '\n' : ''}` },
180
+ selection: { anchor: past ? start + block.length + 1 : start + cursor },
181
+ };
182
+ }
183
+ /** A link around the selection, with its address selected to be typed; `[](https://)` with none. */
184
+ export function insertLink(state) {
185
+ return state.changeByRange((range) => {
186
+ const label = state.sliceDoc(range.from, range.to);
187
+ const insert = `[${label}](https://)`;
188
+ const address = range.from + label.length + 3;
189
+ return {
190
+ changes: { from: range.from, to: range.to, insert },
191
+ range: label.length > 0 ? EditorSelection.range(address, address + 8) : EditorSelection.cursor(range.from + 1),
192
+ };
193
+ });
194
+ }
195
+ /** A wikilink around the selection; with none, `[[]]` with the cursor between, where names are suggested. */
196
+ export function insertWikilink(state) {
197
+ return state.changeByRange((range) => {
198
+ const name = state.sliceDoc(range.from, range.to);
199
+ return {
200
+ changes: { from: range.from, to: range.to, insert: `[[${name}]]` },
201
+ range: EditorSelection.cursor(range.from + 2 + name.length),
202
+ };
203
+ });
204
+ }
205
+ /** A comment around the selection, or an empty one with the cursor inside (§7.6). */
206
+ export function toggleComment(state) {
207
+ const range = state.selection.main;
208
+ const inside = state.sliceDoc(range.from, range.to);
209
+ if (inside.startsWith('%%') && inside.endsWith('%%') && inside.length >= 4) {
210
+ return { changes: { from: range.from, to: range.to, insert: inside.slice(2, -2) } };
211
+ }
212
+ if (range.empty)
213
+ return { changes: { from: range.from, insert: '%% %%' }, selection: { anchor: range.from + 3 } };
214
+ return {
215
+ changes: [
216
+ { from: range.from, insert: '%%' },
217
+ { from: range.to, insert: '%%' },
218
+ ],
219
+ selection: { anchor: range.from + 2, head: range.to + 2 },
220
+ };
221
+ }
222
+ /**
223
+ * A footnote: its reference at the cursor, numbered after the highest number the note uses, and
224
+ * its definition at the end of the note, where the cursor goes to write it (§7.12).
225
+ */
226
+ export function insertFootnote(state) {
227
+ const text = state.sliceDoc();
228
+ const used = [...text.matchAll(/\[\^(\d+)\]/g)].map((match) => Number(match[1]));
229
+ const label = String(Math.max(0, ...used) + 1);
230
+ const at = state.selection.main.head;
231
+ const tail = text.endsWith('\n\n') ? '' : text.endsWith('\n') ? '\n' : '\n\n';
232
+ const definition = `${tail}[^${label}]: `;
233
+ const reference = `[^${label}]`;
234
+ return {
235
+ changes: [
236
+ { from: at, insert: reference },
237
+ { from: text.length, insert: definition },
238
+ ],
239
+ selection: { anchor: text.length + reference.length + definition.length },
240
+ };
241
+ }
242
+ /** Flips the task box whose `[` is at `at`, as `toggleTaskAt` does: `[x]` lower case on the way in. */
243
+ export function toggleTaskBox(state, at) {
244
+ if (state.sliceDoc(at, at + 1) !== '[' || state.sliceDoc(at + 2, at + 3) !== ']')
245
+ return null;
246
+ const current = state.sliceDoc(at + 1, at + 2);
247
+ return { changes: { from: at + 1, to: at + 2, insert: current === ' ' ? 'x' : ' ' } };
248
+ }
249
+ /** The blocks the toolbar inserts, each with where the cursor goes in it. */
250
+ export const BLOCKS = {
251
+ callout: { text: '> [!note]\n> ', cursor: 12 },
252
+ table: { text: '| | |\n| --- | --- |\n| | |', cursor: 2 },
253
+ codeBlock: { text: '```\n\n```', cursor: 4 },
254
+ math: { text: '$$\n\n$$', cursor: 3 },
255
+ diagram: { text: '```mermaid\nflowchart LR\n A --> B\n```', cursor: 23 },
256
+ rule: { text: '---', cursor: 4 },
257
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The frontmatter of a note, read as ranges of its bytes, and the edits the properties form makes
3
+ * to it (#58).
4
+ *
5
+ * An edit rewrites the lines of the one entry it names, in the form that entry was written in —
6
+ * a scalar stays a scalar with its quotes, an inline list stays inline, a dash list keeps its
7
+ * indent — and leaves every other line of the block as written: the order of the keys, their
8
+ * quoting, and any line the form does not read, such as a comment.
9
+ */
10
+ import type { Span } from './analyze.js';
11
+ import type { TextChange } from './table.js';
12
+ export type EntryForm = 'scalar' | 'inline' | 'dash';
13
+ export interface FrontmatterEntry extends Span {
14
+ readonly key: string;
15
+ readonly keySpan: Span;
16
+ readonly form: EntryForm;
17
+ /** The value written on the key's line, after the colon and its spaces: empty for a dash list. */
18
+ readonly valueSpan: Span;
19
+ /** The quote a scalar was written in, which an edit keeps. */
20
+ readonly quote: '"' | "'" | null;
21
+ /** The indent of a dash list's items, which a new item copies. */
22
+ readonly indent: string;
23
+ }
24
+ export interface Frontmatter extends Span {
25
+ readonly source: string;
26
+ /** Where a new entry goes: the start of the closing `---` line. */
27
+ readonly end: number;
28
+ readonly entries: readonly FrontmatterEntry[];
29
+ }
30
+ /** The frontmatter of a note, or `null` when it has none (§6.1). */
31
+ export declare function readFrontmatter(source: string): Frontmatter | null;
32
+ /** A scalar as YAML reads it back as the same text: quoted when it would otherwise mean something else. */
33
+ export declare function scalarText(value: string, quote?: '"' | "'" | null): string;
34
+ /** Sets an entry to one value, keeping its quote; a list becomes the scalar on the key's line. */
35
+ export declare function setScalar(frontmatter: Frontmatter, key: string, value: string): TextChange[];
36
+ /** Sets an entry to a list, in the form it was written in; a scalar becomes an inline list. */
37
+ export declare function setList(frontmatter: Frontmatter, key: string, items: readonly string[]): TextChange[];
38
+ /** Renames a key, and only the key. */
39
+ export declare function renameKey(frontmatter: Frontmatter, key: string, next: string): TextChange[];
40
+ /** Removes an entry with all its lines. */
41
+ export declare function removeEntry(frontmatter: Frontmatter, key: string): TextChange[];
42
+ /** Adds an entry at the end of the block, before its closing line. */
43
+ export declare function addEntry(frontmatter: Frontmatter, key: string, value: string | readonly string[]): TextChange[];
44
+ /** The frontmatter a note gains when its author adds properties to a note with none. */
45
+ export declare const NEW_FRONTMATTER = "---\n---\n";
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The frontmatter of a note, read as ranges of its bytes, and the edits the properties form makes
3
+ * to it (#58).
4
+ *
5
+ * An edit rewrites the lines of the one entry it names, in the form that entry was written in —
6
+ * a scalar stays a scalar with its quotes, an inline list stays inline, a dash list keeps its
7
+ * indent — and leaves every other line of the block as written: the order of the keys, their
8
+ * quoting, and any line the form does not read, such as a comment.
9
+ */
10
+ const KEY = /^([A-Za-z0-9_-]+)([ \t]*:[ \t]*)(.*)$/;
11
+ const DASH = /^([ \t]+)-[ \t]+/;
12
+ /** The frontmatter of a note, or `null` when it has none (§6.1). */
13
+ export function readFrontmatter(source) {
14
+ if (!source.startsWith('---\n'))
15
+ return null;
16
+ const close = /\n---[ \t]*(?:\n|$)/.exec(source.slice(3));
17
+ if (!close)
18
+ return null;
19
+ const end = 3 + close.index + 1;
20
+ const entries = [];
21
+ // The entry the next dash line may belong to; a line the form does not read ends it.
22
+ let open = null;
23
+ let at = 4;
24
+ while (at < end) {
25
+ const newline = source.indexOf('\n', at);
26
+ const lineTo = newline === -1 || newline > end ? end : newline;
27
+ const line = source.slice(at, lineTo);
28
+ const pair = KEY.exec(line);
29
+ const dash = DASH.exec(line);
30
+ if (pair) {
31
+ const key = pair[1];
32
+ const valueFrom = at + key.length + pair[2].length;
33
+ const raw = pair[3].replace(/[ \t]+$/, '');
34
+ const inline = raw.startsWith('[') && raw.endsWith(']') && !/^\[\[[^\]]+\]\]$/.test(raw);
35
+ const quote = /^(["'])[\s\S]*\1$/.exec(raw)?.[1];
36
+ open = {
37
+ key,
38
+ from: at,
39
+ to: lineTo,
40
+ keySpan: { from: at, to: at + key.length },
41
+ form: inline ? 'inline' : 'scalar',
42
+ valueSpan: { from: valueFrom, to: valueFrom + raw.length },
43
+ quote: quote === '"' || quote === "'" ? quote : null,
44
+ indent: ' ',
45
+ };
46
+ entries.push(open);
47
+ }
48
+ else if (open && dash && (open.form === 'dash' || open.valueSpan.from === open.valueSpan.to)) {
49
+ open.form = 'dash';
50
+ open.indent = dash[1];
51
+ open.to = lineTo;
52
+ }
53
+ else {
54
+ open = null;
55
+ }
56
+ at = lineTo + 1;
57
+ }
58
+ return { source, from: 0, to: end + 3, end, entries };
59
+ }
60
+ /** A scalar as YAML reads it back as the same text: quoted when it would otherwise mean something else. */
61
+ export function scalarText(value, quote = null) {
62
+ const single = value.replace(/\r?\n/g, ' ');
63
+ if (quote === "'")
64
+ return `'${single.replace(/'/g, "''")}'`;
65
+ if (quote === '"' || needsQuotes(single))
66
+ return `"${single.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
67
+ return single;
68
+ }
69
+ function needsQuotes(value) {
70
+ if (value.length === 0)
71
+ return false;
72
+ if (/^\[\[[^\]]+\]\]$/.test(value))
73
+ return true;
74
+ return /^[\s\-?:,[\]{}#&*!|>'"%@`]|\s$|: | #|:$/.test(value);
75
+ }
76
+ function itemText(value) {
77
+ const single = value.replace(/\r?\n/g, ' ').trim();
78
+ return /[,[\]{}#&*!|>'"%@`]|: /.test(single) || /^[\s-?:]/.test(single)
79
+ ? `"${single.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
80
+ : single;
81
+ }
82
+ function entryOf(frontmatter, key) {
83
+ return frontmatter.entries.find((entry) => entry.key === key);
84
+ }
85
+ /** Sets an entry to one value, keeping its quote; a list becomes the scalar on the key's line. */
86
+ export function setScalar(frontmatter, key, value) {
87
+ const entry = entryOf(frontmatter, key);
88
+ if (!entry)
89
+ return addEntry(frontmatter, key, value);
90
+ const insert = scalarText(value, entry.quote);
91
+ if (entry.form === 'scalar') {
92
+ const written = frontmatter.source.slice(entry.valueSpan.from, entry.valueSpan.to);
93
+ if (written === insert)
94
+ return [];
95
+ // `key:` with nothing after it gains the space a value is written after.
96
+ const spaced = entry.valueSpan.from === entry.keySpan.to + 1 && insert.length > 0 ? ` ${insert}` : insert;
97
+ return [{ from: entry.valueSpan.from, to: entry.valueSpan.to, insert: spaced }];
98
+ }
99
+ return [{ from: entry.keySpan.to, to: entry.to, insert: `: ${insert}` }];
100
+ }
101
+ /** Sets an entry to a list, in the form it was written in; a scalar becomes an inline list. */
102
+ export function setList(frontmatter, key, items) {
103
+ const entry = entryOf(frontmatter, key);
104
+ const inline = `[${items.map(itemText).join(', ')}]`;
105
+ if (!entry)
106
+ return addEntry(frontmatter, key, items);
107
+ if (entry.form === 'dash' && items.length > 0) {
108
+ const lines = items.map((item) => `\n${entry.indent}- ${itemText(item)}`).join('');
109
+ const written = frontmatter.source.slice(entry.keySpan.to, entry.to);
110
+ const keyLine = frontmatter.source.slice(entry.keySpan.to, frontmatter.source.indexOf('\n', entry.keySpan.to));
111
+ const insert = `${keyLine}${lines}`;
112
+ return written === insert ? [] : [{ from: entry.keySpan.to, to: entry.to, insert }];
113
+ }
114
+ if (entry.form === 'inline' || entry.form === 'scalar') {
115
+ if (frontmatter.source.slice(entry.valueSpan.from, entry.valueSpan.to) === inline)
116
+ return [];
117
+ const spaced = entry.valueSpan.from === entry.keySpan.to + 1 ? ` ${inline}` : inline;
118
+ return [{ from: entry.valueSpan.from, to: entry.valueSpan.to, insert: spaced }];
119
+ }
120
+ return [{ from: entry.keySpan.to, to: entry.to, insert: `: ${inline}` }];
121
+ }
122
+ /** Renames a key, and only the key. */
123
+ export function renameKey(frontmatter, key, next) {
124
+ const entry = entryOf(frontmatter, key);
125
+ if (!entry || !/^[A-Za-z0-9_-]+$/.test(next) || next === key || entryOf(frontmatter, next))
126
+ return [];
127
+ return [{ from: entry.keySpan.from, to: entry.keySpan.to, insert: next }];
128
+ }
129
+ /** Removes an entry with all its lines. */
130
+ export function removeEntry(frontmatter, key) {
131
+ const entry = entryOf(frontmatter, key);
132
+ return entry ? [{ from: entry.from, to: entry.to + 1, insert: '' }] : [];
133
+ }
134
+ /** Adds an entry at the end of the block, before its closing line. */
135
+ export function addEntry(frontmatter, key, value) {
136
+ if (!/^[A-Za-z0-9_-]+$/.test(key) || entryOf(frontmatter, key))
137
+ return [];
138
+ const written = typeof value === 'string' ? scalarText(value) : `[${value.map(itemText).join(', ')}]`;
139
+ return [{ from: frontmatter.end, to: frontmatter.end, insert: `${key}:${written.length > 0 ? ` ${written}` : ''}\n` }];
140
+ }
141
+ /** The frontmatter a note gains when its author adds properties to a note with none. */
142
+ export const NEW_FRONTMATTER = '---\n---\n';
@@ -0,0 +1,5 @@
1
+ export { lineBreakOf, NoteEditor, type NoteEditorProps } from './NoteEditor.js';
2
+ export { analyze, frontmatterSpan, type Analysis, type Construct, type Span } from './analyze.js';
3
+ export { readTable, type Alignment, type Table, type TextChange } from './table.js';
4
+ export { readFrontmatter, type Frontmatter, type FrontmatterEntry } from './frontmatter.js';
5
+ export { isSaveNotice, type SaveAction, type SaveNotice, type SaveOutcome, type SaveRefusal } from './saves.js';
@@ -0,0 +1,7 @@
1
+ // The editor of @memorysmithapp/markdown-renderer, on its own subpath: a host that only draws
2
+ // never loads CodeMirror (ADR-006).
3
+ export { lineBreakOf, NoteEditor } from './NoteEditor.js';
4
+ export { analyze, frontmatterSpan } from './analyze.js';
5
+ export { readTable } from './table.js';
6
+ export { readFrontmatter } from './frontmatter.js';
7
+ export { isSaveNotice } from './saves.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The page the editor draws over the note's text (ADR-006).
3
+ *
4
+ * The text is the model, and nothing here changes it: each construct the analysis finds is drawn
5
+ * by decorations — its notation hidden and its content styled while the cursor is elsewhere, its
6
+ * notation shown, dimmed, while the cursor is in it; a block drawn by the reading surface's own
7
+ * components in place of its lines until the cursor enters it, and then its lines as source.
8
+ */
9
+ import { Facet, StateField, type EditorState, type Extension } from '@codemirror/state';
10
+ import { type DecorationSet } from '@codemirror/view';
11
+ import { type ResolvedHost } from '../host.js';
12
+ import { type Analysis } from './analyze.js';
13
+ /** What the drawn blocks read: the host the note is edited under. */
14
+ export interface EditorEnvironment {
15
+ readonly host: ResolvedHost;
16
+ }
17
+ export declare const editorEnvironment: Facet<EditorEnvironment, EditorEnvironment>;
18
+ /** The note's text with `\n` between lines, whose offsets are the editor's positions. */
19
+ export declare function textOf(state: EditorState): string;
20
+ /** The analysis of the note, kept with the state and made again when the text changes. */
21
+ export declare const analysisField: StateField<Analysis>;
22
+ /** Says the editor took or lost the focus. */
23
+ export declare const setFocused: import("@codemirror/state").StateEffectType<boolean>;
24
+ /** A grammar a code block names has arrived: its tokens can be coloured now (#84). */
25
+ export declare const grammarArrived: import("@codemirror/state").StateEffectType<string>;
26
+ export declare const decorationsField: StateField<DecorationSet>;
27
+ /** The live page: the analysis, the decorations over it, and what the focus and a modifier-click do. */
28
+ export declare function livePage(): Extension;