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.
- package/README.md +147 -6
- package/dist/SforaFs.js +278 -10
- package/dist/api-client.d.ts +290 -5
- package/dist/api-client.js +307 -22
- package/dist/block-commands.d.ts +84 -0
- package/dist/block-commands.js +155 -0
- package/dist/cli.js +323 -29
- package/dist/format/__tests__/byteStable.d.ts +5 -0
- package/dist/format/__tests__/byteStable.js +64 -0
- package/dist/format/blockSplice.d.ts +135 -0
- package/dist/format/blockSplice.js +330 -0
- package/dist/format/blocks/dropClosure.d.ts +81 -0
- package/dist/format/blocks/dropClosure.js +196 -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 +128 -0
- package/dist/format/callout.js +227 -0
- package/dist/format/cardMarkdown.d.ts +2 -0
- package/dist/format/cardMarkdown.js +10 -0
- package/dist/format/checklist.d.ts +34 -0
- package/dist/format/checklist.js +158 -0
- package/dist/format/formatAxes.d.ts +228 -0
- package/dist/format/formatAxes.js +454 -0
- package/dist/format/index.d.ts +19 -4
- package/dist/format/index.js +28 -4
- package/dist/format/lineGeometry.d.ts +100 -0
- package/dist/format/lineGeometry.js +424 -0
- package/dist/format/lint/appliesTo.d.ts +92 -0
- package/dist/format/lint/appliesTo.js +369 -0
- package/dist/format/lint/config.d.ts +106 -0
- package/dist/format/lint/config.js +205 -0
- package/dist/format/lint/fixAll.d.ts +62 -0
- package/dist/format/lint/fixAll.js +107 -0
- package/dist/format/lint/frontmatterSchema.d.ts +181 -0
- package/dist/format/lint/frontmatterSchema.js +660 -0
- package/dist/format/lint/index.d.ts +49 -0
- package/dist/format/lint/index.js +51 -0
- package/dist/format/lint/lintSource.d.ts +56 -0
- package/dist/format/lint/lintSource.js +188 -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/frontmatter-schema.d.ts +2 -0
- package/dist/format/lint/rules/frontmatter-schema.js +92 -0
- package/dist/format/lint/rules/index.d.ts +11 -0
- package/dist/format/lint/rules/index.js +32 -0
- package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
- package/dist/format/lint/rules/malformed-callout.js +88 -0
- package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
- package/dist/format/lint/rules/malformed-checklist.js +65 -0
- package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
- package/dist/format/lint/rules/malformed-frontmatter.js +98 -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/severity.d.ts +15 -0
- package/dist/format/lint/severity.js +50 -0
- package/dist/format/lint/textEdits.d.ts +86 -0
- package/dist/format/lint/textEdits.js +162 -0
- package/dist/format/lint/types.d.ts +116 -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.d.ts +28 -0
- package/dist/format/markdown/slug.js +65 -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 +51 -0
- package/dist/format/postMarkdown.js +3 -1
- package/dist/format/sheetCellSpans.d.ts +95 -0
- package/dist/format/sheetCellSpans.js +223 -0
- package/dist/format/sheetSelection.d.ts +136 -0
- package/dist/format/sheetSelection.js +282 -0
- package/dist/format/taskUploadFilename.d.ts +6 -0
- package/dist/format/taskUploadFilename.js +13 -0
- package/dist/format/textStats.d.ts +23 -0
- package/dist/format/textStats.js +80 -0
- package/dist/format/wayfinder.d.ts +50 -0
- package/dist/format/wayfinder.js +203 -0
- package/dist/format/wikiLinks.d.ts +78 -0
- package/dist/format/wikiLinks.js +266 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +20 -3
- package/dist/mcp-server.js +5 -2
- package/dist/opener.d.ts +23 -0
- package/dist/opener.js +26 -0
- package/dist/render.d.ts +132 -0
- package/dist/render.js +208 -0
- package/dist/shell-commands.d.ts +34 -0
- package/dist/shell-commands.js +108 -0
- package/dist/watch.d.ts +79 -0
- package/dist/watch.js +113 -0
- package/dist/web-url.d.ts +39 -0
- package/dist/web-url.js +63 -0
- package/package.json +7 -6
package/dist/format/index.d.ts
CHANGED
|
@@ -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),
|
|
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 "./formatAxes.js";
|
|
28
|
+
export * from "./wayfinder.js";
|
package/dist/format/index.js
CHANGED
|
@@ -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),
|
|
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
|
+
// 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
|
+
}
|