docsanity 0.1.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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/chunk-DQY4MKSJ.js +514 -0
- package/dist/chunk-DQY4MKSJ.js.map +1 -0
- package/dist/cli.cjs +831 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +319 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +535 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +226 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var readlevel = require('@didrod2539/readlevel');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var linklint = require('@didrod2539/linklint');
|
|
6
|
+
|
|
7
|
+
// src/checks/readability.ts
|
|
8
|
+
function readabilityCheck(page, config) {
|
|
9
|
+
const text = page.md.text;
|
|
10
|
+
const wordCount = text ? text.split(/\s+/).filter(Boolean).length : 0;
|
|
11
|
+
if (wordCount < config.minWordsForReadability) {
|
|
12
|
+
return { findings: [] };
|
|
13
|
+
}
|
|
14
|
+
const a = readlevel.analyze(text);
|
|
15
|
+
const summary = {
|
|
16
|
+
grade: a.grade,
|
|
17
|
+
gradeLabel: a.gradeLabel,
|
|
18
|
+
ease: a.ease,
|
|
19
|
+
words: a.words,
|
|
20
|
+
readingTimeSeconds: a.readingTimeSeconds
|
|
21
|
+
};
|
|
22
|
+
const findings = [];
|
|
23
|
+
if (a.grade > config.maxGrade) {
|
|
24
|
+
findings.push({
|
|
25
|
+
dimension: "readability",
|
|
26
|
+
rule: "readability.too-hard",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `Reads at grade ${a.grade.toFixed(0)} (${a.gradeLabel}); target \u2264 grade ${config.maxGrade}`,
|
|
29
|
+
detail: `Flesch ease ${a.readability.fleschReadingEase.toFixed(0)} (${a.ease}), ${a.averageWordsPerSentence.toFixed(0)} words/sentence`,
|
|
30
|
+
fix: "Shorten sentences and prefer common words to lower the reading grade."
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { findings, summary };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/frontmatter.ts
|
|
37
|
+
function unquote(raw) {
|
|
38
|
+
const s = raw.trim();
|
|
39
|
+
if (s.startsWith('"') && s.endsWith('"') && s.length >= 2 || s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
|
|
40
|
+
return s.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
function coerce(raw) {
|
|
45
|
+
const s = raw.trim();
|
|
46
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
47
|
+
return s.slice(1, -1).split(",").map((x) => unquote(x)).filter((x) => x.length > 0);
|
|
48
|
+
}
|
|
49
|
+
if (s === "true" || s === "false") return s === "true";
|
|
50
|
+
if (s !== "" && !Number.isNaN(Number(s)) && /^-?\d/.test(s)) return Number(s);
|
|
51
|
+
return unquote(s);
|
|
52
|
+
}
|
|
53
|
+
function parseFrontmatter(content) {
|
|
54
|
+
const normalized = content.replace(/^/, "");
|
|
55
|
+
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(normalized);
|
|
56
|
+
if (!match) {
|
|
57
|
+
return { data: {}, body: content, offset: 0, present: false };
|
|
58
|
+
}
|
|
59
|
+
const block = match[1];
|
|
60
|
+
const data = {};
|
|
61
|
+
const lines = block.split(/\r?\n/);
|
|
62
|
+
let pendingListKey = null;
|
|
63
|
+
let pendingList = [];
|
|
64
|
+
const flush = () => {
|
|
65
|
+
if (pendingListKey !== null) {
|
|
66
|
+
data[pendingListKey] = pendingList;
|
|
67
|
+
pendingListKey = null;
|
|
68
|
+
pendingList = [];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (/^\s*-\s+/.test(line) && pendingListKey !== null) {
|
|
73
|
+
pendingList.push(unquote(line.replace(/^\s*-\s+/, "")));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
flush();
|
|
77
|
+
const kv = /^([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line);
|
|
78
|
+
if (!kv) continue;
|
|
79
|
+
const key = kv[1];
|
|
80
|
+
const value = kv[2];
|
|
81
|
+
if (value.trim() === "") {
|
|
82
|
+
pendingListKey = key;
|
|
83
|
+
pendingList = [];
|
|
84
|
+
} else {
|
|
85
|
+
data[key] = coerce(value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
flush();
|
|
89
|
+
const offset = match[0].split(/\r?\n/).length - 1;
|
|
90
|
+
return { data, body: normalized.slice(match[0].length), offset, present: true };
|
|
91
|
+
}
|
|
92
|
+
function asString(value) {
|
|
93
|
+
if (value === void 0) return "";
|
|
94
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
95
|
+
return String(value).trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/checks/seo.ts
|
|
99
|
+
function pageTitle(page) {
|
|
100
|
+
const fm = asString(page.frontmatter.data["title"]);
|
|
101
|
+
if (fm) return { value: fm, source: "frontmatter" };
|
|
102
|
+
const h1 = page.md.headings.find((h) => h.level === 1);
|
|
103
|
+
if (h1) return { value: h1.text, source: "h1" };
|
|
104
|
+
return { value: "", source: null };
|
|
105
|
+
}
|
|
106
|
+
function description(page) {
|
|
107
|
+
return asString(page.frontmatter.data["description"]);
|
|
108
|
+
}
|
|
109
|
+
function buildDuplicateIndex(pages) {
|
|
110
|
+
const titles = /* @__PURE__ */ new Map();
|
|
111
|
+
const descriptions = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const page of pages) {
|
|
113
|
+
const t = pageTitle(page).value.trim().toLowerCase();
|
|
114
|
+
if (t) titles.set(t, [...titles.get(t) ?? [], page.relPath]);
|
|
115
|
+
const d = description(page).trim().toLowerCase();
|
|
116
|
+
if (d) descriptions.set(d, [...descriptions.get(d) ?? [], page.relPath]);
|
|
117
|
+
}
|
|
118
|
+
return { titles, descriptions };
|
|
119
|
+
}
|
|
120
|
+
function seoChecks(page, config, dup) {
|
|
121
|
+
const out = [];
|
|
122
|
+
const required = new Set(config.requireFrontmatter);
|
|
123
|
+
const title = pageTitle(page);
|
|
124
|
+
const desc = description(page);
|
|
125
|
+
if (required.has("title")) {
|
|
126
|
+
if (!title.value) {
|
|
127
|
+
out.push({
|
|
128
|
+
dimension: "seo",
|
|
129
|
+
rule: "seo.missing-title",
|
|
130
|
+
severity: "error",
|
|
131
|
+
message: "Page has no title (no frontmatter `title` and no H1)",
|
|
132
|
+
fix: "Add a `title:` to the frontmatter or an `# H1` heading."
|
|
133
|
+
});
|
|
134
|
+
} else if (title.source === "h1") {
|
|
135
|
+
out.push({
|
|
136
|
+
dimension: "seo",
|
|
137
|
+
rule: "seo.frontmatter-title",
|
|
138
|
+
severity: "info",
|
|
139
|
+
message: "Title comes from the H1; add an explicit frontmatter `title` for SEO control",
|
|
140
|
+
detail: `Using H1: "${title.value}"`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (title.value && title.value.length > config.titleMax) {
|
|
144
|
+
out.push({
|
|
145
|
+
dimension: "seo",
|
|
146
|
+
rule: "seo.title-too-long",
|
|
147
|
+
severity: "warning",
|
|
148
|
+
message: `Title is ${title.value.length} chars (recommended \u2264 ${config.titleMax})`,
|
|
149
|
+
fix: "Shorten the title so it isn't truncated in search results."
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (required.has("description")) {
|
|
154
|
+
if (!desc) {
|
|
155
|
+
out.push({
|
|
156
|
+
dimension: "seo",
|
|
157
|
+
rule: "seo.missing-description",
|
|
158
|
+
severity: "warning",
|
|
159
|
+
message: "No frontmatter `description`",
|
|
160
|
+
fix: `Add a ${config.descriptionMin}\u2013${config.descriptionMax} char description for the search snippet.`
|
|
161
|
+
});
|
|
162
|
+
} else if (desc.length < config.descriptionMin) {
|
|
163
|
+
out.push({
|
|
164
|
+
dimension: "seo",
|
|
165
|
+
rule: "seo.description-short",
|
|
166
|
+
severity: "warning",
|
|
167
|
+
message: `Description is ${desc.length} chars (recommended \u2265 ${config.descriptionMin})`
|
|
168
|
+
});
|
|
169
|
+
} else if (desc.length > config.descriptionMax) {
|
|
170
|
+
out.push({
|
|
171
|
+
dimension: "seo",
|
|
172
|
+
rule: "seo.description-long",
|
|
173
|
+
severity: "warning",
|
|
174
|
+
message: `Description is ${desc.length} chars (recommended \u2264 ${config.descriptionMax})`,
|
|
175
|
+
fix: "Trim it so search engines don't truncate the snippet."
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const key of config.requireFrontmatter) {
|
|
180
|
+
if (key === "title" || key === "description") continue;
|
|
181
|
+
if (!asString(page.frontmatter.data[key])) {
|
|
182
|
+
out.push({
|
|
183
|
+
dimension: "seo",
|
|
184
|
+
rule: `seo.missing-frontmatter`,
|
|
185
|
+
severity: "warning",
|
|
186
|
+
message: `Missing required frontmatter: \`${key}\``
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const tKey = title.value.trim().toLowerCase();
|
|
191
|
+
const others = (dup.titles.get(tKey) ?? []).filter((p) => p !== page.relPath);
|
|
192
|
+
if (tKey && others.length) {
|
|
193
|
+
out.push({
|
|
194
|
+
dimension: "seo",
|
|
195
|
+
rule: "seo.duplicate-title",
|
|
196
|
+
severity: "warning",
|
|
197
|
+
message: `Duplicate title \u2014 also used by ${others.length} other page(s)`,
|
|
198
|
+
detail: others.slice(0, 3).join(", "),
|
|
199
|
+
fix: "Give each page a unique title; duplicates compete in search results."
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const dKey = desc.trim().toLowerCase();
|
|
203
|
+
const dOthers = (dup.descriptions.get(dKey) ?? []).filter((p) => p !== page.relPath);
|
|
204
|
+
if (dKey && dOthers.length) {
|
|
205
|
+
out.push({
|
|
206
|
+
dimension: "seo",
|
|
207
|
+
rule: "seo.duplicate-description",
|
|
208
|
+
severity: "warning",
|
|
209
|
+
message: `Duplicate description \u2014 also used by ${dOthers.length} other page(s)`,
|
|
210
|
+
detail: dOthers.slice(0, 3).join(", "),
|
|
211
|
+
fix: "Write a unique description per page."
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/checks/structure.ts
|
|
218
|
+
function structureChecks(page, _config) {
|
|
219
|
+
const out = [];
|
|
220
|
+
const { headings, images, fences } = page.md;
|
|
221
|
+
const h1s = headings.filter((h) => h.level === 1);
|
|
222
|
+
if (h1s.length === 0) {
|
|
223
|
+
out.push({
|
|
224
|
+
dimension: "structure",
|
|
225
|
+
rule: "structure.no-h1",
|
|
226
|
+
severity: "warning",
|
|
227
|
+
message: "No H1 heading",
|
|
228
|
+
fix: "Start the page with a single `# Title`."
|
|
229
|
+
});
|
|
230
|
+
} else if (h1s.length > 1) {
|
|
231
|
+
out.push({
|
|
232
|
+
dimension: "structure",
|
|
233
|
+
rule: "structure.multiple-h1",
|
|
234
|
+
severity: "warning",
|
|
235
|
+
message: `${h1s.length} H1 headings (use exactly one)`,
|
|
236
|
+
line: h1s[1].line,
|
|
237
|
+
fix: "Demote the extra H1s to H2/H3 so the outline has one top level."
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
let prev = 0;
|
|
241
|
+
for (const h of headings) {
|
|
242
|
+
if (prev !== 0 && h.level > prev + 1) {
|
|
243
|
+
out.push({
|
|
244
|
+
dimension: "structure",
|
|
245
|
+
rule: "structure.skipped-heading",
|
|
246
|
+
severity: "warning",
|
|
247
|
+
message: `Heading jumps from H${prev} to H${h.level} ("${h.text}")`,
|
|
248
|
+
line: h.line,
|
|
249
|
+
fix: "Don't skip levels \u2014 screen readers and outlines rely on order."
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
prev = h.level;
|
|
253
|
+
}
|
|
254
|
+
for (const img of images) {
|
|
255
|
+
if (img.alt.trim() === "") {
|
|
256
|
+
out.push({
|
|
257
|
+
dimension: "structure",
|
|
258
|
+
rule: "structure.image-alt",
|
|
259
|
+
severity: "warning",
|
|
260
|
+
message: `Image has no alt text: ${img.src}`,
|
|
261
|
+
line: img.line,
|
|
262
|
+
fix: "Add descriptive alt text: ``."
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
for (const fence of fences) {
|
|
267
|
+
if (fence.lang === null) {
|
|
268
|
+
out.push({
|
|
269
|
+
dimension: "structure",
|
|
270
|
+
rule: "structure.code-language",
|
|
271
|
+
severity: "info",
|
|
272
|
+
message: "Code block has no language tag",
|
|
273
|
+
line: fence.line,
|
|
274
|
+
fix: "Add a language after the opening fence (e.g. ```ts) for highlighting."
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/config.ts
|
|
282
|
+
var DEFAULT_CONFIG = {
|
|
283
|
+
extensions: [".md", ".mdx", ".markdown"],
|
|
284
|
+
requireFrontmatter: ["title", "description"],
|
|
285
|
+
descriptionMin: 50,
|
|
286
|
+
descriptionMax: 160,
|
|
287
|
+
titleMax: 60,
|
|
288
|
+
maxGrade: 14,
|
|
289
|
+
minWordsForReadability: 80,
|
|
290
|
+
disable: [],
|
|
291
|
+
ignore: [],
|
|
292
|
+
minScore: 0
|
|
293
|
+
};
|
|
294
|
+
var CONFIG_FILENAMES = ["docsanity.config.json", ".docsanityrc.json", ".docsanityrc"];
|
|
295
|
+
function isPlainObject(v) {
|
|
296
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
297
|
+
}
|
|
298
|
+
function mergeConfig(base, override) {
|
|
299
|
+
const out = { ...base };
|
|
300
|
+
for (const [k, v] of Object.entries(override ?? {})) {
|
|
301
|
+
if (v !== void 0) out[k] = v;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
function parseConfig(json, label = "config") {
|
|
306
|
+
let data;
|
|
307
|
+
try {
|
|
308
|
+
data = JSON.parse(json);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
throw new Error(`invalid ${label}: ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
if (!isPlainObject(data)) throw new Error(`invalid ${label}: must be a JSON object`);
|
|
313
|
+
return mergeConfig(DEFAULT_CONFIG, data);
|
|
314
|
+
}
|
|
315
|
+
function isDimensionEnabled(config, dim) {
|
|
316
|
+
return !config.disable.includes(dim);
|
|
317
|
+
}
|
|
318
|
+
function mapSeverity(s) {
|
|
319
|
+
return s === "error" || s === "warning" || s === "info" ? s : "warning";
|
|
320
|
+
}
|
|
321
|
+
function linkFindings(root, pages) {
|
|
322
|
+
const inputs = pages.map((p) => ({ path: p.absPath, content: p.content }));
|
|
323
|
+
const project = linklint.buildProjectFromInputs(root, inputs);
|
|
324
|
+
const report = linklint.analyze(project, linklint.DEFAULT_CONFIG);
|
|
325
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const doc of report.documents) {
|
|
327
|
+
byPath.set(
|
|
328
|
+
doc.path,
|
|
329
|
+
doc.issues.map((i) => ({
|
|
330
|
+
dimension: "links",
|
|
331
|
+
rule: `links.${i.rule}`,
|
|
332
|
+
severity: mapSeverity(i.severity),
|
|
333
|
+
message: i.message,
|
|
334
|
+
line: i.line,
|
|
335
|
+
detail: i.detail,
|
|
336
|
+
fix: i.fix
|
|
337
|
+
}))
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
for (const abs of linklint.findOrphans(project)) {
|
|
341
|
+
const rel = path.relative(root, abs) || abs;
|
|
342
|
+
const list = byPath.get(rel) ?? [];
|
|
343
|
+
list.push({
|
|
344
|
+
dimension: "links",
|
|
345
|
+
rule: "links.orphan-document",
|
|
346
|
+
severity: "warning",
|
|
347
|
+
message: "Orphan page \u2014 nothing links here",
|
|
348
|
+
fix: "Link to it from an index/README, or remove it if it's obsolete."
|
|
349
|
+
});
|
|
350
|
+
byPath.set(rel, list);
|
|
351
|
+
}
|
|
352
|
+
return byPath;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/markdown.ts
|
|
356
|
+
var FENCE_RE = /^(\s*)(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/;
|
|
357
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
358
|
+
var IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
359
|
+
var HTML_IMG_RE = /<img\b[^>]*>/gi;
|
|
360
|
+
function htmlImgAlt(tag) {
|
|
361
|
+
const m = /\balt\s*=\s*["']([^"']*)["']/i.exec(tag);
|
|
362
|
+
return m ? m[1] : "";
|
|
363
|
+
}
|
|
364
|
+
function htmlImgSrc(tag) {
|
|
365
|
+
const m = /\bsrc\s*=\s*["']([^"']*)["']/i.exec(tag);
|
|
366
|
+
return m ? m[1] : "";
|
|
367
|
+
}
|
|
368
|
+
function parseMarkdown(body, lineOffset = 0) {
|
|
369
|
+
const lines = body.split(/\r?\n/);
|
|
370
|
+
const headings = [];
|
|
371
|
+
const images = [];
|
|
372
|
+
const fences = [];
|
|
373
|
+
const textParts = [];
|
|
374
|
+
let inFence = false;
|
|
375
|
+
let fenceMarker = "";
|
|
376
|
+
for (let i = 0; i < lines.length; i++) {
|
|
377
|
+
const raw = lines[i];
|
|
378
|
+
const lineNo = i + 1 + lineOffset;
|
|
379
|
+
const fence = FENCE_RE.exec(raw);
|
|
380
|
+
if (fence && (!inFence || raw.trim().startsWith(fenceMarker))) {
|
|
381
|
+
const marker = fence[2];
|
|
382
|
+
if (!inFence) {
|
|
383
|
+
inFence = true;
|
|
384
|
+
fenceMarker = marker[0].repeat(3);
|
|
385
|
+
fences.push({ lang: fence[3] ? fence[3] : null, line: lineNo });
|
|
386
|
+
} else {
|
|
387
|
+
inFence = false;
|
|
388
|
+
fenceMarker = "";
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (inFence) continue;
|
|
393
|
+
const heading = HEADING_RE.exec(raw);
|
|
394
|
+
if (heading) {
|
|
395
|
+
headings.push({ level: heading[1].length, text: heading[2].trim(), line: lineNo });
|
|
396
|
+
textParts.push(heading[2].trim());
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
let m;
|
|
400
|
+
IMAGE_RE.lastIndex = 0;
|
|
401
|
+
while ((m = IMAGE_RE.exec(raw)) !== null) {
|
|
402
|
+
images.push({ alt: m[1], src: m[2], line: lineNo });
|
|
403
|
+
}
|
|
404
|
+
HTML_IMG_RE.lastIndex = 0;
|
|
405
|
+
while ((m = HTML_IMG_RE.exec(raw)) !== null) {
|
|
406
|
+
images.push({ alt: htmlImgAlt(m[0]), src: htmlImgSrc(m[0]), line: lineNo });
|
|
407
|
+
}
|
|
408
|
+
textParts.push(stripInline(raw));
|
|
409
|
+
}
|
|
410
|
+
return { headings, images, fences, text: textParts.join("\n").trim() };
|
|
411
|
+
}
|
|
412
|
+
function stripInline(line) {
|
|
413
|
+
return line.replace(IMAGE_RE, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/<[^>]+>/g, "").replace(/^[>\s]*>/g, "").replace(/^\s{0,3}([*+-]|\d+\.)\s+/g, "").replace(/[*_~]+/g, "").trim();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/page.ts
|
|
417
|
+
function parsePage(absPath, relPath, content) {
|
|
418
|
+
const frontmatter = parseFrontmatter(content);
|
|
419
|
+
const md = parseMarkdown(frontmatter.body, frontmatter.offset);
|
|
420
|
+
return { absPath, relPath, content, frontmatter, md };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/score.ts
|
|
424
|
+
var PENALTY = { error: 12, warning: 4, info: 1, pass: 0 };
|
|
425
|
+
function scorePage(findings) {
|
|
426
|
+
let penalty = 0;
|
|
427
|
+
for (const f of findings) penalty += PENALTY[f.severity] ?? 0;
|
|
428
|
+
return Math.max(0, 100 - penalty);
|
|
429
|
+
}
|
|
430
|
+
function gradeFor(score) {
|
|
431
|
+
if (score >= 90) return "A";
|
|
432
|
+
if (score >= 80) return "B";
|
|
433
|
+
if (score >= 70) return "C";
|
|
434
|
+
if (score >= 60) return "D";
|
|
435
|
+
return "F";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/types.ts
|
|
439
|
+
var DIMENSIONS = ["links", "seo", "readability", "structure"];
|
|
440
|
+
var DIMENSION_LABELS = {
|
|
441
|
+
links: "Links & references",
|
|
442
|
+
seo: "SEO & frontmatter",
|
|
443
|
+
readability: "Readability",
|
|
444
|
+
structure: "Structure & a11y"
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// src/analyze.ts
|
|
448
|
+
function analyzeDocs(root, inputs, config, meta) {
|
|
449
|
+
const pages = inputs.map((i) => parsePage(i.absPath, i.relPath, i.content));
|
|
450
|
+
const ignore = new Set(config.ignore);
|
|
451
|
+
const dup = buildDuplicateIndex(pages);
|
|
452
|
+
const links = isDimensionEnabled(config, "links") ? linkFindings(root, pages) : /* @__PURE__ */ new Map();
|
|
453
|
+
const pageReports = pages.map((page) => {
|
|
454
|
+
const findings = [];
|
|
455
|
+
let readability;
|
|
456
|
+
if (isDimensionEnabled(config, "links")) {
|
|
457
|
+
findings.push(...links.get(page.relPath) ?? []);
|
|
458
|
+
}
|
|
459
|
+
if (isDimensionEnabled(config, "seo")) {
|
|
460
|
+
findings.push(...seoChecks(page, config, dup));
|
|
461
|
+
}
|
|
462
|
+
if (isDimensionEnabled(config, "structure")) {
|
|
463
|
+
findings.push(...structureChecks(page));
|
|
464
|
+
}
|
|
465
|
+
if (isDimensionEnabled(config, "readability")) {
|
|
466
|
+
const r = readabilityCheck(page, config);
|
|
467
|
+
findings.push(...r.findings);
|
|
468
|
+
readability = r.summary;
|
|
469
|
+
}
|
|
470
|
+
const kept = findings.filter((f) => !ignore.has(f.rule));
|
|
471
|
+
const counts = { error: 0, warning: 0, info: 0 };
|
|
472
|
+
for (const f of kept) {
|
|
473
|
+
if (f.severity === "error") counts.error++;
|
|
474
|
+
else if (f.severity === "warning") counts.warning++;
|
|
475
|
+
else if (f.severity === "info") counts.info++;
|
|
476
|
+
}
|
|
477
|
+
const score2 = scorePage(kept);
|
|
478
|
+
return {
|
|
479
|
+
path: page.relPath,
|
|
480
|
+
title: pageTitle(page).value || null,
|
|
481
|
+
score: score2,
|
|
482
|
+
grade: gradeFor(score2),
|
|
483
|
+
counts,
|
|
484
|
+
findings: kept,
|
|
485
|
+
readability
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
pageReports.sort((a, b) => a.path.localeCompare(b.path));
|
|
489
|
+
const errors = pageReports.reduce((s, p) => s + p.counts.error, 0);
|
|
490
|
+
const warnings = pageReports.reduce((s, p) => s + p.counts.warning, 0);
|
|
491
|
+
const infos = pageReports.reduce((s, p) => s + p.counts.info, 0);
|
|
492
|
+
const score = pageReports.length ? Math.round(pageReports.reduce((s, p) => s + p.score, 0) / pageReports.length) : 100;
|
|
493
|
+
const byDimension = Object.fromEntries(
|
|
494
|
+
DIMENSIONS.map((d) => [d, { errors: 0, warnings: 0, infos: 0 }])
|
|
495
|
+
);
|
|
496
|
+
for (const p of pageReports) {
|
|
497
|
+
for (const f of p.findings) {
|
|
498
|
+
const bucket = byDimension[f.dimension];
|
|
499
|
+
if (f.severity === "error") bucket.errors++;
|
|
500
|
+
else if (f.severity === "warning") bucket.warnings++;
|
|
501
|
+
else if (f.severity === "info") bucket.infos++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
tool: "docsanity",
|
|
506
|
+
version: meta.version,
|
|
507
|
+
generatedAt: meta.generatedAt,
|
|
508
|
+
root,
|
|
509
|
+
summary: { pages: pageReports.length, score, grade: gradeFor(score), errors, warnings, infos, byDimension },
|
|
510
|
+
pages: pageReports
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
exports.CONFIG_FILENAMES = CONFIG_FILENAMES;
|
|
515
|
+
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
|
516
|
+
exports.DIMENSIONS = DIMENSIONS;
|
|
517
|
+
exports.DIMENSION_LABELS = DIMENSION_LABELS;
|
|
518
|
+
exports.analyzeDocs = analyzeDocs;
|
|
519
|
+
exports.asString = asString;
|
|
520
|
+
exports.buildDuplicateIndex = buildDuplicateIndex;
|
|
521
|
+
exports.gradeFor = gradeFor;
|
|
522
|
+
exports.isDimensionEnabled = isDimensionEnabled;
|
|
523
|
+
exports.linkFindings = linkFindings;
|
|
524
|
+
exports.mergeConfig = mergeConfig;
|
|
525
|
+
exports.pageTitle = pageTitle;
|
|
526
|
+
exports.parseConfig = parseConfig;
|
|
527
|
+
exports.parseFrontmatter = parseFrontmatter;
|
|
528
|
+
exports.parseMarkdown = parseMarkdown;
|
|
529
|
+
exports.parsePage = parsePage;
|
|
530
|
+
exports.readabilityCheck = readabilityCheck;
|
|
531
|
+
exports.scorePage = scorePage;
|
|
532
|
+
exports.seoChecks = seoChecks;
|
|
533
|
+
exports.structureChecks = structureChecks;
|
|
534
|
+
//# sourceMappingURL=index.cjs.map
|
|
535
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checks/readability.ts","../src/frontmatter.ts","../src/checks/seo.ts","../src/checks/structure.ts","../src/config.ts","../src/links.ts","../src/markdown.ts","../src/page.ts","../src/score.ts","../src/types.ts","../src/analyze.ts"],"names":["readlevelAnalyze","buildProjectFromInputs","linklintAnalyze","LINKLINT_DEFAULTS","findOrphans","relative","score"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Readability check — leverages the `readlevel` engine on each page's prose and\n * flags pages that read harder than the configured grade ceiling. Docs that are\n * too dense lose readers (and AI answer engines that prefer clear sources).\n */\n\nimport { analyze as readlevelAnalyze } from \"@didrod2539/readlevel\";\nimport type { Config, Finding, ReadabilitySummary } from \"../types.js\";\nimport type { Page } from \"../page.js\";\n\nexport interface ReadabilityResult {\n findings: Finding[];\n summary?: ReadabilitySummary;\n}\n\nexport function readabilityCheck(page: Page, config: Config): ReadabilityResult {\n const text = page.md.text;\n const wordCount = text ? text.split(/\\s+/).filter(Boolean).length : 0;\n\n // Too little prose to judge (e.g. an API reference table or stub).\n if (wordCount < config.minWordsForReadability) {\n return { findings: [] };\n }\n\n const a = readlevelAnalyze(text);\n const summary: ReadabilitySummary = {\n grade: a.grade,\n gradeLabel: a.gradeLabel,\n ease: a.ease,\n words: a.words,\n readingTimeSeconds: a.readingTimeSeconds,\n };\n\n const findings: Finding[] = [];\n if (a.grade > config.maxGrade) {\n findings.push({\n dimension: \"readability\",\n rule: \"readability.too-hard\",\n severity: \"warning\",\n message: `Reads at grade ${a.grade.toFixed(0)} (${a.gradeLabel}); target ≤ grade ${config.maxGrade}`,\n detail: `Flesch ease ${a.readability.fleschReadingEase.toFixed(0)} (${a.ease}), ${a.averageWordsPerSentence.toFixed(0)} words/sentence`,\n fix: \"Shorten sentences and prefer common words to lower the reading grade.\",\n });\n }\n return { findings, summary };\n}\n","/**\n * A small, dependency-free YAML-frontmatter parser. Docs sites (Docusaurus,\n * Astro, Nextra, Hugo, Jekyll, …) put metadata in a `---` fenced block at the\n * top of each Markdown file. We only need scalars and simple lists, so we parse\n * those directly rather than pulling in a full YAML engine.\n */\n\nexport type FrontmatterValue = string | string[] | boolean | number;\n\nexport interface Frontmatter {\n data: Record<string, FrontmatterValue>;\n /** The document body with the frontmatter block removed. */\n body: string;\n /** Number of lines the frontmatter occupied (so body line numbers can be offset). */\n offset: number;\n present: boolean;\n}\n\nfunction unquote(raw: string): string {\n const s = raw.trim();\n if (\n (s.startsWith('\"') && s.endsWith('\"') && s.length >= 2) ||\n (s.startsWith(\"'\") && s.endsWith(\"'\") && s.length >= 2)\n ) {\n return s.slice(1, -1);\n }\n return s;\n}\n\nfunction coerce(raw: string): FrontmatterValue {\n const s = raw.trim();\n // Inline list: [a, b, c]\n if (s.startsWith(\"[\") && s.endsWith(\"]\")) {\n return s\n .slice(1, -1)\n .split(\",\")\n .map((x) => unquote(x))\n .filter((x) => x.length > 0);\n }\n if (s === \"true\" || s === \"false\") return s === \"true\";\n if (s !== \"\" && !Number.isNaN(Number(s)) && /^-?\\d/.test(s)) return Number(s);\n return unquote(s);\n}\n\n/** Parse a leading `---` frontmatter block. Returns empty data when absent. */\nexport function parseFrontmatter(content: string): Frontmatter {\n const normalized = content.replace(/^/, \"\");\n const match = /^---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/.exec(normalized);\n if (!match) {\n return { data: {}, body: content, offset: 0, present: false };\n }\n\n const block = match[1]!;\n const data: Record<string, FrontmatterValue> = {};\n const lines = block.split(/\\r?\\n/);\n\n let pendingListKey: string | null = null;\n let pendingList: string[] = [];\n\n const flush = () => {\n if (pendingListKey !== null) {\n data[pendingListKey] = pendingList;\n pendingListKey = null;\n pendingList = [];\n }\n };\n\n for (const line of lines) {\n if (/^\\s*-\\s+/.test(line) && pendingListKey !== null) {\n pendingList.push(unquote(line.replace(/^\\s*-\\s+/, \"\")));\n continue;\n }\n flush();\n const kv = /^([A-Za-z0-9_.-]+):\\s*(.*)$/.exec(line);\n if (!kv) continue;\n const key = kv[1]!;\n const value = kv[2]!;\n if (value.trim() === \"\") {\n // Could be the start of a block list.\n pendingListKey = key;\n pendingList = [];\n } else {\n data[key] = coerce(value);\n }\n }\n flush();\n\n const offset = match[0].split(/\\r?\\n/).length - 1;\n return { data, body: normalized.slice(match[0].length), offset, present: true };\n}\n\n/** Get a frontmatter value as a trimmed string (lists join with \", \"). */\nexport function asString(value: FrontmatterValue | undefined): string {\n if (value === undefined) return \"\";\n if (Array.isArray(value)) return value.join(\", \");\n return String(value).trim();\n}\n","/**\n * SEO & frontmatter checks. Beyond per-page frontmatter completeness, this\n * includes the **site-wide** checks no single-file linter can do: duplicate\n * titles and duplicate meta descriptions across the whole docs set (both hurt\n * search ranking). That cross-page view is the point of a suite.\n */\n\nimport { asString } from \"../frontmatter.js\";\nimport type { Config, Finding } from \"../types.js\";\nimport type { Page } from \"../page.js\";\n\nexport interface DuplicateIndex {\n titles: Map<string, string[]>;\n descriptions: Map<string, string[]>;\n}\n\n/** Resolve a page's effective title: explicit frontmatter, else the first H1. */\nexport function pageTitle(page: Page): { value: string; source: \"frontmatter\" | \"h1\" | null } {\n const fm = asString(page.frontmatter.data[\"title\"]);\n if (fm) return { value: fm, source: \"frontmatter\" };\n const h1 = page.md.headings.find((h) => h.level === 1);\n if (h1) return { value: h1.text, source: \"h1\" };\n return { value: \"\", source: null };\n}\n\nfunction description(page: Page): string {\n return asString(page.frontmatter.data[\"description\"]);\n}\n\n/** Build the cross-page duplicate index (value → list of page paths). */\nexport function buildDuplicateIndex(pages: Page[]): DuplicateIndex {\n const titles = new Map<string, string[]>();\n const descriptions = new Map<string, string[]>();\n for (const page of pages) {\n const t = pageTitle(page).value.trim().toLowerCase();\n if (t) titles.set(t, [...(titles.get(t) ?? []), page.relPath]);\n const d = description(page).trim().toLowerCase();\n if (d) descriptions.set(d, [...(descriptions.get(d) ?? []), page.relPath]);\n }\n return { titles, descriptions };\n}\n\nexport function seoChecks(page: Page, config: Config, dup: DuplicateIndex): Finding[] {\n const out: Finding[] = [];\n const required = new Set(config.requireFrontmatter);\n const title = pageTitle(page);\n const desc = description(page);\n\n // Title.\n if (required.has(\"title\")) {\n if (!title.value) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.missing-title\",\n severity: \"error\",\n message: \"Page has no title (no frontmatter `title` and no H1)\",\n fix: \"Add a `title:` to the frontmatter or an `# H1` heading.\",\n });\n } else if (title.source === \"h1\") {\n out.push({\n dimension: \"seo\",\n rule: \"seo.frontmatter-title\",\n severity: \"info\",\n message: \"Title comes from the H1; add an explicit frontmatter `title` for SEO control\",\n detail: `Using H1: \"${title.value}\"`,\n });\n }\n if (title.value && title.value.length > config.titleMax) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.title-too-long\",\n severity: \"warning\",\n message: `Title is ${title.value.length} chars (recommended ≤ ${config.titleMax})`,\n fix: \"Shorten the title so it isn't truncated in search results.\",\n });\n }\n }\n\n // Description.\n if (required.has(\"description\")) {\n if (!desc) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.missing-description\",\n severity: \"warning\",\n message: \"No frontmatter `description`\",\n fix: `Add a ${config.descriptionMin}–${config.descriptionMax} char description for the search snippet.`,\n });\n } else if (desc.length < config.descriptionMin) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.description-short\",\n severity: \"warning\",\n message: `Description is ${desc.length} chars (recommended ≥ ${config.descriptionMin})`,\n });\n } else if (desc.length > config.descriptionMax) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.description-long\",\n severity: \"warning\",\n message: `Description is ${desc.length} chars (recommended ≤ ${config.descriptionMax})`,\n fix: \"Trim it so search engines don't truncate the snippet.\",\n });\n }\n }\n\n // Other required keys.\n for (const key of config.requireFrontmatter) {\n if (key === \"title\" || key === \"description\") continue;\n if (!asString(page.frontmatter.data[key])) {\n out.push({\n dimension: \"seo\",\n rule: `seo.missing-frontmatter`,\n severity: \"warning\",\n message: `Missing required frontmatter: \\`${key}\\``,\n });\n }\n }\n\n // Site-wide duplicates.\n const tKey = title.value.trim().toLowerCase();\n const others = (dup.titles.get(tKey) ?? []).filter((p) => p !== page.relPath);\n if (tKey && others.length) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.duplicate-title\",\n severity: \"warning\",\n message: `Duplicate title — also used by ${others.length} other page(s)`,\n detail: others.slice(0, 3).join(\", \"),\n fix: \"Give each page a unique title; duplicates compete in search results.\",\n });\n }\n const dKey = desc.trim().toLowerCase();\n const dOthers = (dup.descriptions.get(dKey) ?? []).filter((p) => p !== page.relPath);\n if (dKey && dOthers.length) {\n out.push({\n dimension: \"seo\",\n rule: \"seo.duplicate-description\",\n severity: \"warning\",\n message: `Duplicate description — also used by ${dOthers.length} other page(s)`,\n detail: dOthers.slice(0, 3).join(\", \"),\n fix: \"Write a unique description per page.\",\n });\n }\n\n return out;\n}\n","/**\n * Structure & accessibility checks on Markdown — the docs-relevant subset of an\n * HTML a11y linter: a single H1, no skipped heading levels, images with alt\n * text, and fenced code blocks that declare a language (for correct syntax\n * highlighting and screen-reader hints).\n */\n\nimport type { Config, Finding } from \"../types.js\";\nimport type { Page } from \"../page.js\";\n\nexport function structureChecks(page: Page, _config: Config): Finding[] {\n const out: Finding[] = [];\n const { headings, images, fences } = page.md;\n\n // Single H1.\n const h1s = headings.filter((h) => h.level === 1);\n if (h1s.length === 0) {\n out.push({\n dimension: \"structure\",\n rule: \"structure.no-h1\",\n severity: \"warning\",\n message: \"No H1 heading\",\n fix: \"Start the page with a single `# Title`.\",\n });\n } else if (h1s.length > 1) {\n out.push({\n dimension: \"structure\",\n rule: \"structure.multiple-h1\",\n severity: \"warning\",\n message: `${h1s.length} H1 headings (use exactly one)`,\n line: h1s[1]!.line,\n fix: \"Demote the extra H1s to H2/H3 so the outline has one top level.\",\n });\n }\n\n // No skipped heading levels (e.g. H2 → H4).\n let prev = 0;\n for (const h of headings) {\n if (prev !== 0 && h.level > prev + 1) {\n out.push({\n dimension: \"structure\",\n rule: \"structure.skipped-heading\",\n severity: \"warning\",\n message: `Heading jumps from H${prev} to H${h.level} (\"${h.text}\")`,\n line: h.line,\n fix: \"Don't skip levels — screen readers and outlines rely on order.\",\n });\n }\n prev = h.level;\n }\n\n // Images need alt text.\n for (const img of images) {\n if (img.alt.trim() === \"\") {\n out.push({\n dimension: \"structure\",\n rule: \"structure.image-alt\",\n severity: \"warning\",\n message: `Image has no alt text: ${img.src}`,\n line: img.line,\n fix: \"Add descriptive alt text: ``.\",\n });\n }\n }\n\n // Code fences should declare a language.\n for (const fence of fences) {\n if (fence.lang === null) {\n out.push({\n dimension: \"structure\",\n rule: \"structure.code-language\",\n severity: \"info\",\n message: \"Code block has no language tag\",\n line: fence.line,\n fix: \"Add a language after the opening fence (e.g. ```ts) for highlighting.\",\n });\n }\n }\n\n return out;\n}\n","/** Configuration defaults, parsing, and resolution (pure — no file I/O). */\n\nimport type { Config, Dimension } from \"./types.js\";\n\nexport const DEFAULT_CONFIG: Config = {\n extensions: [\".md\", \".mdx\", \".markdown\"],\n requireFrontmatter: [\"title\", \"description\"],\n descriptionMin: 50,\n descriptionMax: 160,\n titleMax: 60,\n maxGrade: 14,\n minWordsForReadability: 80,\n disable: [],\n ignore: [],\n minScore: 0,\n};\n\nexport const CONFIG_FILENAMES = [\"docsanity.config.json\", \".docsanityrc.json\", \".docsanityrc\"];\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nexport function mergeConfig(base: Config, override: Partial<Config>): Config {\n const out = { ...base } as unknown as Record<string, unknown>;\n for (const [k, v] of Object.entries(override ?? {})) {\n if (v !== undefined) out[k] = v;\n }\n return out as unknown as Config;\n}\n\nexport function parseConfig(json: string, label = \"config\"): Config {\n let data: unknown;\n try {\n data = JSON.parse(json);\n } catch (e) {\n throw new Error(`invalid ${label}: ${(e as Error).message}`);\n }\n if (!isPlainObject(data)) throw new Error(`invalid ${label}: must be a JSON object`);\n return mergeConfig(DEFAULT_CONFIG, data as Partial<Config>);\n}\n\nexport function isDimensionEnabled(config: Config, dim: Dimension): boolean {\n return !config.disable.includes(dim);\n}\n","/**\n * Links & references — leverages the `linklint` engine, which already builds a\n * cross-file project graph and finds broken relative links, dead `#anchors`,\n * missing images, undefined references and **orphan pages** (nothing links to\n * them). docsanity runs it once over the whole set and folds its findings into\n * the unified per-page report, keyed by each page's root-relative path.\n */\n\nimport { relative } from \"node:path\";\nimport {\n analyze as linklintAnalyze,\n buildProjectFromInputs,\n DEFAULT_CONFIG as LINKLINT_DEFAULTS,\n findOrphans,\n type Issue,\n} from \"@didrod2539/linklint\";\nimport type { Finding, Severity } from \"./types.js\";\nimport type { Page } from \"./page.js\";\n\nfunction mapSeverity(s: Issue[\"severity\"]): Severity {\n return s === \"error\" || s === \"warning\" || s === \"info\" ? s : \"warning\";\n}\n\n/** Run linklint over all pages; return findings keyed by root-relative path. */\nexport function linkFindings(root: string, pages: Page[]): Map<string, Finding[]> {\n const inputs = pages.map((p) => ({ path: p.absPath, content: p.content }));\n const project = buildProjectFromInputs(root, inputs);\n const report = linklintAnalyze(project, LINKLINT_DEFAULTS);\n\n // linklint reports document paths relative to the project root.\n const byPath = new Map<string, Finding[]>();\n for (const doc of report.documents) {\n byPath.set(\n doc.path,\n doc.issues.map((i) => ({\n dimension: \"links\" as const,\n rule: `links.${i.rule}`,\n severity: mapSeverity(i.severity),\n message: i.message,\n line: i.line,\n detail: i.detail,\n fix: i.fix,\n })),\n );\n }\n\n // Orphan detection (off by default in linklint's config) — run it explicitly.\n for (const abs of findOrphans(project)) {\n const rel = relative(root, abs) || abs;\n const list = byPath.get(rel) ?? [];\n list.push({\n dimension: \"links\",\n rule: \"links.orphan-document\",\n severity: \"warning\",\n message: \"Orphan page — nothing links here\",\n fix: \"Link to it from an index/README, or remove it if it's obsolete.\",\n });\n byPath.set(rel, list);\n }\n\n return byPath;\n}\n","/**\n * Lightweight Markdown extraction — headings, images, code fences and a plain\n * text projection — without a full Markdown parser. We track fenced code blocks\n * so `#` inside a code sample is never mistaken for a heading.\n */\n\nexport interface Heading {\n level: number;\n text: string;\n line: number;\n}\n\nexport interface MdImage {\n alt: string;\n src: string;\n line: number;\n}\n\nexport interface CodeFence {\n lang: string | null;\n line: number;\n}\n\nexport interface MarkdownModel {\n headings: Heading[];\n images: MdImage[];\n fences: CodeFence[];\n /** Plain-text projection suitable for readability scoring. */\n text: string;\n}\n\nconst FENCE_RE = /^(\\s*)(`{3,}|~{3,})\\s*([A-Za-z0-9_+-]*)/;\nconst HEADING_RE = /^(#{1,6})\\s+(.+?)\\s*#*\\s*$/;\nconst IMAGE_RE = /!\\[([^\\]]*)\\]\\(([^)\\s]+)(?:\\s+[\"'][^\"']*[\"'])?\\)/g;\nconst HTML_IMG_RE = /<img\\b[^>]*>/gi;\n\nfunction htmlImgAlt(tag: string): string {\n const m = /\\balt\\s*=\\s*[\"']([^\"']*)[\"']/i.exec(tag);\n return m ? m[1]! : \"\";\n}\nfunction htmlImgSrc(tag: string): string {\n const m = /\\bsrc\\s*=\\s*[\"']([^\"']*)[\"']/i.exec(tag);\n return m ? m[1]! : \"\";\n}\n\nexport function parseMarkdown(body: string, lineOffset = 0): MarkdownModel {\n const lines = body.split(/\\r?\\n/);\n const headings: Heading[] = [];\n const images: MdImage[] = [];\n const fences: CodeFence[] = [];\n const textParts: string[] = [];\n\n let inFence = false;\n let fenceMarker = \"\";\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i]!;\n const lineNo = i + 1 + lineOffset;\n\n const fence = FENCE_RE.exec(raw);\n if (fence && (!inFence || raw.trim().startsWith(fenceMarker))) {\n const marker = fence[2]!;\n if (!inFence) {\n inFence = true;\n fenceMarker = marker[0]!.repeat(3);\n fences.push({ lang: fence[3] ? fence[3] : null, line: lineNo });\n } else {\n inFence = false;\n fenceMarker = \"\";\n }\n continue;\n }\n if (inFence) continue; // skip code content entirely\n\n const heading = HEADING_RE.exec(raw);\n if (heading) {\n headings.push({ level: heading[1]!.length, text: heading[2]!.trim(), line: lineNo });\n textParts.push(heading[2]!.trim());\n continue;\n }\n\n let m: RegExpExecArray | null;\n IMAGE_RE.lastIndex = 0;\n while ((m = IMAGE_RE.exec(raw)) !== null) {\n images.push({ alt: m[1]!, src: m[2]!, line: lineNo });\n }\n HTML_IMG_RE.lastIndex = 0;\n while ((m = HTML_IMG_RE.exec(raw)) !== null) {\n images.push({ alt: htmlImgAlt(m[0]), src: htmlImgSrc(m[0]), line: lineNo });\n }\n\n textParts.push(stripInline(raw));\n }\n\n return { headings, images, fences, text: textParts.join(\"\\n\").trim() };\n}\n\n/** Reduce a Markdown line to readable prose for readability scoring. */\nfunction stripInline(line: string): string {\n return line\n .replace(IMAGE_RE, \"\") // images contribute no prose\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n .replace(/`([^`]*)`/g, \"$1\") // inline code\n .replace(/<[^>]+>/g, \"\") // html tags\n .replace(/^[>\\s]*>/g, \"\") // blockquote markers\n .replace(/^\\s{0,3}([*+-]|\\d+\\.)\\s+/g, \"\") // list markers\n .replace(/[*_~]+/g, \"\") // emphasis\n .trim();\n}\n","/** A parsed documentation page: frontmatter + extracted Markdown model. */\n\nimport { parseFrontmatter, type Frontmatter } from \"./frontmatter.js\";\nimport { parseMarkdown, type MarkdownModel } from \"./markdown.js\";\n\nexport interface Page {\n /** Absolute path (needed for cross-file link resolution). */\n absPath: string;\n /** Path relative to the scanned root (for display). */\n relPath: string;\n content: string;\n frontmatter: Frontmatter;\n md: MarkdownModel;\n}\n\nexport function parsePage(absPath: string, relPath: string, content: string): Page {\n const frontmatter = parseFrontmatter(content);\n // Offset Markdown line numbers by the frontmatter block so they map to the file.\n const md = parseMarkdown(frontmatter.body, frontmatter.offset);\n return { absPath, relPath, content, frontmatter, md };\n}\n","/** Weighted scoring: turn findings into a 0–100 score and an A–F grade. */\n\nimport type { Finding } from \"./types.js\";\n\nconst PENALTY: Record<string, number> = { error: 12, warning: 4, info: 1, pass: 0 };\n\n/** Page score: 100 minus capped, weighted penalties for its findings. */\nexport function scorePage(findings: Finding[]): number {\n let penalty = 0;\n for (const f of findings) penalty += PENALTY[f.severity] ?? 0;\n return Math.max(0, 100 - penalty);\n}\n\nexport function gradeFor(score: number): string {\n if (score >= 90) return \"A\";\n if (score >= 80) return \"B\";\n if (score >= 70) return \"C\";\n if (score >= 60) return \"D\";\n return \"F\";\n}\n","/** Core types for docsanity. */\n\nexport type Severity = \"error\" | \"warning\" | \"info\" | \"pass\";\n\n/** The four health dimensions docsanity unifies into one report. */\nexport type Dimension = \"links\" | \"seo\" | \"readability\" | \"structure\";\n\nexport const DIMENSIONS: Dimension[] = [\"links\", \"seo\", \"readability\", \"structure\"];\n\nexport const DIMENSION_LABELS: Record<Dimension, string> = {\n links: \"Links & references\",\n seo: \"SEO & frontmatter\",\n readability: \"Readability\",\n structure: \"Structure & a11y\",\n};\n\nexport interface Finding {\n dimension: Dimension;\n /** Stable id, e.g. \"seo.missing-description\". */\n rule: string;\n severity: Severity;\n message: string;\n /** 1-based line number, when known. */\n line?: number;\n detail?: string;\n fix?: string;\n}\n\nexport interface ReadabilitySummary {\n grade: number;\n gradeLabel: string;\n ease: string;\n words: number;\n readingTimeSeconds: number;\n}\n\nexport interface PageReport {\n /** Path relative to the scanned root. */\n path: string;\n title: string | null;\n score: number;\n grade: string;\n counts: { error: number; warning: number; info: number };\n findings: Finding[];\n readability?: ReadabilitySummary;\n}\n\nexport interface DocsReport {\n tool: \"docsanity\";\n version: string;\n generatedAt: string;\n root: string;\n summary: {\n pages: number;\n score: number;\n grade: string;\n errors: number;\n warnings: number;\n infos: number;\n byDimension: Record<Dimension, { errors: number; warnings: number; infos: number }>;\n };\n pages: PageReport[];\n}\n\nexport interface Config {\n /** File extensions to scan. */\n extensions: string[];\n /** Frontmatter keys every page must have (non-empty). */\n requireFrontmatter: string[];\n /** Recommended meta-description length window. */\n descriptionMin: number;\n descriptionMax: number;\n /** Warn when a title is longer than this (characters). */\n titleMax: number;\n /** Warn when a page's reading grade exceeds this. */\n maxGrade: number;\n /** Minimum word count before readability is scored. */\n minWordsForReadability: number;\n /** Dimensions to skip entirely. */\n disable: Dimension[];\n /** Rule ids to ignore (e.g. \"structure.code-language\"). */\n ignore: string[];\n /** CI gate: overall score floor. */\n minScore: number;\n}\n","/**\n * The orchestrator: parse every page once, run all four dimensions over the\n * shared page set (so site-wide checks like duplicate titles and orphan pages\n * work), and fold everything into one unified report with a combined score.\n */\n\nimport { readabilityCheck } from \"./checks/readability.js\";\nimport { buildDuplicateIndex, pageTitle, seoChecks } from \"./checks/seo.js\";\nimport { structureChecks } from \"./checks/structure.js\";\nimport { isDimensionEnabled } from \"./config.js\";\nimport { linkFindings } from \"./links.js\";\nimport { parsePage, type Page } from \"./page.js\";\nimport { gradeFor, scorePage } from \"./score.js\";\nimport {\n DIMENSIONS,\n type Config,\n type Dimension,\n type DocsReport,\n type Finding,\n type PageReport,\n type ReadabilitySummary,\n} from \"./types.js\";\n\nexport interface InputDoc {\n absPath: string;\n relPath: string;\n content: string;\n}\n\nexport interface AnalyzeMeta {\n version: string;\n generatedAt: string;\n}\n\n/** Analyze a set of documentation pages into a unified {@link DocsReport}. */\nexport function analyzeDocs(\n root: string,\n inputs: InputDoc[],\n config: Config,\n meta: AnalyzeMeta,\n): DocsReport {\n const pages: Page[] = inputs.map((i) => parsePage(i.absPath, i.relPath, i.content));\n const ignore = new Set(config.ignore);\n\n // Cross-page indexes / engines (computed once over the whole set).\n const dup = buildDuplicateIndex(pages);\n const links = isDimensionEnabled(config, \"links\")\n ? linkFindings(root, pages)\n : new Map<string, Finding[]>();\n\n const pageReports: PageReport[] = pages.map((page) => {\n const findings: Finding[] = [];\n let readability: ReadabilitySummary | undefined;\n\n if (isDimensionEnabled(config, \"links\")) {\n findings.push(...(links.get(page.relPath) ?? []));\n }\n if (isDimensionEnabled(config, \"seo\")) {\n findings.push(...seoChecks(page, config, dup));\n }\n if (isDimensionEnabled(config, \"structure\")) {\n findings.push(...structureChecks(page, config));\n }\n if (isDimensionEnabled(config, \"readability\")) {\n const r = readabilityCheck(page, config);\n findings.push(...r.findings);\n readability = r.summary;\n }\n\n const kept = findings.filter((f) => !ignore.has(f.rule));\n const counts = { error: 0, warning: 0, info: 0 };\n for (const f of kept) {\n if (f.severity === \"error\") counts.error++;\n else if (f.severity === \"warning\") counts.warning++;\n else if (f.severity === \"info\") counts.info++;\n }\n const score = scorePage(kept);\n\n return {\n path: page.relPath,\n title: pageTitle(page).value || null,\n score,\n grade: gradeFor(score),\n counts,\n findings: kept,\n readability,\n };\n });\n\n pageReports.sort((a, b) => a.path.localeCompare(b.path));\n\n // Summary.\n const errors = pageReports.reduce((s, p) => s + p.counts.error, 0);\n const warnings = pageReports.reduce((s, p) => s + p.counts.warning, 0);\n const infos = pageReports.reduce((s, p) => s + p.counts.info, 0);\n const score = pageReports.length\n ? Math.round(pageReports.reduce((s, p) => s + p.score, 0) / pageReports.length)\n : 100;\n\n const byDimension = Object.fromEntries(\n DIMENSIONS.map((d) => [d, { errors: 0, warnings: 0, infos: 0 }]),\n ) as Record<Dimension, { errors: number; warnings: number; infos: number }>;\n for (const p of pageReports) {\n for (const f of p.findings) {\n const bucket = byDimension[f.dimension];\n if (f.severity === \"error\") bucket.errors++;\n else if (f.severity === \"warning\") bucket.warnings++;\n else if (f.severity === \"info\") bucket.infos++;\n }\n }\n\n return {\n tool: \"docsanity\",\n version: meta.version,\n generatedAt: meta.generatedAt,\n root,\n summary: { pages: pageReports.length, score, grade: gradeFor(score), errors, warnings, infos, byDimension },\n pages: pageReports,\n };\n}\n"]}
|