sfora-cli 0.9.0 → 0.11.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 (108) hide show
  1. package/README.md +147 -6
  2. package/dist/SforaFs.js +278 -10
  3. package/dist/api-client.d.ts +290 -5
  4. package/dist/api-client.js +307 -22
  5. package/dist/block-commands.d.ts +84 -0
  6. package/dist/block-commands.js +155 -0
  7. package/dist/cli.js +323 -29
  8. package/dist/format/__tests__/byteStable.d.ts +5 -0
  9. package/dist/format/__tests__/byteStable.js +64 -0
  10. package/dist/format/blockSplice.d.ts +135 -0
  11. package/dist/format/blockSplice.js +330 -0
  12. package/dist/format/blocks/dropClosure.d.ts +81 -0
  13. package/dist/format/blocks/dropClosure.js +196 -0
  14. package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
  15. package/dist/format/blocks/markdown-block-catalog.js +162 -0
  16. package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
  17. package/dist/format/blocks/markdown-block-ids.mjs +25 -0
  18. package/dist/format/blocks/parsers.d.ts +105 -0
  19. package/dist/format/blocks/parsers.js +442 -0
  20. package/dist/format/blocks/structured-block-schema.d.ts +8 -0
  21. package/dist/format/blocks/structured-block-schema.js +30 -0
  22. package/dist/format/callout.d.ts +128 -0
  23. package/dist/format/callout.js +227 -0
  24. package/dist/format/cardMarkdown.d.ts +2 -0
  25. package/dist/format/cardMarkdown.js +10 -0
  26. package/dist/format/checklist.d.ts +34 -0
  27. package/dist/format/checklist.js +158 -0
  28. package/dist/format/formatAxes.d.ts +228 -0
  29. package/dist/format/formatAxes.js +454 -0
  30. package/dist/format/index.d.ts +19 -4
  31. package/dist/format/index.js +28 -4
  32. package/dist/format/lineGeometry.d.ts +100 -0
  33. package/dist/format/lineGeometry.js +424 -0
  34. package/dist/format/lint/appliesTo.d.ts +92 -0
  35. package/dist/format/lint/appliesTo.js +369 -0
  36. package/dist/format/lint/config.d.ts +106 -0
  37. package/dist/format/lint/config.js +205 -0
  38. package/dist/format/lint/fixAll.d.ts +62 -0
  39. package/dist/format/lint/fixAll.js +107 -0
  40. package/dist/format/lint/frontmatterSchema.d.ts +181 -0
  41. package/dist/format/lint/frontmatterSchema.js +660 -0
  42. package/dist/format/lint/index.d.ts +49 -0
  43. package/dist/format/lint/index.js +51 -0
  44. package/dist/format/lint/lintSource.d.ts +56 -0
  45. package/dist/format/lint/lintSource.js +188 -0
  46. package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
  47. package/dist/format/lint/rules/broken-wiki-link.js +45 -0
  48. package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
  49. package/dist/format/lint/rules/frontmatter-schema.js +92 -0
  50. package/dist/format/lint/rules/index.d.ts +11 -0
  51. package/dist/format/lint/rules/index.js +32 -0
  52. package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
  53. package/dist/format/lint/rules/malformed-callout.js +88 -0
  54. package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
  55. package/dist/format/lint/rules/malformed-checklist.js +65 -0
  56. package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
  57. package/dist/format/lint/rules/malformed-frontmatter.js +98 -0
  58. package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
  59. package/dist/format/lint/rules/malformed-structured-block.js +134 -0
  60. package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
  61. package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
  62. package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
  63. package/dist/format/lint/rules/orphan-reference.js +87 -0
  64. package/dist/format/lint/severity.d.ts +15 -0
  65. package/dist/format/lint/severity.js +50 -0
  66. package/dist/format/lint/textEdits.d.ts +86 -0
  67. package/dist/format/lint/textEdits.js +162 -0
  68. package/dist/format/lint/types.d.ts +116 -0
  69. package/dist/format/lint/types.js +16 -0
  70. package/dist/format/markdown/dates.js +2 -0
  71. package/dist/format/markdown/document.js +2 -0
  72. package/dist/format/markdown/index.js +2 -0
  73. package/dist/format/markdown/mentions.js +2 -0
  74. package/dist/format/markdown/slug.d.ts +28 -0
  75. package/dist/format/markdown/slug.js +65 -0
  76. package/dist/format/markdown/yaml.js +2 -0
  77. package/dist/format/noteMarkdown.js +2 -0
  78. package/dist/format/parseWithFallback.d.ts +13 -0
  79. package/dist/format/parseWithFallback.js +98 -0
  80. package/dist/format/plaintext.d.ts +5 -0
  81. package/dist/format/plaintext.js +51 -0
  82. package/dist/format/postMarkdown.js +3 -1
  83. package/dist/format/sheetCellSpans.d.ts +95 -0
  84. package/dist/format/sheetCellSpans.js +223 -0
  85. package/dist/format/sheetSelection.d.ts +136 -0
  86. package/dist/format/sheetSelection.js +282 -0
  87. package/dist/format/taskUploadFilename.d.ts +6 -0
  88. package/dist/format/taskUploadFilename.js +13 -0
  89. package/dist/format/textStats.d.ts +23 -0
  90. package/dist/format/textStats.js +80 -0
  91. package/dist/format/wayfinder.d.ts +50 -0
  92. package/dist/format/wayfinder.js +203 -0
  93. package/dist/format/wikiLinks.d.ts +78 -0
  94. package/dist/format/wikiLinks.js +266 -0
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.js +20 -3
  97. package/dist/mcp-server.js +5 -2
  98. package/dist/opener.d.ts +23 -0
  99. package/dist/opener.js +26 -0
  100. package/dist/render.d.ts +132 -0
  101. package/dist/render.js +208 -0
  102. package/dist/shell-commands.d.ts +34 -0
  103. package/dist/shell-commands.js +108 -0
  104. package/dist/watch.d.ts +79 -0
  105. package/dist/watch.js +113 -0
  106. package/dist/web-url.d.ts +39 -0
  107. package/dist/web-url.js +63 -0
  108. package/package.json +7 -6
