sfora-cli 0.9.0 → 0.10.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 (69) hide show
  1. package/README.md +8 -6
  2. package/dist/SforaFs.js +270 -4
  3. package/dist/api-client.d.ts +47 -1
  4. package/dist/api-client.js +60 -3
  5. package/dist/cli.js +6 -3
  6. package/dist/format/__tests__/byteStable.d.ts +5 -0
  7. package/dist/format/__tests__/byteStable.js +64 -0
  8. package/dist/format/blocks/dropClosure.d.ts +72 -0
  9. package/dist/format/blocks/dropClosure.js +186 -0
  10. package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
  11. package/dist/format/blocks/markdown-block-catalog.js +162 -0
  12. package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
  13. package/dist/format/blocks/markdown-block-ids.mjs +25 -0
  14. package/dist/format/blocks/parsers.d.ts +105 -0
  15. package/dist/format/blocks/parsers.js +442 -0
  16. package/dist/format/blocks/structured-block-schema.d.ts +8 -0
  17. package/dist/format/blocks/structured-block-schema.js +30 -0
  18. package/dist/format/callout.d.ts +66 -0
  19. package/dist/format/callout.js +130 -0
  20. package/dist/format/cardMarkdown.d.ts +2 -0
  21. package/dist/format/cardMarkdown.js +10 -0
  22. package/dist/format/checklist.d.ts +34 -0
  23. package/dist/format/checklist.js +151 -0
  24. package/dist/format/index.d.ts +18 -4
  25. package/dist/format/index.js +24 -4
  26. package/dist/format/lineGeometry.d.ts +70 -0
  27. package/dist/format/lineGeometry.js +324 -0
  28. package/dist/format/lint/index.d.ts +20 -0
  29. package/dist/format/lint/index.js +22 -0
  30. package/dist/format/lint/lintSource.d.ts +36 -0
  31. package/dist/format/lint/lintSource.js +154 -0
  32. package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
  33. package/dist/format/lint/rules/broken-wiki-link.js +45 -0
  34. package/dist/format/lint/rules/index.d.ts +10 -0
  35. package/dist/format/lint/rules/index.js +26 -0
  36. package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
  37. package/dist/format/lint/rules/malformed-callout.js +79 -0
  38. package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
  39. package/dist/format/lint/rules/malformed-checklist.js +60 -0
  40. package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
  41. package/dist/format/lint/rules/malformed-frontmatter.js +93 -0
  42. package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
  43. package/dist/format/lint/rules/malformed-structured-block.js +134 -0
  44. package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
  45. package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
  46. package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
  47. package/dist/format/lint/rules/orphan-reference.js +87 -0
  48. package/dist/format/lint/types.d.ts +80 -0
  49. package/dist/format/lint/types.js +16 -0
  50. package/dist/format/markdown/dates.js +2 -0
  51. package/dist/format/markdown/document.js +2 -0
  52. package/dist/format/markdown/index.js +2 -0
  53. package/dist/format/markdown/mentions.js +2 -0
  54. package/dist/format/markdown/slug.js +2 -0
  55. package/dist/format/markdown/yaml.js +2 -0
  56. package/dist/format/noteMarkdown.js +2 -0
  57. package/dist/format/parseWithFallback.d.ts +13 -0
  58. package/dist/format/parseWithFallback.js +98 -0
  59. package/dist/format/plaintext.d.ts +5 -0
  60. package/dist/format/plaintext.js +41 -0
  61. package/dist/format/postMarkdown.js +3 -1
  62. package/dist/format/taskUploadFilename.d.ts +6 -0
  63. package/dist/format/taskUploadFilename.js +13 -0
  64. package/dist/format/wayfinder.d.ts +50 -0
  65. package/dist/format/wayfinder.js +203 -0
  66. package/dist/format/wikiLinks.d.ts +19 -0
  67. package/dist/format/wikiLinks.js +80 -0
  68. package/dist/mcp-server.js +5 -2
  69. package/package.json +7 -6
