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
@@ -1,13 +1,28 @@
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 "./formatAxes.js";
28
+ export * from "./wayfinder.js";
@@ -1,13 +1,37 @@
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
+ // The format-DOF vocabulary the ledger above spends (card #288). It is data
30
+ // and types only — no parser, no walker — so it rides the CLI sync alongside
31
+ // the ledger that would not compile without it.
32
+ export * from "./formatAxes.js";
33
+ export * from "./wayfinder.js";
34
+ // htmlToMarkdown and parseMarkdownAst are deliberately NOT in the barrel:
35
+ // they carry the package's only heavy dependencies (unified/rehype/remark)
36
+ // and are excluded from the CLI sync — consumers import the subpaths
37
+ // `@sfora/markdown/htmlToMarkdown` and `@sfora/markdown/parseMarkdownAst`.
@@ -0,0 +1,100 @@
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
+ * The first line of the document's markdown: 0, or the line after a leading
31
+ * frontmatter block. A frontmatter block that never closes takes the whole
32
+ * document with it, which is what a reader sees too.
33
+ *
34
+ * Lint and the mask both need this number and must not each compute it: one of
35
+ * them being wrong about where the YAML ends is exactly how a `<!--` in a
36
+ * summary field opens a comment over a whole file.
37
+ */
38
+ export declare function markdownStart(lines: readonly string[]): number;
39
+ /**
40
+ * Every fenced code block in the document, in order. `from` skips the
41
+ * frontmatter block, whose contents are not markdown either.
42
+ */
43
+ export declare function fencedRegions(lines: readonly string[], from?: number): FenceRegion[];
44
+ /** True for every line covered by a fenced block, delimiters included. */
45
+ export declare function fenceMask(lines: readonly string[], from?: number): boolean[];
46
+ /**
47
+ * True where a line is inside an inline code span, delimiters included.
48
+ *
49
+ * Rules and parsers mask their matches against this so a token quoted as
50
+ * `` `[[c:1]]` `` — the way documentation talks ABOUT the grammar — is never
51
+ * read as using it, and so a pipe inside `` `a|b` `` is content rather than a
52
+ * cell boundary. Line-local by design: a code span cannot cross a blank line,
53
+ * and the callers hold one line at a time.
54
+ */
55
+ export declare function codeSpanMask(line: string): boolean[];
56
+ /**
57
+ * The non-rendering mask, line by line. Each returned line has exactly the
58
+ * length of the line it masks, so `masked[i][k]` and `lines[i][k]` are the
59
+ * same byte position.
60
+ *
61
+ * `from` is where the document's markdown begins — everything above it is a
62
+ * frontmatter block, which is not markdown at all and is returned verbatim for
63
+ * whoever does read it (lint, with `inFrontmatter`). Scanning it would be
64
+ * worse than useless: a `<!--` in a YAML value would open a comment over the
65
+ * whole document. Callers holding a BODY pass 0 — a leading `---` there is a
66
+ * thematic break, not frontmatter — and `maskNonRenderingContexts` works the
67
+ * boundary out with `markdownStart`.
68
+ *
69
+ * A fence decides the whole line, but only where nothing is open already: a
70
+ * ``` line inside an unterminated comment is comment text, and the document
71
+ * below the `-->` renders. That ordering is CommonMark's — an HTML block ends
72
+ * at its closing condition, not at the next thing that looks like a fence —
73
+ * and it is the one place fence geometry does not go first. Where the line is
74
+ * clear, `fenceRegionAt` answers, so there is still exactly one idea in the
75
+ * package of where a fence starts and ends.
76
+ *
77
+ * Lines are whatever the caller split; a bare `\r` inside one is not a line
78
+ * ending here. `maskNonRenderingContexts` splits the way CommonMark does.
79
+ */
80
+ export declare function maskNonRenderingLines(lines: readonly string[], from?: number): string[];
81
+ /**
82
+ * The whole source, masked. `masked.length === source.length` always: the two
83
+ * strings are the same document, one of them with the non-markdown blanked
84
+ * out, and every offset holds across both.
85
+ *
86
+ * This is the DOCUMENT entry point — cm-lint and `demoteNonRenderingLinks`
87
+ * hand it whole files — so the frontmatter boundary is worked out here rather
88
+ * than assumed to be 0. Pass `from` explicitly when the caller knows better:
89
+ * 0 says "these bytes are all markdown", which is what a body is.
90
+ */
91
+ export declare function maskNonRenderingContexts(source: string, from?: number): string;
92
+ /**
93
+ * True when the source span `[start, end)` holds bytes but no rendering ones —
94
+ * the predicate a consumer asks before trusting anything it found by scanning.
95
+ *
96
+ * A span that was blank to begin with is not "non-rendering", it is empty, and
97
+ * answering true for it would demote every piece of whitespace in the
98
+ * document.
99
+ */
100
+ export declare function isNonRenderingRange(source: string, masked: string, start: number, end: number): boolean;
@@ -0,0 +1,424 @@
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
+ // Line geometry: where the fences and the frontmatter are, and which bytes
4
+ // never reach a reader at all.
5
+ //
6
+ // Lint built this first and is still its heaviest user, but it is not lint's
7
+ // to own — a checkbox-looking line inside a fence is code to the reader, to
8
+ // `checklistProgress`, and to every other tool that walks lines instead of the
9
+ // tree. Those callers must not carry a second, subtly different idea of where
10
+ // a fence starts, so the scan lives here and `lint/lintSource` re-exports it.
11
+ //
12
+ // The second half of the file is the NON-RENDERING MASK: one same-length
13
+ // string in which every byte that does not render — fenced code, inline code
14
+ // spans, HTML comments, raw `<pre>`/`<code>` — has been replaced by a space.
15
+ // Substitution, never deletion, so offset N in the mask is offset N in the
16
+ // source by construction and a line scanner and the parsed tree can never
17
+ // disagree about which bytes are markdown. Every scanner in the package reads
18
+ // it; there is no second copy.
19
+ //
20
+ // Pure line arithmetic: no parser, no dependencies. It ships in the CLI.
21
+ // The optional BOM only ever occurs on the first line, and allowing it here is
22
+ // not cosmetic: without it a file that opens with a fence has no fence at all,
23
+ // and every rule that skips fenced code would walk straight into one.
24
+ const FENCE_OPEN = /^?( {0,3})(`{3,}|~{3,})(.*)$/;
25
+ /** The line without its trailing `\r`, so `$`-anchored rules work on CRLF. */
26
+ export function lineText(lines, i) {
27
+ const raw = lines[i] ?? "";
28
+ return raw.endsWith("\r") ? raw.slice(0, -1) : raw;
29
+ }
30
+ /**
31
+ * Frontmatter extent, or null. Only a document that OPENS with `---` has
32
+ * frontmatter — a `---` further down is a thematic break, and treating it as a
33
+ * fence would make every horizontal rule the start of a lint dead zone.
34
+ */
35
+ export function frontmatterExtent(lines) {
36
+ if (lines.length === 0)
37
+ return null;
38
+ const first = lineText(lines, 0).replace(/^/, "");
39
+ if (first !== "---")
40
+ return null;
41
+ for (let i = 1; i < lines.length; i++) {
42
+ const text = lineText(lines, i).trimEnd();
43
+ if (text === "---" || text === "...")
44
+ return { open: 0, close: i };
45
+ }
46
+ return { open: 0, close: -1 }; // opened, never closed
47
+ }
48
+ /**
49
+ * The first line of the document's markdown: 0, or the line after a leading
50
+ * frontmatter block. A frontmatter block that never closes takes the whole
51
+ * document with it, which is what a reader sees too.
52
+ *
53
+ * Lint and the mask both need this number and must not each compute it: one of
54
+ * them being wrong about where the YAML ends is exactly how a `<!--` in a
55
+ * summary field opens a comment over a whole file.
56
+ */
57
+ export function markdownStart(lines) {
58
+ const frontmatter = frontmatterExtent(lines);
59
+ if (frontmatter === null)
60
+ return 0;
61
+ return frontmatter.close === -1 ? lines.length : frontmatter.close + 1;
62
+ }
63
+ /** The fenced block opening at line `i`, or null if none opens there. */
64
+ function fenceRegionAt(lines, i) {
65
+ const match = FENCE_OPEN.exec(lineText(lines, i));
66
+ if (!match)
67
+ return null;
68
+ const delimiter = match[2];
69
+ const info = match[3];
70
+ // A backtick fence's info string may not contain a backtick — that rules
71
+ // out inline code like ```` ```a``` ```` opening a block.
72
+ if (delimiter.startsWith("`") && info.includes("`"))
73
+ return null;
74
+ const char = delimiter[0];
75
+ const closer = new RegExp(`^ {0,3}[${char}]{${delimiter.length},}[ \\t]*$`);
76
+ let close = lines.length - 1;
77
+ let closed = false;
78
+ for (let j = i + 1; j < lines.length; j++) {
79
+ if (closer.test(lineText(lines, j))) {
80
+ close = j;
81
+ closed = true;
82
+ break;
83
+ }
84
+ }
85
+ return {
86
+ open: i,
87
+ close,
88
+ closed,
89
+ lang: info.trim().split(/\s+/)[0]?.toLowerCase() ?? "",
90
+ indent: match[1].length,
91
+ contentStart: i + 1,
92
+ contentEnd: closed ? close : lines.length,
93
+ };
94
+ }
95
+ /**
96
+ * Every fenced code block in the document, in order. `from` skips the
97
+ * frontmatter block, whose contents are not markdown either.
98
+ */
99
+ export function fencedRegions(lines, from = 0) {
100
+ const regions = [];
101
+ for (let i = from; i < lines.length; i++) {
102
+ const region = fenceRegionAt(lines, i);
103
+ if (region === null)
104
+ continue;
105
+ regions.push(region);
106
+ i = region.close;
107
+ }
108
+ return regions;
109
+ }
110
+ /** True for every line covered by a fenced block, delimiters included. */
111
+ export function fenceMask(lines, from = 0) {
112
+ const mask = new Array(lines.length).fill(false);
113
+ for (const region of fencedRegions(lines, from)) {
114
+ for (let i = region.open; i <= region.close; i++)
115
+ mask[i] = true;
116
+ }
117
+ return mask;
118
+ }
119
+ // ── The non-rendering mask ──────────────────────────────────────────────────
120
+ /**
121
+ * One past the closing backtick run of the code span opening at `start`, or
122
+ * null when the run never closes.
123
+ *
124
+ * CommonMark wants the closing run to be EXACTLY as long as the opening one,
125
+ * which is why this is a scan and not an `indexOf`: in `` `a`` `` the double
126
+ * run does not close the single one and nothing else on the line does either,
127
+ * so there is no code span at all — where a search for the next backtick would
128
+ * end one after `a` and mask two characters of prose as code. An unterminated
129
+ * run opens nothing: a stray backtick must not swallow the rest of the line.
130
+ */
131
+ function codeSpanEnd(text, start) {
132
+ let open = 0;
133
+ while (text[start + open] === "`")
134
+ open++;
135
+ if (open === 0)
136
+ return null;
137
+ let i = start + open;
138
+ while (i < text.length) {
139
+ if (text[i] !== "`") {
140
+ i++;
141
+ continue;
142
+ }
143
+ let run = 0;
144
+ while (text[i + run] === "`")
145
+ run++;
146
+ if (run === open)
147
+ return i + run;
148
+ i += run;
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * True where a line is inside an inline code span, delimiters included.
154
+ *
155
+ * Rules and parsers mask their matches against this so a token quoted as
156
+ * `` `[[c:1]]` `` — the way documentation talks ABOUT the grammar — is never
157
+ * read as using it, and so a pipe inside `` `a|b` `` is content rather than a
158
+ * cell boundary. Line-local by design: a code span cannot cross a blank line,
159
+ * and the callers hold one line at a time.
160
+ */
161
+ export function codeSpanMask(line) {
162
+ const mask = new Array(line.length).fill(false);
163
+ let i = 0;
164
+ while (i < line.length) {
165
+ if (line[i] !== "`") {
166
+ i++;
167
+ continue;
168
+ }
169
+ const end = codeSpanEnd(line, i);
170
+ if (end === null) {
171
+ while (line[i] === "`")
172
+ i++;
173
+ continue;
174
+ }
175
+ for (let k = i; k < end; k++)
176
+ mask[k] = true;
177
+ i = end;
178
+ }
179
+ return mask;
180
+ }
181
+ /** The raw-text tag opening at `start`, or null. */
182
+ function rawTextTagAt(line, start) {
183
+ if (line[start] !== "<")
184
+ return null;
185
+ const lower = line.slice(start, start + 6).toLowerCase();
186
+ for (const tag of ["pre", "code"]) {
187
+ if (!lower.startsWith(`<${tag}`))
188
+ continue;
189
+ // `<pre>`, `<pre/>`, `<pre class=…>` — but not `<precise`.
190
+ const boundary = line[start + tag.length + 1];
191
+ if (boundary === ">" || boundary === "/" || boundary === " " || boundary === "\t") {
192
+ return tag;
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+ /** One past the `>` closing the tag that opens at `start`, or null. */
198
+ function htmlTagEnd(line, start) {
199
+ let quote = null;
200
+ for (let i = start; i < line.length; i++) {
201
+ const char = line[i];
202
+ if (quote !== null) {
203
+ if (char === quote)
204
+ quote = null;
205
+ }
206
+ else if (char === '"' || char === "'") {
207
+ quote = char;
208
+ }
209
+ else if (char === ">") {
210
+ return i + 1;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ /** One past `</pre>` / `</code>` at or after `start`, or null. */
216
+ function rawTextCloseEnd(line, start, tag) {
217
+ const match = new RegExp(`</${tag}\\s*>`, "i").exec(line.slice(start));
218
+ return match === null ? null : start + match.index + match[0].length;
219
+ }
220
+ /**
221
+ * Blank one line's non-rendering spans, carrying `state` forward.
222
+ *
223
+ * Order matters: a backtick inside a comment is not a code span, and a `<!--`
224
+ * inside a code span is not a comment, so whichever construct opens first owns
225
+ * everything up to its own close. Left open at the end of the line, a comment
226
+ * or a raw tag keeps consuming into the lines below — which is what an
227
+ * unterminated `<!--` does to a reader too.
228
+ */
229
+ function maskLine(raw, state) {
230
+ const chars = raw.split("");
231
+ const blank = (from, to) => {
232
+ for (let i = from; i < to; i++)
233
+ chars[i] = " ";
234
+ };
235
+ let i = 0;
236
+ while (i < raw.length) {
237
+ if (state.comment) {
238
+ const close = raw.indexOf("-->", i);
239
+ blank(i, close < 0 ? raw.length : close + 3);
240
+ if (close < 0)
241
+ break;
242
+ state.comment = false;
243
+ i = close + 3;
244
+ continue;
245
+ }
246
+ if (state.rawTag !== null) {
247
+ const end = rawTextCloseEnd(raw, i, state.rawTag);
248
+ blank(i, end ?? raw.length);
249
+ if (end === null)
250
+ break;
251
+ state.rawTag = null;
252
+ i = end;
253
+ continue;
254
+ }
255
+ const char = raw[i];
256
+ if (char === "`") {
257
+ const end = codeSpanEnd(raw, i);
258
+ if (end === null) {
259
+ while (raw[i] === "`")
260
+ i++;
261
+ continue;
262
+ }
263
+ blank(i, end);
264
+ i = end;
265
+ continue;
266
+ }
267
+ if (char === "<") {
268
+ if (raw.startsWith("<!--", i)) {
269
+ // CommonMark 0.31 admits the two degenerate spellings `<!-->` and
270
+ // `<!--->` as complete comments. Without them the `-->` search runs
271
+ // off the end, the comment is read as unterminated, and everything
272
+ // after it in the document is masked on the strength of five bytes.
273
+ const short = raw.startsWith("<!--->", i)
274
+ ? 6
275
+ : raw.startsWith("<!-->", i)
276
+ ? 5
277
+ : 0;
278
+ if (short > 0) {
279
+ blank(i, i + short);
280
+ i += short;
281
+ continue;
282
+ }
283
+ const close = raw.indexOf("-->", i + 4);
284
+ blank(i, close < 0 ? raw.length : close + 3);
285
+ state.comment = close < 0;
286
+ if (state.comment)
287
+ break;
288
+ i = close + 3;
289
+ continue;
290
+ }
291
+ const tag = rawTextTagAt(raw, i);
292
+ if (tag !== null) {
293
+ const openEnd = htmlTagEnd(raw, i);
294
+ // An opening tag that runs past the end of the line: nothing after it
295
+ // on this line can be read with any confidence, so stop here.
296
+ if (openEnd === null)
297
+ break;
298
+ const closeEnd = rawTextCloseEnd(raw, openEnd, tag);
299
+ blank(i, closeEnd ?? raw.length);
300
+ state.rawTag = closeEnd === null ? tag : null;
301
+ if (closeEnd === null)
302
+ break;
303
+ i = closeEnd;
304
+ continue;
305
+ }
306
+ }
307
+ i++;
308
+ }
309
+ return chars.join("");
310
+ }
311
+ /**
312
+ * The non-rendering mask, line by line. Each returned line has exactly the
313
+ * length of the line it masks, so `masked[i][k]` and `lines[i][k]` are the
314
+ * same byte position.
315
+ *
316
+ * `from` is where the document's markdown begins — everything above it is a
317
+ * frontmatter block, which is not markdown at all and is returned verbatim for
318
+ * whoever does read it (lint, with `inFrontmatter`). Scanning it would be
319
+ * worse than useless: a `<!--` in a YAML value would open a comment over the
320
+ * whole document. Callers holding a BODY pass 0 — a leading `---` there is a
321
+ * thematic break, not frontmatter — and `maskNonRenderingContexts` works the
322
+ * boundary out with `markdownStart`.
323
+ *
324
+ * A fence decides the whole line, but only where nothing is open already: a
325
+ * ``` line inside an unterminated comment is comment text, and the document
326
+ * below the `-->` renders. That ordering is CommonMark's — an HTML block ends
327
+ * at its closing condition, not at the next thing that looks like a fence —
328
+ * and it is the one place fence geometry does not go first. Where the line is
329
+ * clear, `fenceRegionAt` answers, so there is still exactly one idea in the
330
+ * package of where a fence starts and ends.
331
+ *
332
+ * Lines are whatever the caller split; a bare `\r` inside one is not a line
333
+ * ending here. `maskNonRenderingContexts` splits the way CommonMark does.
334
+ */
335
+ export function maskNonRenderingLines(lines, from = 0) {
336
+ const state = { comment: false, rawTag: null };
337
+ const out = [];
338
+ let fenceEnd = -1;
339
+ for (let i = 0; i < lines.length; i++) {
340
+ const raw = lines[i];
341
+ if (i < from) {
342
+ out.push(raw);
343
+ continue;
344
+ }
345
+ if (i <= fenceEnd) {
346
+ out.push(" ".repeat(raw.length));
347
+ continue;
348
+ }
349
+ if (!state.comment && state.rawTag === null) {
350
+ const region = fenceRegionAt(lines, i);
351
+ if (region !== null) {
352
+ fenceEnd = region.close;
353
+ out.push(" ".repeat(raw.length));
354
+ continue;
355
+ }
356
+ }
357
+ out.push(maskLine(raw, state));
358
+ }
359
+ return out;
360
+ }
361
+ /**
362
+ * Split a document the way CommonMark does: `\n`, `\r\n` and a bare `\r` all
363
+ * end a line. The terminators are not part of the returned text and are never
364
+ * rewritten, which is how the mask stays byte-aligned with the source.
365
+ */
366
+ function documentLines(source) {
367
+ const lines = [];
368
+ let start = 0;
369
+ for (let i = 0; i < source.length; i++) {
370
+ const char = source[i];
371
+ if (char === "\n") {
372
+ lines.push({ text: source.slice(start, i), start });
373
+ start = i + 1;
374
+ }
375
+ else if (char === "\r") {
376
+ lines.push({ text: source.slice(start, i), start });
377
+ if (source[i + 1] === "\n")
378
+ i++;
379
+ start = i + 1;
380
+ }
381
+ }
382
+ lines.push({ text: source.slice(start), start });
383
+ return lines;
384
+ }
385
+ /**
386
+ * The whole source, masked. `masked.length === source.length` always: the two
387
+ * strings are the same document, one of them with the non-markdown blanked
388
+ * out, and every offset holds across both.
389
+ *
390
+ * This is the DOCUMENT entry point — cm-lint and `demoteNonRenderingLinks`
391
+ * hand it whole files — so the frontmatter boundary is worked out here rather
392
+ * than assumed to be 0. Pass `from` explicitly when the caller knows better:
393
+ * 0 says "these bytes are all markdown", which is what a body is.
394
+ */
395
+ export function maskNonRenderingContexts(source, from) {
396
+ const lines = documentLines(source);
397
+ const texts = lines.map((line) => line.text);
398
+ const masked = maskNonRenderingLines(texts, from ?? markdownStart(texts));
399
+ // Written back in place, never re-joined: the source keeps its own line
400
+ // terminators, whatever mix of them it has.
401
+ const out = source.split("");
402
+ for (let i = 0; i < lines.length; i++) {
403
+ const { start } = lines[i];
404
+ const text = masked[i];
405
+ for (let k = 0; k < text.length; k++)
406
+ out[start + k] = text[k];
407
+ }
408
+ return out.join("");
409
+ }
410
+ /**
411
+ * True when the source span `[start, end)` holds bytes but no rendering ones —
412
+ * the predicate a consumer asks before trusting anything it found by scanning.
413
+ *
414
+ * A span that was blank to begin with is not "non-rendering", it is empty, and
415
+ * answering true for it would demote every piece of whitespace in the
416
+ * document.
417
+ */
418
+ export function isNonRenderingRange(source, masked, start, end) {
419
+ if (end <= start)
420
+ return false;
421
+ if (source.slice(start, end).trim() === "")
422
+ return false;
423
+ return masked.slice(start, end).trim() === "";
424
+ }