sfora-cli 0.8.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.
- package/README.md +8 -6
- package/dist/SforaFs.js +270 -4
- package/dist/api-client.d.ts +47 -1
- package/dist/api-client.js +60 -3
- package/dist/cli.js +24 -11
- package/dist/format/__tests__/byteStable.d.ts +5 -0
- package/dist/format/__tests__/byteStable.js +64 -0
- package/dist/format/blocks/dropClosure.d.ts +72 -0
- package/dist/format/blocks/dropClosure.js +186 -0
- package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
- package/dist/format/blocks/markdown-block-catalog.js +162 -0
- package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
- package/dist/format/blocks/markdown-block-ids.mjs +25 -0
- package/dist/format/blocks/parsers.d.ts +105 -0
- package/dist/format/blocks/parsers.js +442 -0
- package/dist/format/blocks/structured-block-schema.d.ts +8 -0
- package/dist/format/blocks/structured-block-schema.js +30 -0
- package/dist/format/callout.d.ts +66 -0
- package/dist/format/callout.js +130 -0
- package/dist/format/cardMarkdown.d.ts +4 -0
- package/dist/format/cardMarkdown.js +12 -0
- package/dist/format/checklist.d.ts +34 -0
- package/dist/format/checklist.js +151 -0
- package/dist/format/index.d.ts +18 -4
- package/dist/format/index.js +24 -4
- package/dist/format/lineGeometry.d.ts +70 -0
- package/dist/format/lineGeometry.js +324 -0
- package/dist/format/lint/index.d.ts +20 -0
- package/dist/format/lint/index.js +22 -0
- package/dist/format/lint/lintSource.d.ts +36 -0
- package/dist/format/lint/lintSource.js +154 -0
- package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/broken-wiki-link.js +45 -0
- package/dist/format/lint/rules/index.d.ts +10 -0
- package/dist/format/lint/rules/index.js +26 -0
- package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
- package/dist/format/lint/rules/malformed-callout.js +79 -0
- package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
- package/dist/format/lint/rules/malformed-checklist.js +60 -0
- package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
- package/dist/format/lint/rules/malformed-frontmatter.js +93 -0
- package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
- package/dist/format/lint/rules/malformed-structured-block.js +134 -0
- package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
- package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
- package/dist/format/lint/rules/orphan-reference.js +87 -0
- package/dist/format/lint/types.d.ts +80 -0
- package/dist/format/lint/types.js +16 -0
- package/dist/format/markdown/dates.js +2 -0
- package/dist/format/markdown/document.js +2 -0
- package/dist/format/markdown/index.js +2 -0
- package/dist/format/markdown/mentions.js +2 -0
- package/dist/format/markdown/slug.js +2 -0
- package/dist/format/markdown/yaml.js +2 -0
- package/dist/format/noteMarkdown.js +2 -0
- package/dist/format/parseWithFallback.d.ts +13 -0
- package/dist/format/parseWithFallback.js +98 -0
- package/dist/format/plaintext.d.ts +5 -0
- package/dist/format/plaintext.js +41 -0
- package/dist/format/postMarkdown.js +3 -1
- package/dist/format/taskUploadFilename.d.ts +6 -0
- package/dist/format/taskUploadFilename.js +13 -0
- package/dist/format/wayfinder.d.ts +50 -0
- package/dist/format/wayfinder.js +203 -0
- package/dist/format/wikiLinks.d.ts +19 -0
- package/dist/format/wikiLinks.js +80 -0
- package/dist/local/workspace.d.ts +12 -0
- package/dist/local/workspace.js +100 -7
- package/dist/mcp-server.js +11 -5
- 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
|
+
}
|
|
@@ -12,6 +12,10 @@ export interface MarkdownCardInput {
|
|
|
12
12
|
lastActivityAt?: number;
|
|
13
13
|
assignees?: string[];
|
|
14
14
|
labels?: string[];
|
|
15
|
+
kind?: "question";
|
|
16
|
+
resolution?: string;
|
|
17
|
+
scope?: "out";
|
|
18
|
+
blockedBy?: number[];
|
|
15
19
|
}
|
|
16
20
|
export interface MarkdownBoardRef {
|
|
17
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
|
|
@@ -65,10 +67,20 @@ export function cardToMarkdown(card, board, column, commentsCount) {
|
|
|
65
67
|
["boardId", board?._id ?? ""],
|
|
66
68
|
["column", column?.name ?? ""],
|
|
67
69
|
["columnId", column?._id ?? ""],
|
|
70
|
+
["kind", card.kind], // undefined for ordinary tasks → skipped
|
|
68
71
|
["status", card.status],
|
|
72
|
+
["resolution", card.resolution], // set once a question is decided
|
|
73
|
+
["scope", card.scope], // "out" for a question ruled out of scope
|
|
69
74
|
["priority", card.priority ?? "none"],
|
|
70
75
|
["assignees", card.assignees ?? []],
|
|
71
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
|
+
],
|
|
72
84
|
["due", toDateOnly(card.dueAt)],
|
|
73
85
|
["created", toISO(card._creationTime)],
|
|
74
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
|
+
}
|
package/dist/format/index.d.ts
CHANGED
|
@@ -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),
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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";
|
package/dist/format/index.js
CHANGED
|
@@ -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),
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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;
|