@glw907/cairn-cms 0.58.0 → 0.60.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/CHANGELOG.md +84 -0
- package/dist/components/CairnAdmin.svelte +3 -0
- package/dist/components/CairnMediaLibrary.svelte +1101 -27
- package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
- package/dist/components/CairnTidySettings.svelte +553 -0
- package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
- package/dist/components/EditPage.svelte +371 -2
- package/dist/components/MarkdownEditor.svelte +168 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
- package/dist/components/TidyReview.svelte +463 -0
- package/dist/components/TidyReview.svelte.d.ts +47 -0
- package/dist/components/admin-icons.d.ts +1 -0
- package/dist/components/admin-icons.js +1 -0
- package/dist/components/cairn-admin.css +913 -2
- package/dist/components/editor-tidy.d.ts +31 -0
- package/dist/components/editor-tidy.js +199 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +16 -0
- package/dist/components/markdown-directives.js +34 -0
- package/dist/components/objective-errors.d.ts +30 -0
- package/dist/components/objective-errors.js +113 -0
- package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/dist/components/spellcheck-worker.d.ts +80 -0
- package/dist/components/spellcheck-worker.js +161 -0
- package/dist/components/spellcheck.d.ts +146 -0
- package/dist/components/spellcheck.js +541 -0
- package/dist/components/tidy-categorize.d.ts +67 -0
- package/dist/components/tidy-categorize.js +392 -0
- package/dist/components/tidy-diff.d.ts +60 -0
- package/dist/components/tidy-diff.js +147 -0
- package/dist/components/tidy-validate.d.ts +37 -0
- package/dist/components/tidy-validate.js +174 -0
- package/dist/content/compose.d.ts +1 -1
- package/dist/content/compose.js +11 -0
- package/dist/content/site-dictionary.d.ts +31 -0
- package/dist/content/site-dictionary.js +82 -0
- package/dist/content/types.d.ts +25 -0
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +55 -6
- package/dist/doctor/index.js +2 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/media/bulk-delete-plan.d.ts +24 -0
- package/dist/media/bulk-delete-plan.js +25 -0
- package/dist/media/orphan-scan.d.ts +37 -0
- package/dist/media/orphan-scan.js +42 -0
- package/dist/media/reconcile.d.ts +3 -0
- package/dist/media/reconcile.js +3 -2
- package/dist/nav/site-config.d.ts +98 -0
- package/dist/nav/site-config.js +132 -0
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +6 -2
- package/dist/sveltekit/cairn-admin.d.ts +16 -1
- package/dist/sveltekit/cairn-admin.js +28 -3
- package/dist/sveltekit/content-routes.d.ts +171 -4
- package/dist/sveltekit/content-routes.js +597 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/tidy-prompt.d.ts +11 -0
- package/dist/sveltekit/tidy-prompt.js +118 -0
- package/package.json +10 -1
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
- package/src/lib/components/CairnTidySettings.svelte +553 -0
- package/src/lib/components/EditPage.svelte +371 -2
- package/src/lib/components/MarkdownEditor.svelte +168 -1
- package/src/lib/components/TidyReview.svelte +463 -0
- package/src/lib/components/admin-icons.ts +1 -0
- package/src/lib/components/cairn-admin.css +25 -0
- package/src/lib/components/editor-tidy.ts +241 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +35 -0
- package/src/lib/components/objective-errors.ts +155 -0
- package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/src/lib/components/spellcheck-worker.ts +279 -0
- package/src/lib/components/spellcheck.ts +679 -0
- package/src/lib/components/tidy-categorize.ts +460 -0
- package/src/lib/components/tidy-diff.ts +196 -0
- package/src/lib/components/tidy-validate.ts +202 -0
- package/src/lib/content/compose.ts +11 -1
- package/src/lib/content/site-dictionary.ts +84 -0
- package/src/lib/content/types.ts +25 -0
- package/src/lib/doctor/checks-local.ts +59 -5
- package/src/lib/doctor/index.ts +2 -0
- package/src/lib/log/events.ts +9 -1
- package/src/lib/media/bulk-delete-plan.ts +54 -0
- package/src/lib/media/orphan-scan.ts +74 -0
- package/src/lib/media/reconcile.ts +3 -2
- package/src/lib/nav/site-config.ts +197 -0
- package/src/lib/sveltekit/admin-dispatch.ts +7 -3
- package/src/lib/sveltekit/cairn-admin.ts +38 -4
- package/src/lib/sveltekit/content-routes.ts +795 -7
- package/src/lib/sveltekit/index.ts +1 -0
- package/src/lib/sveltekit/tidy-prompt.ts +153 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// The local tidy category taxonomy and the because-line builder (spec 2.5, decision 9). The tidy
|
|
2
|
+
// action returns a corrected STRING; the diff (Task 12) turns it into changes; this module infers each
|
|
3
|
+
// change's category and safety rank LOCALLY from the diff shape and the enabled config, never from a
|
|
4
|
+
// claim the model made and never from a count of the author's own usage. It is pure: the inputs are a
|
|
5
|
+
// change, the captured original, and the resolved conventions, and the outputs are a category and an
|
|
6
|
+
// optional because-line. Approximate by design, so it is unit-tested rather than trusted.
|
|
7
|
+
//
|
|
8
|
+
// The safety rank is the spine. Objective categories (spelling, typo, doubled word, whitespace) read
|
|
9
|
+
// quiet and are swept by Accept-fixes. Judgment categories (a declared normalization, or a grammar fix
|
|
10
|
+
// that reworded more than one token) carry the review-this treatment and are never swept until the
|
|
11
|
+
// author confirms each. The category alone decides the rank, so the surface and the bulk action agree.
|
|
12
|
+
/** True for the objective categories: the safe, pre-kept, Accept-fixes-swept rank. A judgment
|
|
13
|
+
* category (`normalization` or `grammar`) returns false. The bulk action and the surface both read
|
|
14
|
+
* this, so the safety rank is one source of truth. */
|
|
15
|
+
export function isObjective(category) {
|
|
16
|
+
return (category.kind === 'spelling' ||
|
|
17
|
+
category.kind === 'typo' ||
|
|
18
|
+
category.kind === 'doubled' ||
|
|
19
|
+
category.kind === 'whitespace');
|
|
20
|
+
}
|
|
21
|
+
// The token boundary the diff uses, so a change's word/non-word token count here matches the diff's.
|
|
22
|
+
const TOKEN = /[A-Za-z0-9_]+(?:['’][A-Za-z0-9_]+)*|[^A-Za-z0-9_]+/g;
|
|
23
|
+
// The en-dash and em-dash code points, named here so the comments below never type the literal glyph
|
|
24
|
+
// (the prose-guard rejects a literal dash even in a comment). Used by the punctuation conventions.
|
|
25
|
+
const EN_DASH = '–';
|
|
26
|
+
const EM_DASH = '—';
|
|
27
|
+
function tokens(text) {
|
|
28
|
+
return text.match(TOKEN) ?? [];
|
|
29
|
+
}
|
|
30
|
+
// The spelled-out number words the numberStyle convention recognizes against a numeral, the conservative
|
|
31
|
+
// clear cases only. A swap is claimed as a numberStyle normalization only when one side is one of these
|
|
32
|
+
// words and the other side is a plain integer numeral; a compound spelled number ("twenty-five") or any
|
|
33
|
+
// word outside this set is left to the shape rules, never falsely claimed.
|
|
34
|
+
const NUMBER_WORDS = new Set([
|
|
35
|
+
'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
|
|
36
|
+
'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen',
|
|
37
|
+
'nineteen', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety',
|
|
38
|
+
'hundred', 'thousand', 'million', 'billion',
|
|
39
|
+
]);
|
|
40
|
+
// The unit notation pairs the measurements convention recognizes: each spelled-out unit and its
|
|
41
|
+
// abbreviation, in both singular and plural where the word inflects. A swap is claimed as a measurements
|
|
42
|
+
// normalization only when one side is a known abbreviation and the other its spelled-out form, the number
|
|
43
|
+
// untouched (the diff isolates the unit token). The list is deliberately a curated common set, so a unit
|
|
44
|
+
// outside it is left to the shape rules rather than guessed at.
|
|
45
|
+
const UNIT_FORMS = [
|
|
46
|
+
{ abbr: 'cm', words: ['centimeter', 'centimeters', 'centimetre', 'centimetres'] },
|
|
47
|
+
{ abbr: 'mm', words: ['millimeter', 'millimeters', 'millimetre', 'millimetres'] },
|
|
48
|
+
{ abbr: 'm', words: ['meter', 'meters', 'metre', 'metres'] },
|
|
49
|
+
{ abbr: 'km', words: ['kilometer', 'kilometers', 'kilometre', 'kilometres'] },
|
|
50
|
+
{ abbr: 'in', words: ['inch', 'inches'] },
|
|
51
|
+
{ abbr: 'ft', words: ['foot', 'feet'] },
|
|
52
|
+
{ abbr: 'yd', words: ['yard', 'yards'] },
|
|
53
|
+
{ abbr: 'mi', words: ['mile', 'miles'] },
|
|
54
|
+
{ abbr: 'g', words: ['gram', 'grams', 'gramme', 'grammes'] },
|
|
55
|
+
{ abbr: 'kg', words: ['kilogram', 'kilograms', 'kilogramme', 'kilogrammes'] },
|
|
56
|
+
{ abbr: 'mg', words: ['milligram', 'milligrams'] },
|
|
57
|
+
{ abbr: 'lb', words: ['pound', 'pounds'] },
|
|
58
|
+
{ abbr: 'oz', words: ['ounce', 'ounces'] },
|
|
59
|
+
{ abbr: 'l', words: ['liter', 'liters', 'litre', 'litres'] },
|
|
60
|
+
{ abbr: 'ml', words: ['milliliter', 'milliliters', 'millilitre', 'millilitres'] },
|
|
61
|
+
];
|
|
62
|
+
// True when `a` and `b` are the two notations of one measurement unit (one the abbreviation, the other a
|
|
63
|
+
// spelled-out form). Case-insensitive on the word side; the abbreviation is compared exactly so a stray
|
|
64
|
+
// word like "in" the preposition is not mistaken for the inch abbreviation unless the other side is its
|
|
65
|
+
// spelled-out unit. Order-independent: either side may be the abbreviation.
|
|
66
|
+
function isUnitNotationPair(a, b) {
|
|
67
|
+
for (const u of UNIT_FORMS) {
|
|
68
|
+
const aAbbr = a === u.abbr;
|
|
69
|
+
const bAbbr = b === u.abbr;
|
|
70
|
+
const aWord = u.words.includes(a.toLowerCase());
|
|
71
|
+
const bWord = u.words.includes(b.toLowerCase());
|
|
72
|
+
if ((aAbbr && bWord) || (bAbbr && aWord))
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
// The clock-time signature for a token: its digits and meridiem reduced to a canonical key, or null when
|
|
78
|
+
// the token does not read as a time. Whitespace and the periods in "p.m." are dropped and the letters are
|
|
79
|
+
// lowercased, so "5pm", "5 PM", and "5 p.m." all reduce to "5pm" and a reshape between any two of them is
|
|
80
|
+
// recognized as the same time in a different format.
|
|
81
|
+
function timeKey(token) {
|
|
82
|
+
const compact = token.replace(/[\s.]/g, '').toLowerCase();
|
|
83
|
+
const m = /^(\d{1,2})(:\d{2})?(am|pm)$/.exec(compact);
|
|
84
|
+
if (!m)
|
|
85
|
+
return null;
|
|
86
|
+
return `${m[1]}${m[2] ?? ''}${m[3]}`;
|
|
87
|
+
}
|
|
88
|
+
function words(text) {
|
|
89
|
+
return tokens(text).filter((t) => /[A-Za-z0-9_]/.test(t));
|
|
90
|
+
}
|
|
91
|
+
function isWhitespaceOnly(text) {
|
|
92
|
+
return text.length > 0 && /^\s+$/.test(text);
|
|
93
|
+
}
|
|
94
|
+
function isPunctuationOnly(text) {
|
|
95
|
+
return text.length > 0 && /^[^A-Za-z0-9_\s]+$/.test(text);
|
|
96
|
+
}
|
|
97
|
+
/** The word ending immediately before `offset` in `text`, skipping any whitespace just before the
|
|
98
|
+
* offset, or null when none. The doubled-word rule reads it to confirm the deleted word repeats the
|
|
99
|
+
* one before it. Pure text inspection, never a count. */
|
|
100
|
+
function precedingWord(text, offset) {
|
|
101
|
+
let i = offset;
|
|
102
|
+
while (i > 0 && /\s/.test(text[i - 1]))
|
|
103
|
+
i--;
|
|
104
|
+
let j = i;
|
|
105
|
+
while (j > 0 && /[A-Za-z0-9_'’]/.test(text[j - 1]))
|
|
106
|
+
j--;
|
|
107
|
+
return j < i ? text.slice(j, i) : null;
|
|
108
|
+
}
|
|
109
|
+
/** The word starting immediately after `offset` in `text`, skipping any whitespace just after the
|
|
110
|
+
* offset, or null when none. The doubled-word rule reads it as the other half of the look-around. */
|
|
111
|
+
function followingWord(text, offset) {
|
|
112
|
+
let i = offset;
|
|
113
|
+
while (i < text.length && /\s/.test(text[i]))
|
|
114
|
+
i++;
|
|
115
|
+
let j = i;
|
|
116
|
+
while (j < text.length && /[A-Za-z0-9_'’]/.test(text[j]))
|
|
117
|
+
j++;
|
|
118
|
+
return j > i ? text.slice(i, j) : null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Categorize one change against the captured original and the resolved conventions. The rules are
|
|
122
|
+
* deterministic and ordered by safety, objective first:
|
|
123
|
+
* 1. a pure whitespace change (both sides whitespace, or a whitespace insert/delete) is whitespace;
|
|
124
|
+
* 2. a removed repeated word (the original run is two of the same word collapsing to one) is doubled;
|
|
125
|
+
* 3. a single-token punctuation-only change is a typo;
|
|
126
|
+
* 4. a single word replaced by another single word is spelling;
|
|
127
|
+
* 5. a change matching an ENABLED config convention's signature is that convention's normalization;
|
|
128
|
+
* 6. anything else (a multi-token reword) is grammar.
|
|
129
|
+
* A change that looks like a normalization but whose convention is not enabled falls through to typo,
|
|
130
|
+
* spelling, or grammar by its shape, never to a normalization it cannot name. So the surface never
|
|
131
|
+
* offers a normalization that cannot cite an enabled setting.
|
|
132
|
+
*/
|
|
133
|
+
export function categorize(change, original, conventions) {
|
|
134
|
+
const removed = original.slice(change.from, change.to);
|
|
135
|
+
const added = change.replacement;
|
|
136
|
+
// Whitespace: the removed and added runs are each whitespace-only or empty, and at least one is
|
|
137
|
+
// non-empty whitespace. A trailing-space trim (whitespace removed, nothing added) or a run
|
|
138
|
+
// collapsed to a single space both land here.
|
|
139
|
+
const removedWs = removed === '' || isWhitespaceOnly(removed);
|
|
140
|
+
const addedWs = added === '' || isWhitespaceOnly(added);
|
|
141
|
+
if (removedWs && addedWs && (isWhitespaceOnly(removed) || isWhitespaceOnly(added))) {
|
|
142
|
+
return { kind: 'whitespace' };
|
|
143
|
+
}
|
|
144
|
+
// Doubled word: a repeated word collapses to one. The diff keeps the first copy and deletes the
|
|
145
|
+
// second, so the change reads as a deletion of "<whitespace><word>" with an empty replacement, where
|
|
146
|
+
// that word equals the word immediately before the change in the original. The look-back at the
|
|
147
|
+
// preceding word is what tells a doubled word from a plain deletion; it reads the original text, never
|
|
148
|
+
// a usage count. (A change whose own run already holds both copies, "word word" to "word", is the
|
|
149
|
+
// fallback form, handled by the same word-equality test.)
|
|
150
|
+
const removedWords = words(removed);
|
|
151
|
+
const addedWords = words(added);
|
|
152
|
+
if (removedWords.length === 1 && addedWords.length === 0 && /\S/.test(removed)) {
|
|
153
|
+
const w = removedWords[0].toLowerCase();
|
|
154
|
+
// The diff may delete either copy of the pair: the surviving copy is the word just before or just
|
|
155
|
+
// after the deleted run in the original. Either match confirms a doubled word.
|
|
156
|
+
const before = precedingWord(original, change.from);
|
|
157
|
+
const after = followingWord(original, change.to);
|
|
158
|
+
if ((before && before.toLowerCase() === w) || (after && after.toLowerCase() === w)) {
|
|
159
|
+
return { kind: 'doubled' };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (removedWords.length === 2 &&
|
|
163
|
+
addedWords.length === 1 &&
|
|
164
|
+
removedWords[0].toLowerCase() === removedWords[1].toLowerCase() &&
|
|
165
|
+
addedWords[0].toLowerCase() === removedWords[0].toLowerCase()) {
|
|
166
|
+
return { kind: 'doubled' };
|
|
167
|
+
}
|
|
168
|
+
// The single-token shape: exactly one token removed and one token added (a clean replacement),
|
|
169
|
+
// which is how a typo fix and a spelling fix both read.
|
|
170
|
+
const removedTokens = tokens(removed);
|
|
171
|
+
const addedTokens = tokens(added);
|
|
172
|
+
const singleSwap = removedTokens.length === 1 && addedTokens.length === 1;
|
|
173
|
+
// A declared normalization: the change matches an enabled convention's signature. Checked before the
|
|
174
|
+
// single-word spelling rule only when the convention applies (a punctuation or notation change), so a
|
|
175
|
+
// plain misspelling is never miscategorized as a normalization. A normalization is offered ONLY when
|
|
176
|
+
// its config variant is enabled.
|
|
177
|
+
const norm = matchNormalization(removed, added, conventions);
|
|
178
|
+
if (norm)
|
|
179
|
+
return { kind: 'normalization', convention: norm };
|
|
180
|
+
// A single-token punctuation-only change (a stray or wrong mark fixed) is a typo. Reached only after
|
|
181
|
+
// the normalization check, so an enabled punctuation convention claims its change first.
|
|
182
|
+
if (singleSwap && isPunctuationOnly(removed) && isPunctuationOnly(added)) {
|
|
183
|
+
return { kind: 'typo' };
|
|
184
|
+
}
|
|
185
|
+
// A punctuation insert or delete (a missing period added, say) with no other token is also a typo.
|
|
186
|
+
if ((removed === '' && addedTokens.length === 1 && isPunctuationOnly(added)) ||
|
|
187
|
+
(added === '' && removedTokens.length === 1 && isPunctuationOnly(removed))) {
|
|
188
|
+
return { kind: 'typo' };
|
|
189
|
+
}
|
|
190
|
+
// A single word replaced by another single word is a spelling fix.
|
|
191
|
+
if (singleSwap && removedWords.length === 1 && addedWords.length === 1) {
|
|
192
|
+
return { kind: 'spelling' };
|
|
193
|
+
}
|
|
194
|
+
// Anything else is a grammar reword: a multi-token change the author should review.
|
|
195
|
+
return { kind: 'grammar' };
|
|
196
|
+
}
|
|
197
|
+
// Match a change against the enabled conventions' signatures. Returns the convention key when the
|
|
198
|
+
// change's shape is what that convention produces AND the config has it enabled, else null. The
|
|
199
|
+
// signatures are deliberately narrow: each recognizes only the unambiguous form of its convention, so
|
|
200
|
+
// a false match is rare and a missed match falls to the shape-based category (never to a normalization
|
|
201
|
+
// the config did not authorize). Never counts the author's own usage; the only gate is the config.
|
|
202
|
+
function matchNormalization(removed, added, c) {
|
|
203
|
+
// Oxford comma: a serial comma added before the final conjunction (a space becomes a comma then a
|
|
204
|
+
// space) or removed. The diff isolates the punctuation run, so the signature is a comma appearing or
|
|
205
|
+
// disappearing with the surrounding space.
|
|
206
|
+
if (c.oxfordComma === 'always' && /^\s*$/.test(removed) && /^,\s*$/.test(added)) {
|
|
207
|
+
return 'oxfordComma';
|
|
208
|
+
}
|
|
209
|
+
if (c.oxfordComma === 'never' && /^,\s*$/.test(removed) && /^\s*$/.test(added)) {
|
|
210
|
+
return 'oxfordComma';
|
|
211
|
+
}
|
|
212
|
+
// Percent: the word to the sign or back, the whole token swapped.
|
|
213
|
+
if (c.percent === 'sign' && /^percent$/i.test(removed.trim()) && added.trim() === '%') {
|
|
214
|
+
return 'percent';
|
|
215
|
+
}
|
|
216
|
+
if (c.percent === 'word' && removed.trim() === '%' && /^percent$/i.test(added.trim())) {
|
|
217
|
+
return 'percent';
|
|
218
|
+
}
|
|
219
|
+
// Ellipsis: three dots to the single character or back.
|
|
220
|
+
if (c.ellipsis === 'single-char' && removed.includes('...') && added.includes('…')) {
|
|
221
|
+
return 'ellipsis';
|
|
222
|
+
}
|
|
223
|
+
if (c.ellipsis === 'three-dots' && removed.includes('…') && added.includes('...')) {
|
|
224
|
+
return 'ellipsis';
|
|
225
|
+
}
|
|
226
|
+
// En-dash ranges: a hyphen between two numbers becomes an en dash. The diff isolates the separator
|
|
227
|
+
// token between the numbers, so the signature is a hyphen run becoming an en-dash run.
|
|
228
|
+
if (c.enDashRanges && removed.trim() === '-' && added.trim() === EN_DASH) {
|
|
229
|
+
return 'enDashRanges';
|
|
230
|
+
}
|
|
231
|
+
// Em-dash spacing: the spacing around an em dash changes. The dash stays; only the whitespace around
|
|
232
|
+
// it moves, so the change run is the dash-plus-spacing token and the dash count is preserved.
|
|
233
|
+
if (c.emDash !== undefined && removed.includes(EM_DASH) && added.includes(EM_DASH) && removed !== added) {
|
|
234
|
+
if (removed.replace(/\s/g, '') === added.replace(/\s/g, ''))
|
|
235
|
+
return 'emDash';
|
|
236
|
+
}
|
|
237
|
+
// Smart quotes: a straight quote becomes a curly one (or an apostrophe). The signature is a straight
|
|
238
|
+
// quote in the removed run and its curly counterpart in the added run, the letters preserved.
|
|
239
|
+
if (c.smartQuotes &&
|
|
240
|
+
/['"]/.test(removed) &&
|
|
241
|
+
/[‘’“”]/.test(added) &&
|
|
242
|
+
removed.replace(/['"]/g, '') === added.replace(/[‘’“”]/g, '')) {
|
|
243
|
+
return 'smartQuotes';
|
|
244
|
+
}
|
|
245
|
+
// Number style: a spelled-out number word swapped for a plain integer numeral, or back. The diff
|
|
246
|
+
// isolates the single number token, so the signature is one trimmed side a known number word and the
|
|
247
|
+
// other a digit run. Only the clear single-word cases are claimed; a compound spelled number is left to
|
|
248
|
+
// the shape rules. The always-numeral exception sets (ages, dates, measurements, percentages) are the
|
|
249
|
+
// model's job in the prompt; this categorizer only labels the swap that landed.
|
|
250
|
+
if (c.numberStyle !== undefined) {
|
|
251
|
+
const r = removed.trim().toLowerCase();
|
|
252
|
+
const a = added.trim().toLowerCase();
|
|
253
|
+
const wordToNumeral = NUMBER_WORDS.has(r) && /^\d+$/.test(a);
|
|
254
|
+
const numeralToWord = /^\d+$/.test(r) && NUMBER_WORDS.has(a);
|
|
255
|
+
if (wordToNumeral || numeralToWord)
|
|
256
|
+
return 'numberStyle';
|
|
257
|
+
}
|
|
258
|
+
// Measurements: a unit abbreviation swapped for its spelled-out form, or back, the number untouched.
|
|
259
|
+
// The diff isolates the unit token, so the signature is the two trimmed sides forming one unit's
|
|
260
|
+
// notation pair. Notation only, never the system and never the number, exactly the convention's scope.
|
|
261
|
+
if (c.measurements !== undefined && isUnitNotationPair(removed.trim(), added.trim())) {
|
|
262
|
+
return 'measurements';
|
|
263
|
+
}
|
|
264
|
+
// Time format: a clock time reshaped between "5pm", "5 PM", and "5 p.m." styles. This claims only the
|
|
265
|
+
// case where the diff isolates the whole time as one change, so both sides reduce to the same time key.
|
|
266
|
+
// A reshape that adds or moves a space the diff splits into a separate whitespace and letter hunk
|
|
267
|
+
// (for example "5 PM" to "5 p.m."); that case is left to the shape rules, where it stays a judgment
|
|
268
|
+
// hunk that defaults to undecided, so it is still never swept by Accept-fixes.
|
|
269
|
+
if (c.timeFormat !== undefined) {
|
|
270
|
+
const rKey = timeKey(removed.trim());
|
|
271
|
+
const aKey = timeKey(added.trim());
|
|
272
|
+
if (rKey !== null && rKey === aKey && removed.trim() !== added.trim())
|
|
273
|
+
return 'timeFormat';
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Build the because-line for a normalization category. Its ONLY data source is the config-declared
|
|
279
|
+
* setting that authorized the hunk: the convention key indexes the enabled variant on the conventions,
|
|
280
|
+
* and the line names that setting and variant. It NEVER counts the author's own usage. Counting the
|
|
281
|
+
* author's habit to justify a change is the harmonize-to-author judgment cairn must never make, so no
|
|
282
|
+
* code path here reads the buffer or any usage statistic; the conventions are the sole input. Returns
|
|
283
|
+
* null when the convention is somehow not enabled (defensive: categorize never produces such a hunk).
|
|
284
|
+
*/
|
|
285
|
+
export function buildBecause(key, conventions) {
|
|
286
|
+
switch (key) {
|
|
287
|
+
case 'oxfordComma': {
|
|
288
|
+
if (conventions.oxfordComma === undefined)
|
|
289
|
+
return null;
|
|
290
|
+
const variant = conventions.oxfordComma;
|
|
291
|
+
let effect;
|
|
292
|
+
switch (variant) {
|
|
293
|
+
case 'always':
|
|
294
|
+
effect = 'tidy adds the serial comma before the final "and"';
|
|
295
|
+
break;
|
|
296
|
+
case 'never':
|
|
297
|
+
effect = 'tidy removes the serial comma before the final "and"';
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
effect = 'tidy applies the serial comma to a complex series';
|
|
301
|
+
}
|
|
302
|
+
return { label: 'Oxford-comma', variant, effect };
|
|
303
|
+
}
|
|
304
|
+
case 'numberStyle': {
|
|
305
|
+
if (conventions.numberStyle === undefined)
|
|
306
|
+
return null;
|
|
307
|
+
return { label: 'number-style', variant: conventions.numberStyle, effect: 'tidy applies your number style' };
|
|
308
|
+
}
|
|
309
|
+
case 'measurements': {
|
|
310
|
+
if (conventions.measurements === undefined)
|
|
311
|
+
return null;
|
|
312
|
+
return {
|
|
313
|
+
label: 'measurement',
|
|
314
|
+
variant: conventions.measurements,
|
|
315
|
+
effect: 'tidy applies your measurement notation',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
case 'percent': {
|
|
319
|
+
if (conventions.percent === undefined)
|
|
320
|
+
return null;
|
|
321
|
+
const variant = conventions.percent === 'sign' ? 'the sign' : 'the word';
|
|
322
|
+
const effect = conventions.percent === 'sign' ? 'tidy uses the "%" sign' : 'tidy uses the word "percent"';
|
|
323
|
+
return { label: 'percent', variant, effect };
|
|
324
|
+
}
|
|
325
|
+
case 'emDash': {
|
|
326
|
+
if (conventions.emDash === undefined)
|
|
327
|
+
return null;
|
|
328
|
+
return { label: 'em-dash', variant: conventions.emDash, effect: 'tidy applies your em-dash spacing' };
|
|
329
|
+
}
|
|
330
|
+
case 'enDashRanges': {
|
|
331
|
+
if (!conventions.enDashRanges)
|
|
332
|
+
return null;
|
|
333
|
+
return { label: 'number-range', variant: 'en dash', effect: 'tidy uses an en dash between numbers' };
|
|
334
|
+
}
|
|
335
|
+
case 'ellipsis': {
|
|
336
|
+
if (conventions.ellipsis === undefined)
|
|
337
|
+
return null;
|
|
338
|
+
return { label: 'ellipsis', variant: conventions.ellipsis, effect: 'tidy applies your ellipsis style' };
|
|
339
|
+
}
|
|
340
|
+
case 'timeFormat': {
|
|
341
|
+
if (conventions.timeFormat === undefined)
|
|
342
|
+
return null;
|
|
343
|
+
return { label: 'time-format', variant: conventions.timeFormat, effect: 'tidy renders the time that way' };
|
|
344
|
+
}
|
|
345
|
+
case 'smartQuotes': {
|
|
346
|
+
if (!conventions.smartQuotes)
|
|
347
|
+
return null;
|
|
348
|
+
return { label: 'smart-quotes', variant: 'on', effect: 'tidy curls the straight quote' };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** The human badge label for a category, the word shown in the hunk's category pill. A normalization's
|
|
353
|
+
* label is the convention's display name (its comma style, its time format), never "consistency" and
|
|
354
|
+
* never a count. */
|
|
355
|
+
export function categoryLabel(category) {
|
|
356
|
+
switch (category.kind) {
|
|
357
|
+
case 'spelling':
|
|
358
|
+
return 'Spelling';
|
|
359
|
+
case 'typo':
|
|
360
|
+
return 'Punctuation';
|
|
361
|
+
case 'doubled':
|
|
362
|
+
return 'Doubled word';
|
|
363
|
+
case 'whitespace':
|
|
364
|
+
return 'Whitespace';
|
|
365
|
+
case 'grammar':
|
|
366
|
+
return 'Grammar';
|
|
367
|
+
case 'normalization':
|
|
368
|
+
return normalizationLabel(category.convention);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function normalizationLabel(key) {
|
|
372
|
+
switch (key) {
|
|
373
|
+
case 'oxfordComma':
|
|
374
|
+
return 'Comma style';
|
|
375
|
+
case 'numberStyle':
|
|
376
|
+
return 'Number style';
|
|
377
|
+
case 'measurements':
|
|
378
|
+
return 'Measurements';
|
|
379
|
+
case 'percent':
|
|
380
|
+
return 'Percent';
|
|
381
|
+
case 'emDash':
|
|
382
|
+
return 'Em-dash style';
|
|
383
|
+
case 'enDashRanges':
|
|
384
|
+
return 'Number range';
|
|
385
|
+
case 'ellipsis':
|
|
386
|
+
return 'Ellipsis';
|
|
387
|
+
case 'timeFormat':
|
|
388
|
+
return 'Time format';
|
|
389
|
+
case 'smartQuotes':
|
|
390
|
+
return 'Smart quotes';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** One run of the token diff. A run is contiguous tokens of a single kind. */
|
|
2
|
+
export interface DiffRange {
|
|
3
|
+
/**
|
|
4
|
+
* `equal` for tokens kept from the original, `deleted` for tokens removed from the original,
|
|
5
|
+
* `inserted` for tokens that appear only in the corrected text.
|
|
6
|
+
*/
|
|
7
|
+
kind: 'equal' | 'inserted' | 'deleted';
|
|
8
|
+
/**
|
|
9
|
+
* The offset into the captured ORIGINAL where this run begins. For `equal` and `deleted` runs
|
|
10
|
+
* this is the start of the run's text in the original. For an `inserted` run there is no original
|
|
11
|
+
* span, so `from === to`: the offset is the insertion point in the original.
|
|
12
|
+
*/
|
|
13
|
+
from: number;
|
|
14
|
+
/** The offset into the captured ORIGINAL where this run ends. For an `inserted` run, equal to `from`. */
|
|
15
|
+
to: number;
|
|
16
|
+
/** The actual token text of this run (original text for equal/deleted, corrected text for inserted). */
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A change: the unit the review UI accepts and rejects. A change is a deletion, an insertion, or a
|
|
21
|
+
* deletion immediately followed by an insertion that reads as a replacement. Each change is a faithful
|
|
22
|
+
* edit recipe against the captured original: splice `replacement` over the original span `[from, to)`.
|
|
23
|
+
*/
|
|
24
|
+
export interface Change {
|
|
25
|
+
/** A stable, gap-free index (0, 1, 2, ...) assigned in document order. */
|
|
26
|
+
index: number;
|
|
27
|
+
/** The start offset of the change's span in the captured ORIGINAL. */
|
|
28
|
+
from: number;
|
|
29
|
+
/**
|
|
30
|
+
* The end offset of the change's span in the captured ORIGINAL. A pure insertion has a zero-width
|
|
31
|
+
* span (`from === to`); a pure deletion has a non-empty span with an empty `replacement`.
|
|
32
|
+
*/
|
|
33
|
+
to: number;
|
|
34
|
+
/** The text to splice over `[from, to)`. Empty for a pure deletion. */
|
|
35
|
+
replacement: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Diff the original against the corrected text and return runs of equal, inserted, and deleted tokens.
|
|
39
|
+
* Both strings are tokenized into words plus the whitespace and punctuation between them, an LCS over
|
|
40
|
+
* the token sequences finds the kept tokens, and the gaps become deleted and inserted runs.
|
|
41
|
+
*
|
|
42
|
+
* Run offsets index the captured ORIGINAL: an `equal` or `deleted` run spans its original text, an
|
|
43
|
+
* `inserted` run carries a zero-width original span at the insertion point. Concatenating the equal
|
|
44
|
+
* and deleted runs rebuilds the original; concatenating the equal and inserted runs rebuilds the
|
|
45
|
+
* corrected text.
|
|
46
|
+
*/
|
|
47
|
+
export declare function diffTokens(original: string, corrected: string): DiffRange[];
|
|
48
|
+
/**
|
|
49
|
+
* Group the token diff into changes, the unit the review UI accepts and rejects. A run of deletions,
|
|
50
|
+
* a run of insertions, or a deletion run immediately followed by an insertion run (a replacement) all
|
|
51
|
+
* collapse into one change. Equal runs separate changes. Each change carries the original span to
|
|
52
|
+
* replace and the replacement text, with a stable index in document order.
|
|
53
|
+
*/
|
|
54
|
+
export declare function diffChanges(original: string, corrected: string): Change[];
|
|
55
|
+
/**
|
|
56
|
+
* The 1-based line number of an offset in the original, computed by counting newlines before it. The
|
|
57
|
+
* review surface derives every line label this way, from the offset against the captured original, so
|
|
58
|
+
* a label can never drift from the source or depend on a count the model supplied.
|
|
59
|
+
*/
|
|
60
|
+
export declare function lineLabel(original: string, offset: number): number;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// The tidy diff: a Longest Common Subsequence over tokens, poplar's DiffRanges model rebuilt in
|
|
2
|
+
// TypeScript (spec 2.4). A small pure module, not a diff library. The tidy action returns only the
|
|
3
|
+
// corrected string; this module owns every range, offset, and line label. It is the sole source of
|
|
4
|
+
// positional truth for the review surface (Tasks 13 and 14 consume its output), so all positions are
|
|
5
|
+
// computed locally from this diff against the captured original, never taken from the model.
|
|
6
|
+
//
|
|
7
|
+
// Token granularity is the right unit for a copy-edit: a one-letter fix like "it's" to "its" reads
|
|
8
|
+
// as a whole-word replacement an author accepts or rejects as a unit, not a confusing single-character
|
|
9
|
+
// flip. The diff is computed against the original captured at request time; tidy is single-author and
|
|
10
|
+
// on-demand, so there is no rebasing and no three-way merge.
|
|
11
|
+
// A token is either a word (a run of word characters, apostrophes kept inside so "it's" is one token)
|
|
12
|
+
// or a non-word run (whitespace and punctuation between words). Splitting at the word boundary gives
|
|
13
|
+
// whole-word granularity: a homophone or typo fix lands on the word, not a single character.
|
|
14
|
+
const TOKEN = /[A-Za-z0-9_]+(?:['’][A-Za-z0-9_]+)*|[^A-Za-z0-9_]+/g;
|
|
15
|
+
function tokenize(text) {
|
|
16
|
+
const tokens = [];
|
|
17
|
+
for (const m of text.matchAll(TOKEN)) {
|
|
18
|
+
tokens.push({ text: m[0], offset: m.index });
|
|
19
|
+
}
|
|
20
|
+
return tokens;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Diff the original against the corrected text and return runs of equal, inserted, and deleted tokens.
|
|
24
|
+
* Both strings are tokenized into words plus the whitespace and punctuation between them, an LCS over
|
|
25
|
+
* the token sequences finds the kept tokens, and the gaps become deleted and inserted runs.
|
|
26
|
+
*
|
|
27
|
+
* Run offsets index the captured ORIGINAL: an `equal` or `deleted` run spans its original text, an
|
|
28
|
+
* `inserted` run carries a zero-width original span at the insertion point. Concatenating the equal
|
|
29
|
+
* and deleted runs rebuilds the original; concatenating the equal and inserted runs rebuilds the
|
|
30
|
+
* corrected text.
|
|
31
|
+
*/
|
|
32
|
+
export function diffTokens(original, corrected) {
|
|
33
|
+
const a = tokenize(original);
|
|
34
|
+
const b = tokenize(corrected);
|
|
35
|
+
const n = a.length;
|
|
36
|
+
const m = b.length;
|
|
37
|
+
// Standard LCS table over the token sequences.
|
|
38
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
39
|
+
for (let i = 1; i <= n; i++) {
|
|
40
|
+
for (let j = 1; j <= m; j++) {
|
|
41
|
+
if (a[i - 1].text === b[j - 1].text) {
|
|
42
|
+
lcs[i][j] = lcs[i - 1][j - 1] + 1;
|
|
43
|
+
}
|
|
44
|
+
else if (lcs[i - 1][j] >= lcs[i][j - 1]) {
|
|
45
|
+
lcs[i][j] = lcs[i - 1][j];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
lcs[i][j] = lcs[i][j - 1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const reversed = [];
|
|
53
|
+
let i = n;
|
|
54
|
+
let j = m;
|
|
55
|
+
while (i > 0 || j > 0) {
|
|
56
|
+
if (i > 0 && j > 0 && a[i - 1].text === b[j - 1].text) {
|
|
57
|
+
reversed.push({ kind: 'equal', from: a[i - 1].offset, to: a[i - 1].offset + a[i - 1].text.length, text: a[i - 1].text });
|
|
58
|
+
i--;
|
|
59
|
+
j--;
|
|
60
|
+
}
|
|
61
|
+
else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
|
|
62
|
+
// The original offset of an insertion is the start of the next kept original token (the one
|
|
63
|
+
// at index i), or the end of the original when nothing more remains.
|
|
64
|
+
const at = i < n ? a[i].offset : original.length;
|
|
65
|
+
reversed.push({ kind: 'inserted', from: at, to: at, text: b[j - 1].text });
|
|
66
|
+
j--;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
reversed.push({ kind: 'deleted', from: a[i - 1].offset, to: a[i - 1].offset + a[i - 1].text.length, text: a[i - 1].text });
|
|
70
|
+
i--;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const ops = reversed.reverse();
|
|
74
|
+
// Coalesce adjacent ops of the same kind into runs. A run's text is the concatenation of its
|
|
75
|
+
// tokens; offsets span from the first token's `from` to the last token's `to`.
|
|
76
|
+
const runs = [];
|
|
77
|
+
for (const op of ops) {
|
|
78
|
+
const last = runs[runs.length - 1];
|
|
79
|
+
if (last && last.kind === op.kind && last.to === op.from) {
|
|
80
|
+
last.to = op.to;
|
|
81
|
+
last.text += op.text;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
runs.push({ kind: op.kind, from: op.from, to: op.to, text: op.text });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return runs;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Group the token diff into changes, the unit the review UI accepts and rejects. A run of deletions,
|
|
91
|
+
* a run of insertions, or a deletion run immediately followed by an insertion run (a replacement) all
|
|
92
|
+
* collapse into one change. Equal runs separate changes. Each change carries the original span to
|
|
93
|
+
* replace and the replacement text, with a stable index in document order.
|
|
94
|
+
*/
|
|
95
|
+
export function diffChanges(original, corrected) {
|
|
96
|
+
const runs = diffTokens(original, corrected);
|
|
97
|
+
const changes = [];
|
|
98
|
+
let k = 0;
|
|
99
|
+
while (k < runs.length) {
|
|
100
|
+
const run = runs[k];
|
|
101
|
+
if (run.kind === 'equal') {
|
|
102
|
+
k++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Start a change at the first non-equal run and absorb the contiguous deleted/inserted block.
|
|
106
|
+
// A deletion immediately followed by an insertion reads as a replacement; either alone is a
|
|
107
|
+
// pure deletion or insertion.
|
|
108
|
+
let from = run.from;
|
|
109
|
+
let to = run.from;
|
|
110
|
+
let replacement = '';
|
|
111
|
+
while (k < runs.length && runs[k].kind !== 'equal') {
|
|
112
|
+
const r = runs[k];
|
|
113
|
+
if (r.kind === 'deleted') {
|
|
114
|
+
// A deleted run spans original text; extend the original span to cover it.
|
|
115
|
+
if (replacement === '' && to === from)
|
|
116
|
+
from = r.from;
|
|
117
|
+
to = r.to;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// An inserted run contributes replacement text and pins the span start at its insertion
|
|
121
|
+
// point when no deletion has set it yet (a pure insertion is zero-width).
|
|
122
|
+
if (to === from) {
|
|
123
|
+
from = r.from;
|
|
124
|
+
to = r.from;
|
|
125
|
+
}
|
|
126
|
+
replacement += r.text;
|
|
127
|
+
}
|
|
128
|
+
k++;
|
|
129
|
+
}
|
|
130
|
+
changes.push({ index: changes.length, from, to, replacement });
|
|
131
|
+
}
|
|
132
|
+
return changes;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The 1-based line number of an offset in the original, computed by counting newlines before it. The
|
|
136
|
+
* review surface derives every line label this way, from the offset against the captured original, so
|
|
137
|
+
* a label can never drift from the source or depend on a count the model supplied.
|
|
138
|
+
*/
|
|
139
|
+
export function lineLabel(original, offset) {
|
|
140
|
+
let line = 1;
|
|
141
|
+
const end = Math.min(offset, original.length);
|
|
142
|
+
for (let i = 0; i < end; i++) {
|
|
143
|
+
if (original[i] === '\n')
|
|
144
|
+
line++;
|
|
145
|
+
}
|
|
146
|
+
return line;
|
|
147
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Change } from './tidy-diff.js';
|
|
2
|
+
/** The reason a tidy result was rejected. Task 14 branches on this; every value maps to the one
|
|
3
|
+
* honest author-facing message, so the reason is for logging and tests, not the user surface.
|
|
4
|
+
* - `structure`: a directive opener/closer sequence, a heading count or level, or a fenced-code
|
|
5
|
+
* count diverged (the result restructured the document).
|
|
6
|
+
* - `frontmatter`: the frontmatter block is not byte-for-byte equal.
|
|
7
|
+
* - `media`: the multiset of `media:` hashes differs (a hash was altered, dropped, or invented).
|
|
8
|
+
* - `code`: a code span or fenced code block was edited.
|
|
9
|
+
* - `divergence`: the changed-token amount exceeds the length-aware bound (a wholesale rewrite). */
|
|
10
|
+
export type TidyRejectionReason = 'structure' | 'frontmatter' | 'media' | 'code' | 'divergence';
|
|
11
|
+
/** The honest author-facing message a rejection maps to. The same message for every reason, by
|
|
12
|
+
* design: an author does not need the validator's internal taxonomy, only that the result was
|
|
13
|
+
* discarded and their text is safe. */
|
|
14
|
+
export declare const TIDY_REJECTION_MESSAGE = "Tidy returned a result that changed more than the wording, so it was discarded. Your text is unchanged.";
|
|
15
|
+
/** The outcome of validating a tidy result. On success it carries the Task 12 change set the review
|
|
16
|
+
* surface accepts and rejects against; on failure it carries the typed reason and the message. */
|
|
17
|
+
export type TidyValidation = {
|
|
18
|
+
ok: true;
|
|
19
|
+
changes: Change[];
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: TidyRejectionReason;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Validate a tidy result against the captured original. Runs the exact structural checks first (a
|
|
27
|
+
* restructure or a token or code edit is a hard reject regardless of how little else changed), then
|
|
28
|
+
* the length-aware divergence bound. On success returns the Task 12 change set for the review
|
|
29
|
+
* surface; on failure returns the typed reason and the one honest message.
|
|
30
|
+
*
|
|
31
|
+
* The checks, in order: the directive opener/closer sequence and depths, the ATX heading count and
|
|
32
|
+
* levels, the fenced-code-block count (folded into the code-contents multiset), the byte-for-byte
|
|
33
|
+
* frontmatter via the shared frontmatterSpan helper, the media-hash multiset, the code-span and
|
|
34
|
+
* code-block contents, and finally the divergence bound. A pure function: it reads the two strings
|
|
35
|
+
* and nothing else, and it never mutates the buffer.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateTidy(original: string, corrected: string): TidyValidation;
|