@@ -0,0 +1,130 @@
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
+ /**
4
+ * Callouts — the GFM alert grammar, `> [!NOTE]`.
5
+ *
6
+ * A callout is an ordinary blockquote whose first line is a type marker. That
7
+ * is the whole trick: every reader that does not know about callouts still
8
+ * sees quoted prose, and the bytes stay a blockquote on disk. There is no new
9
+ * fence, no new frontmatter key, nothing an agent has to learn.
10
+ *
11
+ * > [!WARNING] Ship blocker
12
+ * > The migration has to run before the deploy.
13
+ *
14
+ * Five types, matching GitHub: note, tip, important, warning, caution. The
15
+ * marker is written uppercase and read case-insensitively, because people type
16
+ * `[!note]` and pasted GitHub markdown says `[!NOTE]` — both are the same
17
+ * callout, and the first save canonicalizes.
18
+ *
19
+ * Pure functions over lines: no DOM, no ProseMirror, no React. The reader
20
+ * (`src/lib/utils/markdown.ts`), the Tiptap node
21
+ * (`src/components/editor/extensions/callout.ts`), and the CLI all read the
22
+ * grammar from here so they cannot disagree about what a callout is.
23
+ */
24
+ export const CALLOUT_TYPES = [
25
+ "note",
26
+ "tip",
27
+ "important",
28
+ "warning",
29
+ "caution",
30
+ ];
31
+ const MARKER = /^\[!([A-Za-z]+)\][ \t]*(.*)$/;
32
+ /** The marker as written: uppercase, so `[!NOTE]` is what lands on disk. */
33
+ export function calloutMarker(type) {
34
+ return `[!${type.toUpperCase()}]`;
35
+ }
36
+ export function isCalloutType(value) {
37
+ return CALLOUT_TYPES.includes(value.toLowerCase());
38
+ }
39
+ /**
40
+ * Drop one level of `>` quoting. A single space after the marker is part of
41
+ * the marker, not the content — `> indented` keeps one space.
42
+ */
43
+ export function stripQuoteMarkers(lines) {
44
+ return lines.map((line) => line.replace(/^[ \t]{0,3}>[ ]?/, ""));
45
+ }
46
+ /**
47
+ * Read a callout out of the lines of a blockquote.
48
+ *
49
+ * Accepts either raw source lines (`> [!NOTE]`) or lines whose quote markers a
50
+ * caller has already stripped (`[!NOTE]`) — the reader strips as it scans, the
51
+ * editor does not. The two are told apart by looking at the block as a whole:
52
+ * if every non-blank line is quoted, one level comes off. That way a callout
53
+ * whose body contains a nested quote survives either way round.
54
+ *
55
+ * Returns null for anything that is not a callout, which is the signal to
56
+ * render an ordinary blockquote.
57
+ */
58
+ export function parseCallout(blockquoteLines) {
59
+ return readCallout(blockquoteLines)?.callout ?? null;
60
+ }
61
+ /**
62
+ * Index, within the same lines, of the callout body's first line — or -1 when
63
+ * the block is not a callout. A caller that re-parses `body` as markdown needs
64
+ * it to map a node back to the document line it came from; the reader joins
65
+ * its checkboxes to the document's line scan that way. Kept off `Callout`
66
+ * itself because the type is a value the editor and CLI construct by hand.
67
+ */
68
+ export function calloutBodyLine(blockquoteLines) {
69
+ return readCallout(blockquoteLines)?.bodyLine ?? -1;
70
+ }
71
+ function readCallout(blockquoteLines) {
72
+ const nonBlank = blockquoteLines.filter((line) => line.trim() !== "");
73
+ if (nonBlank.length === 0)
74
+ return null;
75
+ const quoted = nonBlank.every((line) => /^[ \t]{0,3}>/.test(line));
76
+ const lines = quoted ? stripQuoteMarkers(blockquoteLines) : [...blockquoteLines];
77
+ // The emptiness check above ran on the RAW lines, and a line can be
78
+ // non-blank before its `>` marker comes off and blank after — a bare `>` is
79
+ // the whole of CommonMark's empty blockquote. Without this the findIndex
80
+ // returns -1 and the deref below throws, taking down the render of any post
81
+ // that contains one.
82
+ const first = lines.findIndex((line) => line.trim() !== "");
83
+ if (first === -1)
84
+ return null;
85
+ const match = MARKER.exec(lines[first].trim());
86
+ if (!match)
87
+ return null;
88
+ const keyword = match[1].toLowerCase();
89
+ if (!isCalloutType(keyword))
90
+ return null;
91
+ const title = match[2].trim();
92
+ // Same trim as `trimBlankEdges`, unrolled so the leading blanks it drops can
93
+ // be added to the body's line index rather than silently lost.
94
+ const rest = lines.slice(first + 1).map((line) => line.replace(/\s+$/, ""));
95
+ let start = 0;
96
+ while (start < rest.length && rest[start] === "")
97
+ start++;
98
+ let end = rest.length;
99
+ while (end > start && rest[end - 1] === "")
100
+ end--;
101
+ const body = rest.slice(start, end).join("\n");
102
+ return {
103
+ callout: title ? { type: keyword, title, body } : { type: keyword, body },
104
+ bodyLine: first + 1 + start,
105
+ };
106
+ }
107
+ /**
108
+ * Write a callout back out. The inverse of `parseCallout` on the canonical
109
+ * form, and the only place the serialized shape is spelled: one blockquote,
110
+ * marker line first, body quoted line by line. A blank body line is a bare
111
+ * `>` — no trailing space, so the bytes survive editors that strip them.
112
+ */
113
+ export function calloutToMarkdown(callout) {
114
+ const title = callout.title?.trim();
115
+ const head = title
116
+ ? `> ${calloutMarker(callout.type)} ${title}`
117
+ : `> ${calloutMarker(callout.type)}`;
118
+ const body = trimBlankEdges(callout.body.split("\n"));
119
+ if (body.length === 0)
120
+ return head;
121
+ return [head, ...body.map((line) => (line === "" ? ">" : `> ${line}`))].join("\n");
122
+ }
123
+ function trimBlankEdges(lines) {
124
+ const out = lines.map((line) => line.replace(/\s+$/, ""));
125
+ while (out.length > 0 && out[0] === "")
126
+ out.shift();
127
+ while (out.length > 0 && out[out.length - 1] === "")
128
+ out.pop();
129
+ return out;
130
+ }
@@ -14,6 +14,8 @@ export interface MarkdownCardInput {
14
14
  labels?: string[];
15
15
  kind?: "question";
16
16
  resolution?: string;
17
+ scope?: "out";
18
+ blockedBy?: number[];
17
19
  }
