sfora-cli 0.10.0 → 0.12.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 +174 -0
- package/dist/SforaFs.js +8 -6
- package/dist/api-client.d.ts +344 -4
- package/dist/api-client.js +289 -21
- package/dist/block-commands.d.ts +84 -0
- package/dist/block-commands.js +155 -0
- package/dist/chat.d.ts +89 -0
- package/dist/chat.js +189 -0
- package/dist/cli-args.d.ts +32 -0
- package/dist/cli-args.js +88 -0
- package/dist/cli.js +530 -88
- package/dist/format/blockSplice.d.ts +135 -0
- package/dist/format/blockSplice.js +330 -0
- package/dist/format/blocks/dropClosure.d.ts +10 -1
- package/dist/format/blocks/dropClosure.js +11 -1
- package/dist/format/callout.d.ts +69 -7
- package/dist/format/callout.js +112 -15
- package/dist/format/checklist.js +11 -4
- package/dist/format/formatAxes.d.ts +228 -0
- package/dist/format/formatAxes.js +454 -0
- package/dist/format/index.d.ts +1 -0
- package/dist/format/index.js +4 -0
- package/dist/format/lineGeometry.d.ts +34 -4
- package/dist/format/lineGeometry.js +140 -40
- 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 +34 -5
- package/dist/format/lint/index.js +34 -5
- package/dist/format/lint/lintSource.d.ts +27 -7
- package/dist/format/lint/lintSource.js +67 -33
- 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 +2 -1
- package/dist/format/lint/rules/index.js +7 -1
- package/dist/format/lint/rules/malformed-callout.js +25 -16
- package/dist/format/lint/rules/malformed-checklist.js +8 -3
- package/dist/format/lint/rules/malformed-frontmatter.js +6 -1
- 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 +44 -8
- package/dist/format/markdown/slug.d.ts +28 -0
- package/dist/format/markdown/slug.js +63 -0
- package/dist/format/plaintext.js +13 -3
- 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/textStats.d.ts +23 -0
- package/dist/format/textStats.js +80 -0
- package/dist/format/wikiLinks.d.ts +60 -1
- package/dist/format/wikiLinks.js +195 -9
- package/dist/index.d.ts +26 -1
- package/dist/index.js +20 -3
- package/dist/opener.d.ts +23 -0
- package/dist/opener.js +26 -0
- package/dist/render.d.ts +162 -0
- package/dist/render.js +280 -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 +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
// The four levels a sfora-law diagnostic can speak at, and what each one means.
|
|
4
|
+
//
|
|
5
|
+
// One level is not enough. Every rule in this directory answers the same
|
|
6
|
+
// question — "will this byte sequence render as the author obviously meant it
|
|
7
|
+
// to?" — but the ANSWERS differ in kind, and flattening them made the gutter
|
|
8
|
+
// lie in both directions: an unclosed frontmatter fence, which costs a document
|
|
9
|
+
// every field it claims to have, wore the same amber as a checkbox that renders
|
|
10
|
+
// perfectly and is merely uncounted.
|
|
11
|
+
//
|
|
12
|
+
// The ladder is ordered by WHAT IS LOST, not by how loud the rule feels:
|
|
13
|
+
//
|
|
14
|
+
// error the document as a whole stops meaning what it says. Everything
|
|
15
|
+
// downstream of the mark is read as something else entirely.
|
|
16
|
+
// warning something the author wrote will not render as they meant it. The
|
|
17
|
+
// bytes are there; the reading is wrong.
|
|
18
|
+
// info something the author wrote is silently dropped. The render is not
|
|
19
|
+
// wrong, it is missing a piece, and nothing on screen says so.
|
|
20
|
+
// hint the render is exactly right. Only a derived number — a progress
|
|
21
|
+
// count, an index — comes out different than the author expects.
|
|
22
|
+
//
|
|
23
|
+
// Every level has at least one rule standing on it (asserted in
|
|
24
|
+
// `__tests__/lint.test.ts`), because a severity nothing emits is a severity
|
|
25
|
+
// nobody has thought about.
|
|
26
|
+
//
|
|
27
|
+
// The names match LSP's `DiagnosticSeverity` and CodeMirror's `Severity`, in
|
|
28
|
+
// that order, so the editor adapter is a lookup rather than a translation.
|
|
29
|
+
/** Most severe first. The order IS the rank. */
|
|
30
|
+
export const LINT_SEVERITIES = ["error", "warning", "info", "hint"];
|
|
31
|
+
/** 0 for `error`, 3 for `hint`. Lower is more severe. */
|
|
32
|
+
export function severityRank(severity) {
|
|
33
|
+
const index = LINT_SEVERITIES.indexOf(severity);
|
|
34
|
+
// An unknown string is treated as the quietest thing we have rather than as
|
|
35
|
+
// a crash: a diagnostic that arrived from an older copy of the package must
|
|
36
|
+
// still sort somewhere.
|
|
37
|
+
return index === -1 ? LINT_SEVERITIES.length : index;
|
|
38
|
+
}
|
|
39
|
+
/** Negative when `a` is more severe than `b`. Sorts most-severe-first. */
|
|
40
|
+
export function compareSeverity(a, b) {
|
|
41
|
+
return severityRank(a) - severityRank(b);
|
|
42
|
+
}
|
|
43
|
+
/** True when `severity` is at least as severe as `floor`. */
|
|
44
|
+
export function atLeastAsSevere(severity, floor) {
|
|
45
|
+
return severityRank(severity) <= severityRank(floor);
|
|
46
|
+
}
|
|
47
|
+
export function isLintSeverity(value) {
|
|
48
|
+
return (typeof value === "string" &&
|
|
49
|
+
LINT_SEVERITIES.includes(value));
|
|
50
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { LintEdit, LintFix } from "./types.js";
|
|
2
|
+
/** Why a list of edits cannot be applied as one atomic change. */
|
|
3
|
+
export type LintEditRejection = {
|
|
4
|
+
reason: "inverted";
|
|
5
|
+
edit: LintEdit;
|
|
6
|
+
} | {
|
|
7
|
+
reason: "out-of-range";
|
|
8
|
+
edit: LintEdit;
|
|
9
|
+
} | {
|
|
10
|
+
reason: "overlap";
|
|
11
|
+
edit: LintEdit;
|
|
12
|
+
conflictsWith: LintEdit;
|
|
13
|
+
};
|
|
14
|
+
export type LintEditResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
rejection: LintEditRejection;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* True when two edits cannot both be applied to the same document state.
|
|
23
|
+
*
|
|
24
|
+
* Proper overlap is the obvious case. The second clause is the one that bites:
|
|
25
|
+
* two pure INSERTIONS at the same offset do not overlap by any interval test,
|
|
26
|
+
* yet the text they produce depends entirely on which goes first, and a fix
|
|
27
|
+
* whose output depends on sort stability is not a fix. Adjacency
|
|
28
|
+
* (`a.to === b.from`) is fine and stays allowed — that is two rules repairing
|
|
29
|
+
* neighbouring spans, which is exactly what fix-all is for.
|
|
30
|
+
*/
|
|
31
|
+
export declare function editsConflict(a: LintEdit, b: LintEdit): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The application order. Descending by start, then by end, so an edit that
|
|
34
|
+
* starts later lands first and leaves every earlier offset untouched.
|
|
35
|
+
*
|
|
36
|
+
* This function is the reason the offsets in a fix mean anything. Sorting it
|
|
37
|
+
* the other way still "works" for a single-edit fix — which is most of them,
|
|
38
|
+
* which is how a bug here would hide — and quietly corrupts every multi-edit
|
|
39
|
+
* one.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sortEditsForApply(edits: readonly LintEdit[]): LintEdit[];
|
|
42
|
+
/**
|
|
43
|
+
* Can these edits compose over `source`? Returns the first reason they cannot,
|
|
44
|
+
* or null. Checked against the source the edits were DERIVED from — an edit
|
|
45
|
+
* checked against a document that has moved on is meaningless.
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkEdits(source: string, edits: readonly LintEdit[]): LintEditRejection | null;
|
|
48
|
+
/**
|
|
49
|
+
* Apply `edits` to `source` as one atomic change, or say why not.
|
|
50
|
+
*
|
|
51
|
+
* Never throws and never half-applies: on any rejection the caller gets the
|
|
52
|
+
* reason and the original text is untouched. Lint is a background nicety in a
|
|
53
|
+
* text editor; it is never allowed to be the reason a document is damaged.
|
|
54
|
+
*/
|
|
55
|
+
export declare function tryApplyEdits(source: string, edits: readonly LintEdit[]): LintEditResult;
|
|
56
|
+
/**
|
|
57
|
+
* `tryApplyEdits` for callers that have already validated, or that would
|
|
58
|
+
* rather lose the fix than reason about a rejection. Returns `source`
|
|
59
|
+
* unchanged when the edits do not compose.
|
|
60
|
+
*/
|
|
61
|
+
export declare function applyEdits(source: string, edits: readonly LintEdit[]): string;
|
|
62
|
+
/**
|
|
63
|
+
* True when every edit still describes the bytes it was computed against.
|
|
64
|
+
*
|
|
65
|
+
* The staleness question, factored out of the editor adapter. A fix's offsets
|
|
66
|
+
* point into the source a rule read; applying them to a document that has been
|
|
67
|
+
* typed into since is not a repair, it is corruption at a plausible-looking
|
|
68
|
+
* offset. Both the CodeMirror action and the batch path ask this before they
|
|
69
|
+
* commit.
|
|
70
|
+
*/
|
|
71
|
+
export declare function editsMatch(source: string, edits: readonly LintEdit[], expected: string): boolean;
|
|
72
|
+
/** LSP's `Position`: zero-based line, zero-based UTF-16 offset within it. */
|
|
73
|
+
export interface LintPosition {
|
|
74
|
+
line: number;
|
|
75
|
+
character: number;
|
|
76
|
+
}
|
|
77
|
+
/** LSP's `TextEdit`, for anything that talks over a wire instead of a buffer. */
|
|
78
|
+
export interface LintTextEdit {
|
|
79
|
+
range: {
|
|
80
|
+
start: LintPosition;
|
|
81
|
+
end: LintPosition;
|
|
82
|
+
};
|
|
83
|
+
newText: string;
|
|
84
|
+
}
|
|
85
|
+
/** One fix's edits in LSP wire form, in the order LSP wants them (any). */
|
|
86
|
+
export declare function fixToTextEdits(source: string, fix: LintFix): LintTextEdit[];
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
// The edit mechanics every quick fix rides on.
|
|
4
|
+
//
|
|
5
|
+
// A fix is a LIST of replacements over ONE document state, not a sequence of
|
|
6
|
+
// steps. That distinction is the whole of this file. Each edit's offsets are
|
|
7
|
+
// absolute into the PRE-fix source, so applying them in document order is
|
|
8
|
+
// wrong by construction: the first edit moves every offset after it, and the
|
|
9
|
+
// second one then cuts the wrong bytes out of a string that no longer matches
|
|
10
|
+
// the one the rule read. Applying them last-one-first is the fix — an edit at a
|
|
11
|
+
// LOWER offset cannot disturb an edit at a higher one that has already landed.
|
|
12
|
+
//
|
|
13
|
+
// (`applyLintFix` in lintSource.ts has always reverse-sorted. What it did not
|
|
14
|
+
// do was check that the edits could compose at all: it skipped an
|
|
15
|
+
// out-of-range edit and applied the rest, which is a HALF-applied fix — the
|
|
16
|
+
// one outcome worse than no fix, because the author accepted a repair and got
|
|
17
|
+
// a different broken document. Everything here exists so the answer is "all of
|
|
18
|
+
// it or none of it".)
|
|
19
|
+
//
|
|
20
|
+
// Two edits that overlap have no correct order in either direction, so they
|
|
21
|
+
// are rejected rather than resolved: whichever one is applied second is
|
|
22
|
+
// reading bytes the first one already replaced. That is the shape of the guard
|
|
23
|
+
// `fixAll` needs when it composes fixes from different rules over one
|
|
24
|
+
// document.
|
|
25
|
+
//
|
|
26
|
+
// Offsets, not `{line, character}`. LSP's `TextEdit` carries positions because
|
|
27
|
+
// its wire protocol has no shared buffer; here the source string is right
|
|
28
|
+
// there, CodeMirror's `changes` spec wants offsets, and a line/character pair
|
|
29
|
+
// would be converted twice for no one's benefit. The SHAPE is LSP's — a fix is
|
|
30
|
+
// a list of ranged replacements applied reverse-sorted — and `lintEditToLsp`
|
|
31
|
+
// below converts on the way out for anything that wants the wire form.
|
|
32
|
+
/**
|
|
33
|
+
* True when two edits cannot both be applied to the same document state.
|
|
34
|
+
*
|
|
35
|
+
* Proper overlap is the obvious case. The second clause is the one that bites:
|
|
36
|
+
* two pure INSERTIONS at the same offset do not overlap by any interval test,
|
|
37
|
+
* yet the text they produce depends entirely on which goes first, and a fix
|
|
38
|
+
* whose output depends on sort stability is not a fix. Adjacency
|
|
39
|
+
* (`a.to === b.from`) is fine and stays allowed — that is two rules repairing
|
|
40
|
+
* neighbouring spans, which is exactly what fix-all is for.
|
|
41
|
+
*/
|
|
42
|
+
export function editsConflict(a, b) {
|
|
43
|
+
if (a.from < b.to && b.from < a.to)
|
|
44
|
+
return true;
|
|
45
|
+
return a.from === a.to && b.from === b.to && a.from === b.from;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The application order. Descending by start, then by end, so an edit that
|
|
49
|
+
* starts later lands first and leaves every earlier offset untouched.
|
|
50
|
+
*
|
|
51
|
+
* This function is the reason the offsets in a fix mean anything. Sorting it
|
|
52
|
+
* the other way still "works" for a single-edit fix — which is most of them,
|
|
53
|
+
* which is how a bug here would hide — and quietly corrupts every multi-edit
|
|
54
|
+
* one.
|
|
55
|
+
*/
|
|
56
|
+
export function sortEditsForApply(edits) {
|
|
57
|
+
return [...edits].sort((a, b) => b.from - a.from || b.to - a.to);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Can these edits compose over `source`? Returns the first reason they cannot,
|
|
61
|
+
* or null. Checked against the source the edits were DERIVED from — an edit
|
|
62
|
+
* checked against a document that has moved on is meaningless.
|
|
63
|
+
*/
|
|
64
|
+
export function checkEdits(source, edits) {
|
|
65
|
+
for (const edit of edits) {
|
|
66
|
+
if (edit.from > edit.to)
|
|
67
|
+
return { reason: "inverted", edit };
|
|
68
|
+
if (edit.from < 0 || edit.to > source.length) {
|
|
69
|
+
return { reason: "out-of-range", edit };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// O(n²) over the edits of one fix, which is at most a handful. `fixAll`
|
|
73
|
+
// pays the same price across a document's worth of fixes and it is still
|
|
74
|
+
// nothing next to the parse that produced them.
|
|
75
|
+
for (let i = 0; i < edits.length; i++) {
|
|
76
|
+
for (let j = i + 1; j < edits.length; j++) {
|
|
77
|
+
const a = edits[i];
|
|
78
|
+
const b = edits[j];
|
|
79
|
+
if (editsConflict(a, b)) {
|
|
80
|
+
return { reason: "overlap", edit: b, conflictsWith: a };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Apply `edits` to `source` as one atomic change, or say why not.
|
|
88
|
+
*
|
|
89
|
+
* Never throws and never half-applies: on any rejection the caller gets the
|
|
90
|
+
* reason and the original text is untouched. Lint is a background nicety in a
|
|
91
|
+
* text editor; it is never allowed to be the reason a document is damaged.
|
|
92
|
+
*/
|
|
93
|
+
export function tryApplyEdits(source, edits) {
|
|
94
|
+
const rejection = checkEdits(source, edits);
|
|
95
|
+
if (rejection)
|
|
96
|
+
return { ok: false, rejection };
|
|
97
|
+
let out = source;
|
|
98
|
+
for (const edit of sortEditsForApply(edits)) {
|
|
99
|
+
out = out.slice(0, edit.from) + edit.insert + out.slice(edit.to);
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, text: out };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `tryApplyEdits` for callers that have already validated, or that would
|
|
105
|
+
* rather lose the fix than reason about a rejection. Returns `source`
|
|
106
|
+
* unchanged when the edits do not compose.
|
|
107
|
+
*/
|
|
108
|
+
export function applyEdits(source, edits) {
|
|
109
|
+
const result = tryApplyEdits(source, edits);
|
|
110
|
+
return result.ok ? result.text : source;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* True when every edit still describes the bytes it was computed against.
|
|
114
|
+
*
|
|
115
|
+
* The staleness question, factored out of the editor adapter. A fix's offsets
|
|
116
|
+
* point into the source a rule read; applying them to a document that has been
|
|
117
|
+
* typed into since is not a repair, it is corruption at a plausible-looking
|
|
118
|
+
* offset. Both the CodeMirror action and the batch path ask this before they
|
|
119
|
+
* commit.
|
|
120
|
+
*/
|
|
121
|
+
export function editsMatch(source, edits, expected) {
|
|
122
|
+
if (checkEdits(expected, edits) !== null)
|
|
123
|
+
return false;
|
|
124
|
+
return edits.every((edit) => edit.to <= source.length &&
|
|
125
|
+
source.slice(edit.from, edit.to) === expected.slice(edit.from, edit.to));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Offsets → line/character, for one document. Built once per conversion so a
|
|
129
|
+
* fix with several edits does not rescan the source per edit.
|
|
130
|
+
*/
|
|
131
|
+
function lineStartsOf(source) {
|
|
132
|
+
const starts = [0];
|
|
133
|
+
for (let i = 0; i < source.length; i++) {
|
|
134
|
+
if (source[i] === "\n")
|
|
135
|
+
starts.push(i + 1);
|
|
136
|
+
}
|
|
137
|
+
return starts;
|
|
138
|
+
}
|
|
139
|
+
function positionAt(lineStarts, offset) {
|
|
140
|
+
// Binary search for the last line start at or before `offset`.
|
|
141
|
+
let lo = 0;
|
|
142
|
+
let hi = lineStarts.length - 1;
|
|
143
|
+
while (lo < hi) {
|
|
144
|
+
const mid = (lo + hi + 1) >> 1;
|
|
145
|
+
if (lineStarts[mid] <= offset)
|
|
146
|
+
lo = mid;
|
|
147
|
+
else
|
|
148
|
+
hi = mid - 1;
|
|
149
|
+
}
|
|
150
|
+
return { line: lo, character: offset - lineStarts[lo] };
|
|
151
|
+
}
|
|
152
|
+
/** One fix's edits in LSP wire form, in the order LSP wants them (any). */
|
|
153
|
+
export function fixToTextEdits(source, fix) {
|
|
154
|
+
const lineStarts = lineStartsOf(source);
|
|
155
|
+
return fix.edits.map((edit) => ({
|
|
156
|
+
range: {
|
|
157
|
+
start: positionAt(lineStarts, edit.from),
|
|
158
|
+
end: positionAt(lineStarts, edit.to),
|
|
159
|
+
},
|
|
160
|
+
newText: edit.insert,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { Root } from "mdast";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
export type LintSeverity = "error" | "warning" | "info";
|
|
2
|
+
import type { LintConfig } from "./config.js";
|
|
3
|
+
import type { SelectedFrontmatterSchema } from "./frontmatterSchema.js";
|
|
4
|
+
import type { LintSeverity } from "./severity.js";
|
|
5
|
+
export type { LintSeverity };
|
|
8
6
|
/** A replacement over the PRE-fix document. Offsets are absolute. */
|
|
9
7
|
export interface LintEdit {
|
|
10
8
|
from: number;
|
|
@@ -12,8 +10,15 @@ export interface LintEdit {
|
|
|
12
10
|
insert: string;
|
|
13
11
|
}
|
|
14
12
|
/**
|
|
15
|
-
* One quick fix
|
|
16
|
-
*
|
|
13
|
+
* One quick fix, LSP-shaped: a LIST of ranged replacements over ONE document
|
|
14
|
+
* state, not a script of steps. All the edits are computed against the same
|
|
15
|
+
* pre-fix source, so they compose atomically and must not overlap — see
|
|
16
|
+
* ./textEdits, which owns the ordering, the overlap check and the application.
|
|
17
|
+
*
|
|
18
|
+
* A fix with several edits is the normal case, not an exotic one: repairing a
|
|
19
|
+
* fenced block's opening and closing delimiter together, or removing a dead
|
|
20
|
+
* frontmatter line while rewriting the live one, is two edits far apart in the
|
|
21
|
+
* file that only mean anything if they land as a unit.
|
|
17
22
|
*/
|
|
18
23
|
export interface LintFix {
|
|
19
24
|
label: string;
|
|
@@ -48,6 +53,14 @@ export interface LintContext {
|
|
|
48
53
|
* Rules never fire inside a fence: the bytes there are not markdown.
|
|
49
54
|
*/
|
|
50
55
|
inFence: (lineIndex: number) => boolean;
|
|
56
|
+
/**
|
|
57
|
+
* True for a line that holds bytes, none of which render — inside an HTML
|
|
58
|
+
* comment, or wholly inside an inline code span. The wider judgement behind
|
|
59
|
+
* `inFence`, from the same shared mask every other scanner in the package
|
|
60
|
+
* reads, so a rule cannot decide that a line is markdown when the reader has
|
|
61
|
+
* already decided it is not.
|
|
62
|
+
*/
|
|
63
|
+
nonRendering: (lineIndex: number) => boolean;
|
|
51
64
|
/** True for the frontmatter fences and everything between them. */
|
|
52
65
|
inFrontmatter: (lineIndex: number) => boolean;
|
|
53
66
|
/** Line extents of the frontmatter block, or null when there is none. */
|
|
@@ -55,6 +68,16 @@ export interface LintContext {
|
|
|
55
68
|
open: number;
|
|
56
69
|
close: number;
|
|
57
70
|
} | null;
|
|
71
|
+
/**
|
|
72
|
+
* The frontmatter schemas the workspace declared for THIS document's path,
|
|
73
|
+
* scope already applied. Absent or empty on every document until a workspace
|
|
74
|
+
* writes one down — see ./frontmatterSchema, which owns the selection.
|
|
75
|
+
*
|
|
76
|
+
* Selected here rather than in the rule for the same reason the fence mask
|
|
77
|
+
* is: the runner knows the document's path and the rule does not, and a rule
|
|
78
|
+
* that re-derived the scope would be a second place for the two to disagree.
|
|
79
|
+
*/
|
|
80
|
+
frontmatterSchemas?: readonly SelectedFrontmatterSchema[];
|
|
58
81
|
/** Present only when a parser was injected AND it succeeded. */
|
|
59
82
|
ast?: Root;
|
|
60
83
|
/**
|
|
@@ -77,4 +100,17 @@ export interface LintSourceOptions {
|
|
|
77
100
|
/** Injected mdast parser — `parseMarkdownAst(source).root`. */
|
|
78
101
|
parseAst?: (source: string) => Root;
|
|
79
102
|
resolveLink?: LintContext["resolveLink"];
|
|
103
|
+
/**
|
|
104
|
+
* What the workspace says about these rules: which are off, which report at
|
|
105
|
+
* another level, which are scoped to a set of paths. Absent means every rule
|
|
106
|
+
* runs at the severity it declares, which is what every caller got before
|
|
107
|
+
* the config existed.
|
|
108
|
+
*/
|
|
109
|
+
config?: LintConfig;
|
|
110
|
+
/**
|
|
111
|
+
* The document's path, for `appliesTo` scoping —
|
|
112
|
+
* `projects/sfora/docs/plan.md`. A scoped rule runs anyway when this is
|
|
113
|
+
* missing; see `scopeAdmits` for why silence is the worse failure.
|
|
114
|
+
*/
|
|
115
|
+
path?: string;
|
|
80
116
|
}
|
|
@@ -1,2 +1,30 @@
|
|
|
1
1
|
export declare function slugify(title: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* The slug of one heading's text, and the anchor half of `[[target#anchor]]`.
|
|
4
|
+
*
|
|
5
|
+
* THE WHOLE POINT is that one function serves both sides: the read path stamps
|
|
6
|
+
* `id={toAnchorSlug(headingText)}` on the rendered heading, and a wiki link's
|
|
7
|
+
* anchor resolves through `toAnchorSlug(anchor)`. Two spellings of this rule
|
|
8
|
+
* would mean `[[page#my-heading]]` scrolls to nothing, and nothing would say
|
|
9
|
+
* so — which is why `anchorSlugAgreement` in the tests mutates one caller and
|
|
10
|
+
* asserts the other fails.
|
|
11
|
+
*
|
|
12
|
+
* Returns "" for text with no letters or numbers in it at all. That is not a
|
|
13
|
+
* fallback: a heading of "***" has no name to be linked by, and inventing one
|
|
14
|
+
* ("untitled") would hand two such headings the same anchor.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toAnchorSlug(text: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Suffix a repeated slug so every id in one document is unique: the first
|
|
19
|
+
* `## Notes` is `notes`, the second `notes-1`, the third `notes-2`.
|
|
20
|
+
*
|
|
21
|
+
* `counts` is the caller's ledger for ONE document, mutated in place — pass a
|
|
22
|
+
* fresh `Map` per render. The suffix is a positional fact, so a link written
|
|
23
|
+
* as `[[page#notes-1]]` means "the second heading called Notes" and moves when
|
|
24
|
+
* a third one is inserted above it. That is the same bargain every heading-
|
|
25
|
+
* anchor scheme makes; there is no stable name for a name used twice.
|
|
26
|
+
*/
|
|
27
|
+
export declare function disambiguateSlug(baseSlug: string, counts: Map<string, number>): string;
|
|
28
|
+
/** `toAnchorSlug` + dedupe. The id a rendered heading carries. */
|
|
29
|
+
export declare function headingSlug(text: string, counts: Map<string, number>): string;
|
|
2
30
|
export declare function slugFromFilename(filename: string): string;
|
|
@@ -14,6 +14,69 @@ export function slugify(title) {
|
|
|
14
14
|
.replace(/^-+|-+$/g, "");
|
|
15
15
|
return s || "untitled";
|
|
16
16
|
}
|
|
17
|
+
// ─── heading anchors ──────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// A DIFFERENT slug from `slugify` above, and the difference is the point.
|
|
20
|
+
// `slugify` names FILES, so it is ASCII-only on purpose: a filename crosses a
|
|
21
|
+
// filesystem, a URL path, and a tarball, and `café.md` is three different byte
|
|
22
|
+
// strings depending on which one wrote it. A heading anchor never leaves the
|
|
23
|
+
// document — it is an `id` on an `<h2>` and a `#fragment` pointing at it — so
|
|
24
|
+
// throwing away every non-ASCII letter there would collapse `## Café` and
|
|
25
|
+
// `## 咖啡` to the same empty slug and make both unlinkable.
|
|
26
|
+
//
|
|
27
|
+
// The two must not be merged. Widening `slugify` to Unicode would rename every
|
|
28
|
+
// file the fs API has ever written.
|
|
29
|
+
//
|
|
30
|
+
// Rules follow open-knowledge's `toWikiLinkSlug`
|
|
31
|
+
// (context/open-knowledge/packages/core/src/utils/slug.ts): trim, NFKD, drop
|
|
32
|
+
// combining marks, lowercase, any run of non-letter/non-number to one hyphen,
|
|
33
|
+
// strip edge hyphens.
|
|
34
|
+
const COMBINING_MARK_RE = /\p{M}+/gu;
|
|
35
|
+
const NON_LETTER_OR_NUMBER_RE = /[^\p{L}\p{N}]+/gu;
|
|
36
|
+
const EDGE_HYPHENS_RE = /^-+|-+$/g;
|
|
37
|
+
/**
|
|
38
|
+
* The slug of one heading's text, and the anchor half of `[[target#anchor]]`.
|
|
39
|
+
*
|
|
40
|
+
* THE WHOLE POINT is that one function serves both sides: the read path stamps
|
|
41
|
+
* `id={toAnchorSlug(headingText)}` on the rendered heading, and a wiki link's
|
|
42
|
+
* anchor resolves through `toAnchorSlug(anchor)`. Two spellings of this rule
|
|
43
|
+
* would mean `[[page#my-heading]]` scrolls to nothing, and nothing would say
|
|
44
|
+
* so — which is why `anchorSlugAgreement` in the tests mutates one caller and
|
|
45
|
+
* asserts the other fails.
|
|
46
|
+
*
|
|
47
|
+
* Returns "" for text with no letters or numbers in it at all. That is not a
|
|
48
|
+
* fallback: a heading of "***" has no name to be linked by, and inventing one
|
|
49
|
+
* ("untitled") would hand two such headings the same anchor.
|
|
50
|
+
*/
|
|
51
|
+
export function toAnchorSlug(text) {
|
|
52
|
+
return text
|
|
53
|
+
.trim()
|
|
54
|
+
.normalize("NFKD")
|
|
55
|
+
.replace(COMBINING_MARK_RE, "")
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(NON_LETTER_OR_NUMBER_RE, "-")
|
|
58
|
+
.replace(EDGE_HYPHENS_RE, "");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Suffix a repeated slug so every id in one document is unique: the first
|
|
62
|
+
* `## Notes` is `notes`, the second `notes-1`, the third `notes-2`.
|
|
63
|
+
*
|
|
64
|
+
* `counts` is the caller's ledger for ONE document, mutated in place — pass a
|
|
65
|
+
* fresh `Map` per render. The suffix is a positional fact, so a link written
|
|
66
|
+
* as `[[page#notes-1]]` means "the second heading called Notes" and moves when
|
|
67
|
+
* a third one is inserted above it. That is the same bargain every heading-
|
|
68
|
+
* anchor scheme makes; there is no stable name for a name used twice.
|
|
69
|
+
*/
|
|
70
|
+
export function disambiguateSlug(baseSlug, counts) {
|
|
71
|
+
const count = counts.get(baseSlug) ?? 0;
|
|
72
|
+
counts.set(baseSlug, count + 1);
|
|
73
|
+
return count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
74
|
+
}
|
|
75
|
+
/** `toAnchorSlug` + dedupe. The id a rendered heading carries. */
|
|
76
|
+
export function headingSlug(text, counts) {
|
|
77
|
+
const base = toAnchorSlug(text);
|
|
78
|
+
return base ? disambiguateSlug(base, counts) : "";
|
|
79
|
+
}
|
|
17
80
|
// Pull the slug back out of a filename: drop the .md and an optional leading
|
|
18
81
|
// YYYY-MM-DD- date prefix. Used to match a requested filename to an entity.
|
|
19
82
|
export function slugFromFilename(filename) {
|
package/dist/format/plaintext.js
CHANGED
|
@@ -23,9 +23,19 @@ export function stripToPlainText(body, options = {}) {
|
|
|
23
23
|
.replace(/<[^>]+>/g, " ") // raw HTML tags
|
|
24
24
|
.replace(/!\[([^\]]*)\]\([^)]*\)/g, images === "alt" ? "$1" : " ")
|
|
25
25
|
.replace(/@\[([^\]]+)\]\([^)]*\)/g, "@$1") // mention → @Name
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// `[[target|Label]]` → Label, and `![[target|Label]]` with it. The optional
|
|
27
|
+
// bang is the embed marker (card #298) and it has to be eaten HERE: left
|
|
28
|
+
// out of the pattern, every embed in every excerpt turns into a stray
|
|
29
|
+
// exclamation mark in front of its label. An embed flattens to the same
|
|
30
|
+
// words a link does — a one-line preview has nowhere to put a card.
|
|
31
|
+
.replace(/!?\[\[[^\]|]*\|([^\]]*)\]\]/g, "$1");
|
|
32
|
+
// Label-less tokens. `wikiLinkPattern()` matches the brackets only, so the
|
|
33
|
+
// bang is stripped separately rather than by widening the grammar's own
|
|
34
|
+
// pattern — that pattern's span is what `canonicalizeReferences` rewrites,
|
|
35
|
+
// and a span that swallowed the bang would rewrite embeds into links.
|
|
36
|
+
out = out
|
|
37
|
+
.replace(/!\[\[([^\]]+)\]\]/g, wikiTokens === "keep" ? "$1" : " ")
|
|
38
|
+
.replace(wikiLinkPattern(), wikiTokens === "keep" ? "$1" : " ");
|
|
29
39
|
return out
|
|
30
40
|
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) → text
|
|
31
41
|
.replace(/^\s*\|(?:\s*:?-+:?\s*\|)+\s*$/gm, " ") // table separator rows
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where every sheet cell lives in the authored bytes.
|
|
3
|
+
*
|
|
4
|
+
* `parseSheet` hands the reader a grid of DECODED strings — trimmed, escapes
|
|
5
|
+
* resolved, short rows padded. That is the right shape to draw and the wrong
|
|
6
|
+
* shape to write back through: re-serializing the whole grid from it would
|
|
7
|
+
* rewrite the author's column padding, their outer pipes, their delimiter row,
|
|
8
|
+
* every byte they lined up by hand.
|
|
9
|
+
*
|
|
10
|
+
* So an edit is a splice, not a re-serialization. This module answers "which
|
|
11
|
+
* byte range is that cell?" and writes a single cell in place. Everything the
|
|
12
|
+
* author wrote outside the edited cell survives untouched, which is what makes
|
|
13
|
+
* the no-op case byte-identical by construction rather than by luck:
|
|
14
|
+
* `writeSheetCell(source, r, c, currentText)` returns the same string back.
|
|
15
|
+
*
|
|
16
|
+
* The escape rules here are a second reading of the same grammar `sheetCells`
|
|
17
|
+
* in `blocks/parsers.ts` reads, and a second reading is only trustworthy if
|
|
18
|
+
* something checks it against the first. `sheetCellSpans.test.ts` does: every
|
|
19
|
+
* span's slice is fed back through `parseSheet` and must equal the cell the
|
|
20
|
+
* parser reports. A divergence is a test failure, never a silent misalignment.
|
|
21
|
+
*
|
|
22
|
+
* The CODE-SPAN half is no longer a second reading at all. While this module
|
|
23
|
+
* lived in `src/components/editor/` it could not reach the engine's private
|
|
24
|
+
* helpers, so it carried a hand-rolled `codeSpanMask` that closed a backtick
|
|
25
|
+
* run with `indexOf` — which lets a longer run close a shorter one, where
|
|
26
|
+
* CommonMark (and `lineGeometry.codeSpanMask`, which `sheetCells` uses) wants
|
|
27
|
+
* the closing run to be exactly as long as the opening one. Moving the file
|
|
28
|
+
* into the engine (board card #278) retires the copy: both readings of "where
|
|
29
|
+
* are the code spans" are now the one function, so the two cannot drift.
|
|
30
|
+
*/
|
|
31
|
+
/** One cell's trimmed content, addressed in the block source. */
|
|
32
|
+
export type SheetCellSpan = {
|
|
33
|
+
/** Offset of the first byte of trimmed content. */
|
|
34
|
+
start: number;
|
|
35
|
+
/** Offset just past the last byte. `start === end` for an empty cell. */
|
|
36
|
+
end: number;
|
|
37
|
+
/** The text the reader shows — the same string `parseSheet` reports. */
|
|
38
|
+
text: string;
|
|
39
|
+
};
|
|
40
|
+
/** One source line's cells, plus where a missing trailing cell would go. */
|
|
41
|
+
export type SheetRowSpan = {
|
|
42
|
+
/** Cells present in the source. May be shorter than the column count. */
|
|
43
|
+
cells: SheetCellSpan[];
|
|
44
|
+
/** End-of-line offset, where a short row grows. */
|
|
45
|
+
appendAt: number;
|
|
46
|
+
/** Whether the line closes with a pipe, which decides the append shape. */
|
|
47
|
+
trailingPipe: boolean;
|
|
48
|
+
};
|
|
49
|
+
export type SheetCellGrid = {
|
|
50
|
+
columns: number;
|
|
51
|
+
header: SheetRowSpan;
|
|
52
|
+
rows: SheetRowSpan[];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Build the source-anchored grid, or null when the source is not a sheet the
|
|
56
|
+
* reader would draw — the same null the renderer already falls back on.
|
|
57
|
+
*
|
|
58
|
+
* The cell TEXTS come from `parseSheet`, never from this file's own decoding:
|
|
59
|
+
* one reader of the grammar owns what a cell says, and this module only owns
|
|
60
|
+
* where it sits. When the two disagree about how many cells the header has,
|
|
61
|
+
* the grid degrades to null instead of handing back a misaligned map.
|
|
62
|
+
*/
|
|
63
|
+
export declare function sheetCellGrid(source: string): SheetCellGrid | null;
|
|
64
|
+
/**
|
|
65
|
+
* Encode a value for a table cell.
|
|
66
|
+
*
|
|
67
|
+
* The inverse of the engine's decoding over the alphabet that decoding
|
|
68
|
+
* touches: `\` and `|`. Everything else is left as the author typed it —
|
|
69
|
+
* the sheet renders cell text literally, so nothing else needs a spelling.
|
|
70
|
+
* A row cannot hold a line break, so whitespace runs collapse to one space.
|
|
71
|
+
*/
|
|
72
|
+
export declare function encodeSheetCell(value: string): string;
|
|
73
|
+
export type SheetWriteResult = {
|
|
74
|
+
ok: true;
|
|
75
|
+
source: string;
|
|
76
|
+
/** The value as the reader will now show it, after normalization. */
|
|
77
|
+
value: string;
|
|
78
|
+
/** False when the write was a no-op — `source` is then the input string. */
|
|
79
|
+
changed: boolean;
|
|
80
|
+
/** True when a line break or padding was collapsed to fit a row. */
|
|
81
|
+
normalized: boolean;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
reason: "no-grid" | "row-out-of-range" | "column-out-of-range";
|
|
85
|
+
/** Always the untouched input, so a caller can assign unconditionally. */
|
|
86
|
+
source: string;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Write one cell and return the whole block source back.
|
|
90
|
+
*
|
|
91
|
+
* Total by construction: every refusal names itself, nothing returns
|
|
92
|
+
* `undefined`, and the unchanged source always comes back on the result so a
|
|
93
|
+
* caller cannot accidentally drop the document by not checking `ok`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function writeSheetCell(source: string, rowIndex: number, columnIndex: number, value: string): SheetWriteResult;
|