@@ -0,0 +1,442 @@
1
+ // GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
2
+ // Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
3
+ // Parsers for sfora's structured fenced blocks (```status / ```board / ```chat
4
+ // / ```sheet). Pure string -> data: no React, no DOM, no Convex. The renderer
5
+ // (src/components/editor/markdown-structured-block.tsx) imports these and only
6
+ // owns the presentation; the CLI, the agent fs, and tests can read the same
7
+ // blocks without pulling in the editor.
8
+ //
9
+ // Every parser returns `null` when the source doesn't look like its block, which
10
+ // is the renderer's signal to fall back to a plain code fence. That contract is
11
+ // load-bearing — a malformed block must stay visible and copyable, never vanish.
12
+ //
13
+ // Each parser also has a `…WithAccounting` twin that reports which lines it
14
+ // read and which it dropped. Those are the real implementations; the plain
15
+ // parsers are one-line wrappers that keep the data and discard the ledger, so
16
+ // the accounting cannot drift from the parse — it IS the parse. `blocks/
17
+ // dropClosure.ts` adjudicates every reason a drop can carry, and lint reads
18
+ // the drops instead of re-deriving them from a second copy of the grammar.
19
+ import { codeSpanMask } from "../lineGeometry.js";
20
+ import { parseWayfinderTickets, } from "../wayfinder.js";
21
+ import { isWholeLineDrop } from "./dropClosure.js";
22
+ export function cleanLines(source) {
23
+ return source
24
+ .split(/\r?\n/)
25
+ .map((line) => line.trim())
26
+ .filter(Boolean);
27
+ }
28
+ /** Content lines, and the blank-line drops that sit between them. */
29
+ function bodyLines(source) {
30
+ const lines = [];
31
+ const blanks = [];
32
+ source.split(/\r?\n/).forEach((raw, line) => {
33
+ const text = raw.trim();
34
+ if (text === "")
35
+ blanks.push({ line, text: "", reason: "blank-line" });
36
+ else
37
+ lines.push({ line, text });
38
+ });
39
+ return { lines, blanks };
40
+ }
41
+ const byLine = (a, b) => a.line - b.line;
42
+ /**
43
+ * Close one parse's books.
44
+ *
45
+ * A parser that returned null rendered nothing, so nothing was consumed and
46
+ * every content line is a `block-unread` drop — the bytes are still on screen
47
+ * inside the fallback code fence, which is what that verdict says. Partial
48
+ * drops are discarded along with the parse that produced them: there is no
49
+ * half-lost line when the whole block was never read.
50
+ */
51
+ function settle(lines, blanks, data, consumed, drops) {
52
+ if (data === null) {
53
+ const unread = lines.map((line) => ({
54
+ line: line.line,
55
+ text: line.text,
56
+ reason: "block-unread",
57
+ }));
58
+ return { data: null, consumed: [], drops: [...blanks, ...unread].sort(byLine) };
59
+ }
60
+ return {
61
+ data,
62
+ consumed: [...consumed].sort((a, b) => a - b),
63
+ drops: [...blanks, ...drops].sort(byLine),
64
+ };
65
+ }
66
+ /** The status block's header line. Only the first one is the block's state. */
67
+ const STATE_LINE = /^state:\s*(\S.*)$/i;
68
+ /** The `-` / `*` a status or chat entry may be written with. */
69
+ const ENTRY_BULLET = /^([-*])\s+/;
70
+ const TIMELINE_ENTRY = /^(?:(\d{1,2}:\d{2}|\d{4}-\d{2}-\d{2}T\S+)\s+)?@?([^:]+):\s*(.+)$/;
71
+ export function parseTimelineWithAccounting(source) {
72
+ const { lines, blanks } = bodyLines(source);
73
+ const consumed = [];
74
+ const drops = [];
75
+ const entries = [];
76
+ const stateIndex = lines.findIndex((line) => STATE_LINE.test(line.text));
77
+ const stateLine = stateIndex >= 0 ? STATE_LINE.exec(lines[stateIndex].text) : null;
78
+ lines.forEach((line, index) => {
79
+ // The FIRST `state:` line is the block's header. Any later one is a
80
+ // state CHANGE and stays visible as a marker entry in the timeline —
81
+ // neither an update signed by an actor called "state" (the old bug)
82
+ // nor silently dropped (the old fix): the reader shows every line the
83
+ // author wrote.
84
+ const asState = STATE_LINE.exec(line.text);
85
+ if (asState) {
86
+ consumed.push(line.line);
87
+ if (index !== stateIndex) {
88
+ entries.push({ actor: "", message: asState[1].trim(), kind: "state" });
89
+ }
90
+ return;
91
+ }
92
+ const bullet = ENTRY_BULLET.exec(line.text);
93
+ const value = line.text.replace(ENTRY_BULLET, "");
94
+ const match = TIMELINE_ENTRY.exec(value);
95
+ if (!match) {
96
+ drops.push({
97
+ line: line.line,
98
+ text: line.text,
99
+ reason: "status:unsigned-line",
100
+ });
101
+ return;
102
+ }
103
+ consumed.push(line.line);
104
+ if (bullet) {
105
+ drops.push({
106
+ line: line.line,
107
+ text: bullet[1],
108
+ reason: "entry-bullet-marker",
109
+ });
110
+ }
111
+ entries.push({
112
+ time: match[1],
113
+ actor: match[2].trim(),
114
+ message: match[3].trim(),
115
+ });
116
+ });
117
+ const data = !stateLine && entries.length === 0
118
+ ? null
119
+ : { state: stateLine?.[1].trim(), entries };
120
+ return settle(lines, blanks, data, consumed, drops);
121
+ }
122
+ export function parseTimeline(source) {
123
+ return parseTimelineWithAccounting(source).data;
124
+ }
125
+ const BOARD_HEADING = /^#{1,6}\s+(.+)$/;
126
+ const BOARD_ITEM = /^[-*]\s+(?:\[([^\]])\]\s+)?(.+)$/;
127
+ /**
128
+ * True for a line parseBoard keeps: a column heading or a card. A convenience
129
+ * for callers holding one line — the parser's own accounting is what lint and
130
+ * the closure suite read, so this is never the second opinion.
131
+ */
132
+ export function isBoardLine(line) {
133
+ const text = line.trim();
134
+ return BOARD_HEADING.test(text) || BOARD_ITEM.test(text);
135
+ }
136
+ export function parseBoardWithAccounting(source) {
137
+ const { lines, blanks } = bodyLines(source);
138
+ const consumed = [];
139
+ const drops = [];
140
+ const columns = [];
141
+ let current = null;
142
+ for (const line of lines) {
143
+ const heading = BOARD_HEADING.exec(line.text);
144
+ if (heading) {
145
+ consumed.push(line.line);
146
+ current = { title: heading[1].trim(), items: [] };
147
+ columns.push(current);
148
+ continue;
149
+ }
150
+ const item = BOARD_ITEM.exec(line.text);
151
+ if (item) {
152
+ consumed.push(line.line);
153
+ current ??= { title: "Items", items: [] };
154
+ if (!columns.includes(current))
155
+ columns.push(current);
156
+ current.items.push({
157
+ state: item[1]?.trim() ?? "",
158
+ text: item[2].trim(),
159
+ });
160
+ continue;
161
+ }
162
+ drops.push({
163
+ line: line.line,
164
+ text: line.text,
165
+ reason: "board:unparsed-line",
166
+ });
167
+ }
168
+ const data = columns.some((column) => column.items.length > 0)
169
+ ? columns
170
+ : null;
171
+ return settle(lines, blanks, data, consumed, drops);
172
+ }
173
+ export function parseBoard(source) {
174
+ return parseBoardWithAccounting(source).data;
175
+ }
176
+ // The clock-time alternative has to lead: without it the lazy actor group
177
+ // stops at the hour's colon and "09:04 @mara: hi" is signed by "09".
178
+ const CHAT_ENTRY = /^(?:(\d{1,2}:\d{2}|\d{4}-\d{2}-\d{2}T\S+)\s+)?@?(.+?)(?:\s+\(([^)]+)\))?\s*:\s*(.+)$/;
179
+ export function parseChatWithAccounting(source) {
180
+ const { lines, blanks } = bodyLines(source);
181
+ const consumed = [];
182
+ const drops = [];
183
+ const entries = [];
184
+ for (const line of lines) {
185
+ const bullet = ENTRY_BULLET.exec(line.text);
186
+ const value = line.text.replace(ENTRY_BULLET, "");
187
+ const match = CHAT_ENTRY.exec(value);
188
+ if (!match?.[2].trim() || !match[4].trim()) {
189
+ drops.push({
190
+ line: line.line,
191
+ text: line.text,
192
+ reason: "chat:unparsed-line",
193
+ });
194
+ continue;
195
+ }
196
+ consumed.push(line.line);
197
+ if (bullet) {
198
+ drops.push({
199
+ line: line.line,
200
+ text: bullet[1],
201
+ reason: "entry-bullet-marker",
202
+ });
203
+ }
204
+ entries.push({
205
+ time: match[1],
206
+ actor: match[2].trim(),
207
+ role: match[3]?.trim(),
208
+ message: match[4].trim(),
209
+ });
210
+ }
211
+ return settle(lines, blanks, entries.length > 0 ? entries : null, consumed, drops);
212
+ }
213
+ export function parseChat(source) {
214
+ return parseChatWithAccounting(source).data;
215
+ }
216
+ // GFM's delimiter cell: optional colons around one or more dashes. Demanding
217
+ // three would render `| - | - |` as a row of literal dashes.
218
+ const SHEET_SEPARATOR_CELL = /^:?-+:?$/;
219
+ // The escapable set, per CommonMark. A backslash before anything else is a
220
+ // literal backslash — Windows paths and regexes must survive intact.
221
+ const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
222
+ /** True when the character at `index` is preceded by an odd run of backslashes. */
223
+ function isEscapedAt(text, index) {
224
+ let slashes = 0;
225
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i--)
226
+ slashes++;
227
+ return slashes % 2 === 1;
228
+ }
229
+ /**
230
+ * True for a line parseSheet reads as a table row. GFM makes the outer pipes
231
+ * optional, so the test is "carries a pipe that would split it" — an escaped
232
+ * or code-spanned pipe is content, not structure.
233
+ */
234
+ export function isSheetRow(line) {
235
+ const text = line.trim();
236
+ const inCode = codeSpanMask(text);
237
+ for (let i = 0; i < text.length; i++) {
238
+ if (text[i] === "|" && !inCode[i] && !isEscapedAt(text, i))
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+ function sheetCells(line) {
244
+ let content = line.trim();
245
+ if (content.startsWith("|"))
246
+ content = content.slice(1);
247
+ if (content.endsWith("|") && !isEscapedAt(content, content.length - 1)) {
248
+ content = content.slice(0, -1);
249
+ }
250
+ const inCode = codeSpanMask(content);
251
+ const result = [];
252
+ let cell = "";
253
+ for (let i = 0; i < content.length; i++) {
254
+ const character = content[i];
255
+ if (character === "\\") {
256
+ const next = content[i + 1];
257
+ // GFM resolves `\|` before inline parsing, so it is a literal pipe even
258
+ // inside a code span — that is the only way to write one there. Every
259
+ // other escape belongs to inline parsing and stops at a code span.
260
+ if (next === "|" ||
261
+ (next !== undefined && !inCode[i] && ASCII_PUNCTUATION.test(next))) {
262
+ cell += next;
263
+ i++;
264
+ continue;
265
+ }
266
+ cell += character;
267
+ continue;
268
+ }
269
+ if (character === "|" && !inCode[i]) {
270
+ result.push(cell.trim());
271
+ cell = "";
272
+ continue;
273
+ }
274
+ cell += character;
275
+ }
276
+ result.push(cell.trim());
277
+ return result;
278
+ }
279
+ export function parseSheetWithAccounting(source) {
280
+ const { lines, blanks } = bodyLines(source);
281
+ const consumed = [];
282
+ const drops = [];
283
+ const rowLines = [];
284
+ for (const line of lines) {
285
+ if (isSheetRow(line.text))
286
+ rowLines.push(line);
287
+ else {
288
+ drops.push({
289
+ line: line.line,
290
+ text: line.text,
291
+ reason: "sheet:non-row-line",
292
+ });
293
+ }
294
+ }
295
+ if (rowLines.length < 2)
296
+ return settle(lines, blanks, null, [], drops);
297
+ const headers = sheetCells(rowLines[0].text);
298
+ const separator = sheetCells(rowLines[1].text);
299
+ const hasSeparator = separator.length === headers.length &&
300
+ separator.every((cell) => SHEET_SEPARATOR_CELL.test(cell));
301
+ const dataLines = rowLines.slice(hasSeparator ? 2 : 1);
302
+ const rows = dataLines.map((line) => sheetCells(line.text));
303
+ if (headers.length === 0 || rows.length === 0) {
304
+ return settle(lines, blanks, null, [], drops);
305
+ }
306
+ consumed.push(rowLines[0].line);
307
+ if (hasSeparator) {
308
+ // The delimiter row is alignment, not data: read into every column below
309
+ // and gone from the grid, which is why it signs as retained-by-capture.
310
+ drops.push({
311
+ line: rowLines[1].line,
312
+ text: rowLines[1].text,
313
+ reason: "sheet:delimiter-row",
314
+ });
315
+ }
316
+ const normalizedRows = rows.map((row, index) => {
317
+ const line = dataLines[index];
318
+ consumed.push(line.line);
319
+ if (row.length > headers.length) {
320
+ drops.push({
321
+ line: line.line,
322
+ text: row.slice(headers.length).join(" | "),
323
+ reason: "sheet:excess-cells",
324
+ });
325
+ }
326
+ return headers.map((_, column) => row[column] ?? "");
327
+ });
328
+ const columns = headers.map((header, index) => {
329
+ const marker = hasSeparator ? separator[index] : "";
330
+ const authoredAlignment = marker.startsWith(":") && marker.endsWith(":")
331
+ ? "center"
332
+ : marker.endsWith(":")
333
+ ? "right"
334
+ : marker.startsWith(":")
335
+ ? "left"
336
+ : null;
337
+ const values = normalizedRows.map((row) => row[index]).filter(Boolean);
338
+ const numericValues = values.filter((value) => /^(?:[-+]?[$€£]?\s*\d[\d,.]*(?:%|[KMBT])?|N\/?A|—)$/i.test(value));
339
+ // First column is the row label, so it always reads left; elsewhere a
340
+ // majority of numeric cells wins right alignment when the source didn't say.
341
+ const inferredAlignment = index > 0 &&
342
+ numericValues.length > 0 &&
343
+ numericValues.length >= Math.ceil(values.length / 2)
344
+ ? "right"
345
+ : "left";
346
+ return {
347
+ label: header || `Column ${index + 1}`,
348
+ alignment: index === 0
349
+ ? "left"
350
+ : (authoredAlignment ?? inferredAlignment),
351
+ };
352
+ });
353
+ return settle(lines, blanks, { columns, rows: normalizedRows }, consumed, drops);
354
+ }
355
+ export function parseSheet(source) {
356
+ return parseSheetWithAccounting(source).data;
357
+ }
358
+ // ── The map block ───────────────────────────────────────────────────
359
+ /** The map block's header line, mirroring the status block's `state:`. */
360
+ const MAP_DESTINATION_LINE = /^destination:\s*(\S.*)$/i;
361
+ /**
362
+ * True for a line parseMapBlock keeps: the destination header or a ticket in
363
+ * the wayfinder line grammar. Same standing as {@link isBoardLine} — a
364
+ * convenience, not the authority.
365
+ */
366
+ export function isMapLine(line) {
367
+ const text = line.trim();
368
+ return (MAP_DESTINATION_LINE.test(text) ||
369
+ parseWayfinderTickets(text).length === 1);
370
+ }
371
+ /**
372
+ * Parse a ```map block: an optional `destination:` header, then tickets in
373
+ * the wayfinder line grammar (`- [~] Name (type) <- Blocker`). Null when no
374
+ * ticket parses — the renderer's signal to fall back to a visible code fence,
375
+ * same contract as every other structured block.
376
+ */
377
+ export function parseMapBlockWithAccounting(source) {
378
+ const { lines, blanks } = bodyLines(source);
379
+ const consumed = [];
380
+ const drops = [];
381
+ const tickets = [];
382
+ let destination;
383
+ for (const line of lines) {
384
+ const header = MAP_DESTINATION_LINE.exec(line.text);
385
+ // Only the first destination line is the header; a second one is not a
386
+ // ticket either, so it falls through to the drop below.
387
+ if (header && destination === undefined) {
388
+ destination = header[1].trim();
389
+ consumed.push(line.line);
390
+ continue;
391
+ }
392
+ // The ticket grammar is line-local and stateless, so reading it a line at
393
+ // a time gives exactly the list a whole-section read would.
394
+ const parsed = parseWayfinderTickets(line.text);
395
+ if (parsed.length === 1) {
396
+ consumed.push(line.line);
397
+ tickets.push(parsed[0]);
398
+ continue;
399
+ }
400
+ drops.push({
401
+ line: line.line,
402
+ text: line.text,
403
+ reason: "map:unparsed-line",
404
+ });
405
+ }
406
+ const data = tickets.length === 0 ? null : { destination, tickets };
407
+ return settle(lines, blanks, data, consumed, drops);
408
+ }
409
+ /**
410
+ * Parse a ```map block: an optional `destination:` header, then tickets in
411
+ * the wayfinder line grammar (`- [~] Name (type) <- Blocker`). Null when no
412
+ * ticket parses — the renderer's signal to fall back to a visible code fence,
413
+ * same contract as every other structured block.
414
+ */
415
+ export function parseMapBlock(source) {
416
+ return parseMapBlockWithAccounting(source).data;
417
+ }
418
+ /** Every structured block's parser, by language. */
419
+ const ACCOUNTING_PARSERS = {
420
+ status: parseTimelineWithAccounting,
421
+ board: parseBoardWithAccounting,
422
+ chat: parseChatWithAccounting,
423
+ sheet: parseSheetWithAccounting,
424
+ map: parseMapBlockWithAccounting,
425
+ };
426
+ /**
427
+ * Parse a fence body as the block its language names, with the ledger. The
428
+ * one door lint and the closure suite use, so neither carries its own table of
429
+ * which parser reads which language.
430
+ */
431
+ export function parseStructuredBlock(language, source) {
432
+ return ACCOUNTING_PARSERS[language](source);
433
+ }
434
+ /**
435
+ * Body line indices whose bytes did not survive the parse at all. Partial
436
+ * drops are excluded by construction — their line rendered.
437
+ */
438
+ export function droppedLineNumbers(accounting) {
439
+ return accounting.drops
440
+ .filter((drop) => isWholeLineDrop(drop.reason))
441
+ .map((drop) => drop.line);
442
+ }
@@ -0,0 +1,8 @@
1
+ export declare const STRUCTURED_BLOCK_LANGUAGES: readonly ["status", "board", "chat", "sheet", "map"];
2
+ export declare const RICH_BLOCK_LANGUAGES: readonly ["mermaid", "status", "board", "chat", "sheet", "map"];
3
+ export type StructuredBlockLanguage = (typeof STRUCTURED_BLOCK_LANGUAGES)[number];
4
+ export type RichBlockLanguage = (typeof RICH_BLOCK_LANGUAGES)[number];
5
+ export declare const STRUCTURED_BLOCK_STARTERS: Record<StructuredBlockLanguage, string>;
6
+ export declare const RICH_BLOCK_STARTERS: Record<RichBlockLanguage, string>;
7
+ export declare function isStructuredMarkdownLanguage(language: string): language is StructuredBlockLanguage;
8
+ export declare function isRichMarkdownLanguage(language: string): language is RichBlockLanguage;
@@ -0,0 +1,30 @@
1
+ // GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
2
+ // Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
3
+ export const STRUCTURED_BLOCK_LANGUAGES = [
4
+ "status",
5
+ "board",
6
+ "chat",
7
+ "sheet",
8
+ "map",
9
+ ];
10
+ export const RICH_BLOCK_LANGUAGES = [
11
+ "mermaid",
12
+ ...STRUCTURED_BLOCK_LANGUAGES,
13
+ ];
14
+ export const STRUCTURED_BLOCK_STARTERS = {
15
+ status: "state: building\n- @you: Add an update",
16
+ board: "## In progress\n- [ ] Describe the active work\n\n## Review\n- [ ] Add a review item\n\n## Done\n- [x] Capture completed work",
17
+ chat: "- @you: Add a message",
18
+ sheet: "| Field | Value |\n| --- | --- |\n| Status | Building |\n| Owner | You |",
19
+ map: "destination: What shipping looks like\n- [x] Name the destination\n- [ ] Chart the first question <- Name the destination",
20
+ };
21
+ export const RICH_BLOCK_STARTERS = {
22
+ mermaid: "flowchart LR\n Source[Markdown] --> Result[Rendered block]",
23
+ ...STRUCTURED_BLOCK_STARTERS,
24
+ };
25
+ export function isStructuredMarkdownLanguage(language) {
26
+ return STRUCTURED_BLOCK_LANGUAGES.includes(language.toLowerCase());
27
+ }
28
+ export function isRichMarkdownLanguage(language) {
29
+ return RICH_BLOCK_LANGUAGES.includes(language.toLowerCase());
30
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Callouts — the GFM alert grammar, `> [!NOTE]`.
3
+ *
4
+ * A callout is an ordinary blockquote whose first line is a type marker. That
5
+ * is the whole trick: every reader that does not know about callouts still
6
+ * sees quoted prose, and the bytes stay a blockquote on disk. There is no new
7
+ * fence, no new frontmatter key, nothing an agent has to learn.
8
+ *
9
+ * > [!WARNING] Ship blocker
10
+ * > The migration has to run before the deploy.
11
+ *
12
+ * Fifteen types. The first five are GitHub's alert set — note, tip, important,
13
+ * warning, caution — and the other ten are the Obsidian set every vault and
14
+ * every other markdown tool already writes, so a document pasted in from one of
15
+ * them keeps its tone instead of falling back to a plain quote. The marker is
16
+ * written uppercase and read case-insensitively, because people type `[!note]`
17
+ * and pasted GitHub markdown says `[!NOTE]` — both are the same callout, and
18
+ * the first save canonicalizes.
19
+ *
20
+ * Two things beyond the type sit on the marker line, and both are round-tripped
21
+ * rather than normalised away:
22
+ *
23
+ * - an ALIAS. `[!WARN]`, `[!SUMMARY]`, `[!ERROR]` name a type by another
24
+ * word. They resolve to the canonical type for rendering, and the word the
25
+ * author actually typed is carried on `authoredAs` so the bytes come back
26
+ * unchanged. Normalising the spelling would rewrite a line the author did
27
+ * not touch, which is churn in a file two agents and a person share.
28
+ * - a FOLD marker. Obsidian's `[!NOTE]+` and `[!NOTE]-` say the callout is
29
+ * collapsible and whether it starts open. It is one character of grammar
30
+ * and it is the whole difference between a callout and a details block.
31
+ *
32
+ * Pure functions over lines: no DOM, no ProseMirror, no React. The reader
33
+ * (`src/lib/utils/markdown.ts`), the Tiptap node
34
+ * (`src/components/editor/extensions/callout.ts`), and the CLI all read the
35
+ * grammar from here so they cannot disagree about what a callout is.
36
+ */
37
+ export declare const CALLOUT_TYPES: readonly ["note", "tip", "important", "warning", "caution", "abstract", "info", "todo", "success", "question", "failure", "danger", "bug", "example", "quote"];
38
+ export type CalloutType = (typeof CALLOUT_TYPES)[number];
39
+ /**
40
+ * The other words people write for a type that already exists. Ported from
41
+ * open-knowledge's `callout-transformer.ts:41-71`, which took them from
42
+ * Obsidian, so a vault's markdown lands here meaning what it meant there.
43
+ *
44
+ * An alias is a spelling, not a type: it never widens `CalloutType`, and the
45
+ * authored word survives on `Callout.authoredAs` rather than in the enum.
46
+ */
47
+ export declare const CALLOUT_ALIASES: Readonly<Record<string, CalloutType>>;
48
+ /** Obsidian's foldable marker: `+` starts open, `-` starts collapsed. */
49
+ export type CalloutFold = "+" | "-";
50
+ export interface Callout {
51
+ type: CalloutType;
52
+ /** Custom heading on the marker line. Absent means "use the type's label". */
53
+ title?: string;
54
+ /** Everything below the marker line, quote markers already removed. */
55
+ body: string;
56
+ /**
57
+ * The token exactly as authored, when it is not the canonical type name —
58
+ * `WARN` for a warning, `Summary` for an abstract. Absent when the author
59
+ * already wrote the canonical word in any case, which is what keeps
60
+ * `[!note]` canonicalizing to `[!NOTE]` as it always has.
61
+ */
62
+ authoredAs?: string;
63
+ /** Present when the marker carried `+` or `-`, which makes it collapsible. */
64
+ fold?: CalloutFold;
65
+ }
66
+ export interface CalloutMarkerOptions {
67
+ /** The authored spelling to write back instead of the canonical word. */
68
+ authoredAs?: string | null;
69
+ fold?: CalloutFold | null;
70
+ }
71
+ /**
72
+ * The marker as written: uppercase, so `[!NOTE]` is what lands on disk — unless
73
+ * the author spelled the type another way, in which case their word goes back.
74
+ *
75
+ * `authoredAs` is checked, not trusted. It is an attribute by the time it gets
76
+ * here (the editor carries it on the node), and an attribute a paste or a
77
+ * command could have set to anything; a marker that no longer resolves to this
78
+ * callout's type would come back as a plain quote with a stray bracket. So a
79
+ * spelling that does not resolve to `type` is discarded and the canonical word
80
+ * is written instead — the tone is preserved and only the churn is paid.
81
+ */
82
+ export declare function calloutMarker(type: CalloutType, options?: CalloutMarkerOptions): string;
83
+ /** True for the fifteen canonical type names, in any case. Aliases are not types. */
84
+ export declare function isCalloutType(value: string): value is CalloutType;
85
+ /**
86
+ * The token on a marker line — canonical or alias — resolved to the type that
87
+ * renders it, or null when it names nothing. Every caller that has to decide
88
+ * "is this a callout?" asks this rather than `isCalloutType`, because an alias
89
+ * IS a callout and only differs in how it is spelled.
90
+ */
91
+ export declare function resolveCalloutType(token: string): CalloutType | null;
92
+ /**
93
+ * Drop one level of `>` quoting. A single space after the marker is part of
94
+ * the marker, not the content — `> indented` keeps one space.
95
+ */
96
+ export declare function stripQuoteMarkers(lines: readonly string[]): string[];
97
+ /**
98
+ * Read a callout out of the lines of a blockquote.
99
+ *
100
+ * Accepts either raw source lines (`> [!NOTE]`) or lines whose quote markers a
101
+ * caller has already stripped (`[!NOTE]`) — the reader strips as it scans, the
102
+ * editor does not. The two are told apart by looking at the block as a whole:
103
+ * if every non-blank line is quoted, one level comes off. That way a callout
104
+ * whose body contains a nested quote survives either way round.
105
+ *
106
+ * Returns null for anything that is not a callout, which is the signal to
107
+ * render an ordinary blockquote.
108
+ */
109
+ export declare function parseCallout(blockquoteLines: readonly string[]): Callout | null;
110
+ /**
111
+ * Index, within the same lines, of the callout body's first line — or -1 when
112
+ * the block is not a callout. A caller that re-parses `body` as markdown needs
113
+ * it to map a node back to the document line it came from; the reader joins
114
+ * its checkboxes to the document's line scan that way. Kept off `Callout`
115
+ * itself because the type is a value the editor and CLI construct by hand.
116
+ */
117
+ export declare function calloutBodyLine(blockquoteLines: readonly string[]): number;
118
+ /**
119
+ * Write a callout back out. The inverse of `parseCallout` on the canonical
120
+ * form, and the only place the serialized shape is spelled: one blockquote,
121
+ * marker line first, body quoted line by line. A blank body line is a bare
122
+ * `>` — no trailing space, so the bytes survive editors that strip them.
123
+ *
124
+ * The marker line is rebuilt from the three fields that spell it — type,
125
+ * authored word, fold — rather than kept as a string, so a callout the editor
126
+ * constructed by hand and one parsed off disk are written by the same code.
127
+ */
128
+ export declare function calloutToMarkdown(callout: Callout): string;