18
20
  export interface MarkdownBoardRef {
19
21
  _id: string;
@@ -1,3 +1,5 @@
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).
1
3
  // Pure markdown <-> kanban card serialization for the agent FS API (/v1/fs).
2
4
  //
3
5
  // Mirrors postMarkdown.ts: frontmatter/YAML, mentions, slugs, dates, and the
@@ -68,9 +70,17 @@ export function cardToMarkdown(card, board, column, commentsCount) {
68
70
  ["kind", card.kind], // undefined for ordinary tasks → skipped
69
71
  ["status", card.status],
70
72
  ["resolution", card.resolution], // set once a question is decided
73
+ ["scope", card.scope], // "out" for a question ruled out of scope
71
74
  ["priority", card.priority ?? "none"],
72
75
  ["assignees", card.assignees ?? []],
73
76
  ["labels", card.labels ?? []],
77
+ // Emitted only when edges exist — key absent means "no blockers".
78
+ [
79
+ "blocked-by",
80
+ card.blockedBy && card.blockedBy.length > 0
81
+ ? card.blockedBy.map(String)
82
+ : undefined,
83
+ ],
74
84
  ["due", toDateOnly(card.dueAt)],
75
85
  ["created", toISO(card._creationTime)],
76
86
  ["lastActivityAt", toISO(card.lastActivityAt)],
@@ -0,0 +1,34 @@
1
+ export declare const CHECKBOX_RE: RegExp;
2
+ /** One checkbox, as both the scan and the reader see it. */
3
+ export interface ChecklistItem {
4
+ /** 0-based line index in the body. The reader's join key. */
5
+ line: number;
6
+ /** 0-based position in document order — the index `toggleChecklistItem` takes. */
7
+ index: number;
8
+ checked: boolean;
9
+ /** The item's text, markup intact. */
10
+ text: string;
11
+ }
12
+ export interface ChecklistProgress {
13
+ done: number;
14
+ total: number;
15
+ }
16
+ /**
17
+ * Every checkbox in a body, in document order.
18
+ *
19
+ * Non-rendering bytes are skipped: a `- [ ]` inside a ``` block, an inline
20
+ * code span or an HTML comment is a sample, not a task, and counting it is how
21
+ * a click on the first real checkbox ends up rewriting a line inside someone's
22
+ * code. That judgement comes from `lineGeometry`'s shared mask — the same one
23
+ * lint and the block parsers read — so nothing here decides on its own what
24
+ * renders. A fence inside a blockquote or a callout counts too, since a quoted
25
+ * `- [ ]` is otherwise a task.
26
+ *
27
+ * Indented code is skipped the only way a line scan can tell: four spaces of
28
+ * indent with no enclosing list item above it is a code block, whereas the
29
+ * same indent under a bullet is a nested list. Bodies carry no frontmatter —
30
+ * callers hand us a body, same as the reader — so the fence scan starts at 0.
31
+ */
32
+ export declare function scanChecklist(body: string | undefined | null): ChecklistItem[];
33
+ export declare function checklistProgress(body: string | undefined | null): ChecklistProgress;
34
+ export declare function toggleChecklistItem(body: string, index: number): string;
@@ -0,0 +1,151 @@
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
+ // Checklist items are plain markdown checkboxes — `- [ ]` / `- [x]` — in an
4
+ // entity body. There is no separate sub-task entity: the body IS the list, so
5
+ // progress and toggling are string operations over it.
6
+ //
7
+ // ONE scan answers all three questions — which lines are checkboxes, how many
8
+ // are done, and which line index N addresses. The reader (src/lib/utils/
9
+ // markdown.ts) walks the mdast tree instead, and joins back to this scan BY
10
+ // LINE, so a rendered checkbox and the line a toggle rewrites are the same
11
+ // line by construction rather than by two counters agreeing.
12
+ //
13
+ // Pure: no React, no Convex, no Node. The web app re-exports this from
14
+ // src/lib/subtasks.ts; the mobile preprocessor keeps its own copy because
15
+ // packages/mobile installs outside the pnpm workspace.
16
+ import { lineText, maskNonRenderingLines } from "./lineGeometry.js";
17
+ /** The blockquote markers opening a line — `>` runs, callout syntax included. */
18
+ const QUOTE_PREFIX_RE = /^(?:[ \t]{0,3}>[ \t]?)*/;
19
+ /**
20
+ * Non-rendering lines that only a blockquote-relative scan can see.
21
+ *
22
+ * The mask reads the raw lines, where `> ```` ``` ```` is not a fence open at
23
+ * all, so a sample checkbox inside a quoted or callout fence would count as a
24
+ * task. Quote markers therefore come off before the scan — but only within a
25
+ * contiguous run of lines at the SAME quote depth, because a blockquote is its
26
+ * own container: an unclosed fence inside one ends with the quote and must not
27
+ * swallow the real checkboxes that follow it at top level.
28
+ *
29
+ * Depth 0 is left to the caller's raw mask, which already reads those lines
30
+ * with their `>`-looking content intact.
31
+ */
32
+ function quotedNonRenderingMask(lines) {
33
+ const mask = new Array(lines.length).fill(false);
34
+ const depths = lines.map((_, i) => (lineText(lines, i).match(QUOTE_PREFIX_RE)[0].match(/>/g) ?? []).length);
35
+ for (let start = 0; start < lines.length; start++) {
36
+ const depth = depths[start];
37
+ if (depth === 0)
38
+ continue;
39
+ let end = start;
40
+ while (end + 1 < lines.length && depths[end + 1] === depth)
41
+ end++;
42
+ const stripped = [];
43
+ for (let i = start; i <= end; i++) {
44
+ stripped.push(lineText(lines, i).replace(QUOTE_PREFIX_RE, ""));
45
+ }
46
+ const quoted = maskNonRenderingLines(stripped);
47
+ for (let i = 0; i < quoted.length; i++) {
48
+ if (quoted[i].trim() === "")
49
+ mask[start + i] = true;
50
+ }
51
+ start = end;
52
+ }
53
+ return mask;
54
+ }
55
+ // A checkbox line: optional blockquote markers, then a bullet (`-`/`*`/`+`) or
56
+ // an ordered marker (`1.`/`1)`), then the state box. Every one of these forms
57
+ // is a task item to GFM, so every one of them is addressable here — a reader
58
+ // that renders `1. [ ] ship` interactive and a toggler whose regex cannot see
59
+ // it is the divergence this grammar exists to close.
60
+ //
61
+ // Captures the prefix, the state character, the gap, and the item text, so a
62
+ // toggle rebuilds the line byte for byte apart from the box it flipped.
63
+ export const CHECKBOX_RE = /^((?:[ \t]{0,3}>[ \t]?)*[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+)\[([ xX])\]([ \t]+)(.*)$/;
64
+ // The same opening, without the box — any list item at all. Establishes the
65
+ // list context that tells an indented nested checkbox from indented code.
66
+ const BULLET_RE = /^(?:[ \t]{0,3}>[ \t]?)*([ \t]*)(?:[-*+]|\d{1,9}[.)])[ \t]+/;
67
+ /** Indentation of a line's content, blockquote markers not counted. */
68
+ const INDENT_RE = /^(?:[ \t]{0,3}>[ \t]?)*([ \t]*)/;
69
+ /**
70
+ * Every checkbox in a body, in document order.
71
+ *
72
+ * Non-rendering bytes are skipped: a `- [ ]` inside a ``` block, an inline
73
+ * code span or an HTML comment is a sample, not a task, and counting it is how
74
+ * a click on the first real checkbox ends up rewriting a line inside someone's
75
+ * code. That judgement comes from `lineGeometry`'s shared mask — the same one
76
+ * lint and the block parsers read — so nothing here decides on its own what
77
+ * renders. A fence inside a blockquote or a callout counts too, since a quoted
78
+ * `- [ ]` is otherwise a task.
79
+ *
80
+ * Indented code is skipped the only way a line scan can tell: four spaces of
81
+ * indent with no enclosing list item above it is a code block, whereas the
82
+ * same indent under a bullet is a nested list. Bodies carry no frontmatter —
83
+ * callers hand us a body, same as the reader — so the fence scan starts at 0.
84
+ */
85
+ export function scanChecklist(body) {
86
+ if (!body)
87
+ return [];
88
+ const lines = body.split("\n");
89
+ const masked = maskNonRenderingLines(lines);
90
+ const quoted = quotedNonRenderingMask(lines);
91
+ const out = [];
92
+ // Indent of the innermost list item still open above the cursor, or null
93
+ // when we are at top level. A non-blank unindented line that is not itself a
94
+ // list item closes the list.
95
+ let openIndent = null;
96
+ for (let i = 0; i < lines.length; i++) {
97
+ if (quoted[i])
98
+ continue;
99
+ // Structure is read off the MASKED line, so a fenced, commented-out or
100
+ // code-spanned checkbox is not a list item and not a task; the item's own
101
+ // text still comes off the raw line, markup intact.
102
+ const line = lineText(masked, i);
103
+ if (line.trim() === "")
104
+ continue;
105
+ const bullet = BULLET_RE.exec(line);
106
+ if (!bullet) {
107
+ if (INDENT_RE.exec(line)[1].length === 0)
108
+ openIndent = null;
109
+ continue;
110
+ }
111
+ const indent = bullet[1].length;
112
+ if (indent >= 4 && (openIndent === null || openIndent >= indent))
113
+ continue;
114
+ openIndent = indent;
115
+ const raw = lineText(lines, i);
116
+ const box = CHECKBOX_RE.exec(raw);
117
+ // The state box has to be one of the bytes that render. `- [ ] a` inside
118
+ // an inline code span survives the bullet test above — the marker is
119
+ // outside the span — but the box itself is masked, and a toggle would be
120
+ // rewriting someone's sample.
121
+ if (!box || line[box[1].length] !== "[")
122
+ continue;
123
+ out.push({
124
+ line: i,
125
+ index: out.length,
126
+ checked: box[2] !== " ",
127
+ text: box[4],
128
+ });
129
+ }
130
+ return out;
131
+ }
132
+ // Count checkbox items in a body. total === 0 means "no checklist".
133
+ export function checklistProgress(body) {
134
+ const items = scanChecklist(body);
135
+ return { done: items.filter((item) => item.checked).length, total: items.length };
136
+ }
137
+ // Toggle the Nth checkbox (0-based, in document order) and return the new body.
138
+ // Returns the original body unchanged if the index is out of range.
139
+ export function toggleChecklistItem(body, index) {
140
+ const item = scanChecklist(body)[index];
141
+ if (!item)
142
+ return body;
143
+ const lines = body.split("\n");
144
+ const raw = lines[item.line];
145
+ const eol = raw.endsWith("\r") ? "\r" : "";
146
+ const m = CHECKBOX_RE.exec(eol ? raw.slice(0, -1) : raw);
147
+ if (!m)
148
+ return body;
149
+ lines[item.line] = `${m[1]}[${item.checked ? " " : "x"}]${m[3]}${m[4]}${eol}`;
150
+ return lines.join("\n");
151
+ }
@@ -1,13 +1,27 @@
1
1
  /**
2
2
  * The sfora markdown format — the single source of truth for how posts, tasks
3
- * (cards), and docs (notes) serialize to/from markdown files.
3
+ * (cards), and docs (notes) serialize to/from markdown files, plus the shared
4
+ * grammar every surface used to reimplement (wiki links, checklists,
5
+ * plaintext flattening, structured blocks).
4
6
  *
5
7
  * This package is canonical: the Convex backend re-exports these modules
6
- * (convex/lib/* are thin shims), so cloud files and local `.sfora/` files are
7
- * byte-identical by construction. Round-trip tests live in convex/lib/__tests__
8
- * and exercise this code through the shims.
8
+ * (convex/lib/* are thin shims), the published CLI syncs this source into its
9
+ * own tree at build time (packages/sfora/scripts/sync-format.mjs), and the web
10
+ * app reaches it through the `@sfora/markdown` alias — so cloud files, local
11
+ * `.sfora/` files, and everything in between are byte-identical by
12
+ * construction. Round-trip and parse-health tests live in `src/__tests__/`.
9
13
  */
10
14
  export * from "./markdown/index.js";
11
15
  export * from "./cardMarkdown.js";
12
16
  export * from "./postMarkdown.js";
13
17
  export * from "./noteMarkdown.js";
18
+ export * from "./wikiLinks.js";
19
+ export * from "./plaintext.js";
20
+ export * from "./checklist.js";
21
+ export * from "./parseWithFallback.js";
22
+ export * from "./callout.js";
23
+ export * from "./lint/index.js";
24
+ export * from "./blocks/structured-block-schema.js";
25
+ export * from "./blocks/markdown-block-catalog.js";
26
+ export * from "./blocks/dropClosure.js";
27
+ export * from "./wayfinder.js";
@@ -1,13 +1,33 @@
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).
1
3
  /**
2
4
  * The sfora markdown format — the single source of truth for how posts, tasks
3
- * (cards), and docs (notes) serialize to/from markdown files.
5
+ * (cards), and docs (notes) serialize to/from markdown files, plus the shared
6
+ * grammar every surface used to reimplement (wiki links, checklists,
7
+ * plaintext flattening, structured blocks).
4
8
  *
5
9
  * This package is canonical: the Convex backend re-exports these modules
6
- * (convex/lib/* are thin shims), so cloud files and local `.sfora/` files are
7
- * byte-identical by construction. Round-trip tests live in convex/lib/__tests__
8
- * and exercise this code through the shims.
10
+ * (convex/lib/* are thin shims), the published CLI syncs this source into its
11
+ * own tree at build time (packages/sfora/scripts/sync-format.mjs), and the web
12
+ * app reaches it through the `@sfora/markdown` alias — so cloud files, local
13
+ * `.sfora/` files, and everything in between are byte-identical by
14
+ * construction. Round-trip and parse-health tests live in `src/__tests__/`.
9
15
  */
10
16
  export * from "./markdown/index.js";
11
17
  export * from "./cardMarkdown.js";
12
18
  export * from "./postMarkdown.js";
13
19
  export * from "./noteMarkdown.js";
20
+ export * from "./wikiLinks.js";
21
+ export * from "./plaintext.js";
22
+ export * from "./checklist.js";
23
+ export * from "./parseWithFallback.js";
24
+ export * from "./callout.js";
25
+ export * from "./lint/index.js";
26
+ export * from "./blocks/structured-block-schema.js";
27
+ export * from "./blocks/markdown-block-catalog.js";
28
+ export * from "./blocks/dropClosure.js";
29
+ export * from "./wayfinder.js";
30
+ // htmlToMarkdown and parseMarkdownAst are deliberately NOT in the barrel:
31
+ // they carry the package's only heavy dependencies (unified/rehype/remark)
32
+ // and are excluded from the CLI sync — consumers import the subpaths
33
+ // `@sfora/markdown/htmlToMarkdown` and `@sfora/markdown/parseMarkdownAst`.
@@ -0,0 +1,70 @@
1
+ /** A fenced code block, delimiters included. */
2
+ export interface FenceRegion {
3
+ /** Line index of the opening delimiter. */
4
+ open: number;
5
+ /** Line index of the closing delimiter, or of the last line when unclosed. */
6
+ close: number;
7
+ /** True when a closing delimiter was actually found. */
8
+ closed: boolean;
9
+ /** First word of the info string, lowercased. Empty for a bare fence. */
10
+ lang: string;
11
+ /** Leading spaces on the opening delimiter — content is dedented by these. */
12
+ indent: number;
13
+ /** First content line index (exclusive of the delimiter). */
14
+ contentStart: number;
15
+ /** One past the last content line. */
16
+ contentEnd: number;
17
+ }
18
+ /** The line without its trailing `\r`, so `$`-anchored rules work on CRLF. */
19
+ export declare function lineText(lines: readonly string[], i: number): string;
20
+ /**
21
+ * Frontmatter extent, or null. Only a document that OPENS with `---` has
22
+ * frontmatter — a `---` further down is a thematic break, and treating it as a
23
+ * fence would make every horizontal rule the start of a lint dead zone.
24
+ */
25
+ export declare function frontmatterExtent(lines: readonly string[]): {
26
+ open: number;
27
+ close: number;
28
+ } | null;
29
+ /**
30
+ * Every fenced code block in the document, in order. `from` skips the
31
+ * frontmatter block, whose contents are not markdown either.
32
+ */
33
+ export declare function fencedRegions(lines: readonly string[], from?: number): FenceRegion[];
34
+ /** True for every line covered by a fenced block, delimiters included. */
35
+ export declare function fenceMask(lines: readonly string[], from?: number): boolean[];
36
+ /**
37
+ * True where a line is inside an inline code span, delimiters included.
38
+ *
39
+ * Rules and parsers mask their matches against this so a token quoted as
40
+ * `` `[[c:1]]` `` — the way documentation talks ABOUT the grammar — is never
41
+ * read as using it, and so a pipe inside `` `a|b` `` is content rather than a
42
+ * cell boundary. Line-local by design: a code span cannot cross a blank line,
43
+ * and the callers hold one line at a time.
44
+ */
45
+ export declare function codeSpanMask(line: string): boolean[];
46
+ /**
47
+ * The non-rendering mask, line by line. Each returned line has exactly the
48
+ * length of the line it masks, so `masked[i][k]` and `lines[i][k]` are the
49
+ * same byte position.
50
+ *
51
+ * `from` skips a leading frontmatter block for the FENCE scan only, mirroring
52
+ * {@link fenceMask} — whether the frontmatter itself renders is the caller's
53
+ * question, and lint already answers it with `inFrontmatter`.
54
+ */
55
+ export declare function maskNonRenderingLines(lines: readonly string[], from?: number): string[];
56
+ /**
57
+ * The whole source, masked. `masked.length === source.length` always: the two
58
+ * strings are the same document, one of them with the non-markdown blanked
59
+ * out, and every offset holds across both.
60
+ */
61
+ export declare function maskNonRenderingContexts(source: string): string;
62
+ /**
63
+ * True when the source span `[start, end)` holds bytes but no rendering ones —
64
+ * the predicate a consumer asks before trusting anything it found by scanning.
65
+ *
66
+ * A span that was blank to begin with is not "non-rendering", it is empty, and
67
+ * answering true for it would demote every piece of whitespace in the
68
+ * document.
69
+ */
70
+ export declare function isNonRenderingRange(source: string, masked: string, start: number, end: number): boolean;