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.
@@ -0,0 +1,226 @@
1
+ /** Core types for docsanity. */
2
+ type Severity = "error" | "warning" | "info" | "pass";
3
+ /** The four health dimensions docsanity unifies into one report. */
4
+ type Dimension = "links" | "seo" | "readability" | "structure";
5
+ declare const DIMENSIONS: Dimension[];
6
+ declare const DIMENSION_LABELS: Record<Dimension, string>;
7
+ interface Finding {
8
+ dimension: Dimension;
9
+ /** Stable id, e.g. "seo.missing-description". */
10
+ rule: string;
11
+ severity: Severity;
12
+ message: string;
13
+ /** 1-based line number, when known. */
14
+ line?: number;
15
+ detail?: string;
16
+ fix?: string;
17
+ }
18
+ interface ReadabilitySummary {
19
+ grade: number;
20
+ gradeLabel: string;
21
+ ease: string;
22
+ words: number;
23
+ readingTimeSeconds: number;
24
+ }
25
+ interface PageReport {
26
+ /** Path relative to the scanned root. */
27
+ path: string;
28
+ title: string | null;
29
+ score: number;
30
+ grade: string;
31
+ counts: {
32
+ error: number;
33
+ warning: number;
34
+ info: number;
35
+ };
36
+ findings: Finding[];
37
+ readability?: ReadabilitySummary;
38
+ }
39
+ interface DocsReport {
40
+ tool: "docsanity";
41
+ version: string;
42
+ generatedAt: string;
43
+ root: string;
44
+ summary: {
45
+ pages: number;
46
+ score: number;
47
+ grade: string;
48
+ errors: number;
49
+ warnings: number;
50
+ infos: number;
51
+ byDimension: Record<Dimension, {
52
+ errors: number;
53
+ warnings: number;
54
+ infos: number;
55
+ }>;
56
+ };
57
+ pages: PageReport[];
58
+ }
59
+ interface Config {
60
+ /** File extensions to scan. */
61
+ extensions: string[];
62
+ /** Frontmatter keys every page must have (non-empty). */
63
+ requireFrontmatter: string[];
64
+ /** Recommended meta-description length window. */
65
+ descriptionMin: number;
66
+ descriptionMax: number;
67
+ /** Warn when a title is longer than this (characters). */
68
+ titleMax: number;
69
+ /** Warn when a page's reading grade exceeds this. */
70
+ maxGrade: number;
71
+ /** Minimum word count before readability is scored. */
72
+ minWordsForReadability: number;
73
+ /** Dimensions to skip entirely. */
74
+ disable: Dimension[];
75
+ /** Rule ids to ignore (e.g. "structure.code-language"). */
76
+ ignore: string[];
77
+ /** CI gate: overall score floor. */
78
+ minScore: number;
79
+ }
80
+
81
+ /**
82
+ * The orchestrator: parse every page once, run all four dimensions over the
83
+ * shared page set (so site-wide checks like duplicate titles and orphan pages
84
+ * work), and fold everything into one unified report with a combined score.
85
+ */
86
+
87
+ interface InputDoc {
88
+ absPath: string;
89
+ relPath: string;
90
+ content: string;
91
+ }
92
+ interface AnalyzeMeta {
93
+ version: string;
94
+ generatedAt: string;
95
+ }
96
+ /** Analyze a set of documentation pages into a unified {@link DocsReport}. */
97
+ declare function analyzeDocs(root: string, inputs: InputDoc[], config: Config, meta: AnalyzeMeta): DocsReport;
98
+
99
+ /**
100
+ * A small, dependency-free YAML-frontmatter parser. Docs sites (Docusaurus,
101
+ * Astro, Nextra, Hugo, Jekyll, …) put metadata in a `---` fenced block at the
102
+ * top of each Markdown file. We only need scalars and simple lists, so we parse
103
+ * those directly rather than pulling in a full YAML engine.
104
+ */
105
+ type FrontmatterValue = string | string[] | boolean | number;
106
+ interface Frontmatter {
107
+ data: Record<string, FrontmatterValue>;
108
+ /** The document body with the frontmatter block removed. */
109
+ body: string;
110
+ /** Number of lines the frontmatter occupied (so body line numbers can be offset). */
111
+ offset: number;
112
+ present: boolean;
113
+ }
114
+ /** Parse a leading `---` frontmatter block. Returns empty data when absent. */
115
+ declare function parseFrontmatter(content: string): Frontmatter;
116
+ /** Get a frontmatter value as a trimmed string (lists join with ", "). */
117
+ declare function asString(value: FrontmatterValue | undefined): string;
118
+
119
+ /**
120
+ * Lightweight Markdown extraction — headings, images, code fences and a plain
121
+ * text projection — without a full Markdown parser. We track fenced code blocks
122
+ * so `#` inside a code sample is never mistaken for a heading.
123
+ */
124
+ interface Heading {
125
+ level: number;
126
+ text: string;
127
+ line: number;
128
+ }
129
+ interface MdImage {
130
+ alt: string;
131
+ src: string;
132
+ line: number;
133
+ }
134
+ interface CodeFence {
135
+ lang: string | null;
136
+ line: number;
137
+ }
138
+ interface MarkdownModel {
139
+ headings: Heading[];
140
+ images: MdImage[];
141
+ fences: CodeFence[];
142
+ /** Plain-text projection suitable for readability scoring. */
143
+ text: string;
144
+ }
145
+ declare function parseMarkdown(body: string, lineOffset?: number): MarkdownModel;
146
+
147
+ /** A parsed documentation page: frontmatter + extracted Markdown model. */
148
+
149
+ interface Page {
150
+ /** Absolute path (needed for cross-file link resolution). */
151
+ absPath: string;
152
+ /** Path relative to the scanned root (for display). */
153
+ relPath: string;
154
+ content: string;
155
+ frontmatter: Frontmatter;
156
+ md: MarkdownModel;
157
+ }
158
+ declare function parsePage(absPath: string, relPath: string, content: string): Page;
159
+
160
+ /**
161
+ * SEO & frontmatter checks. Beyond per-page frontmatter completeness, this
162
+ * includes the **site-wide** checks no single-file linter can do: duplicate
163
+ * titles and duplicate meta descriptions across the whole docs set (both hurt
164
+ * search ranking). That cross-page view is the point of a suite.
165
+ */
166
+
167
+ interface DuplicateIndex {
168
+ titles: Map<string, string[]>;
169
+ descriptions: Map<string, string[]>;
170
+ }
171
+ /** Resolve a page's effective title: explicit frontmatter, else the first H1. */
172
+ declare function pageTitle(page: Page): {
173
+ value: string;
174
+ source: "frontmatter" | "h1" | null;
175
+ };
176
+ /** Build the cross-page duplicate index (value → list of page paths). */
177
+ declare function buildDuplicateIndex(pages: Page[]): DuplicateIndex;
178
+ declare function seoChecks(page: Page, config: Config, dup: DuplicateIndex): Finding[];
179
+
180
+ /**
181
+ * Structure & accessibility checks on Markdown — the docs-relevant subset of an
182
+ * HTML a11y linter: a single H1, no skipped heading levels, images with alt
183
+ * text, and fenced code blocks that declare a language (for correct syntax
184
+ * highlighting and screen-reader hints).
185
+ */
186
+
187
+ declare function structureChecks(page: Page, _config: Config): Finding[];
188
+
189
+ /**
190
+ * Readability check — leverages the `readlevel` engine on each page's prose and
191
+ * flags pages that read harder than the configured grade ceiling. Docs that are
192
+ * too dense lose readers (and AI answer engines that prefer clear sources).
193
+ */
194
+
195
+ interface ReadabilityResult {
196
+ findings: Finding[];
197
+ summary?: ReadabilitySummary;
198
+ }
199
+ declare function readabilityCheck(page: Page, config: Config): ReadabilityResult;
200
+
201
+ /**
202
+ * Links & references — leverages the `linklint` engine, which already builds a
203
+ * cross-file project graph and finds broken relative links, dead `#anchors`,
204
+ * missing images, undefined references and **orphan pages** (nothing links to
205
+ * them). docsanity runs it once over the whole set and folds its findings into
206
+ * the unified per-page report, keyed by each page's root-relative path.
207
+ */
208
+
209
+ /** Run linklint over all pages; return findings keyed by root-relative path. */
210
+ declare function linkFindings(root: string, pages: Page[]): Map<string, Finding[]>;
211
+
212
+ /** Weighted scoring: turn findings into a 0–100 score and an A–F grade. */
213
+
214
+ /** Page score: 100 minus capped, weighted penalties for its findings. */
215
+ declare function scorePage(findings: Finding[]): number;
216
+ declare function gradeFor(score: number): string;
217
+
218
+ /** Configuration defaults, parsing, and resolution (pure — no file I/O). */
219
+
220
+ declare const DEFAULT_CONFIG: Config;
221
+ declare const CONFIG_FILENAMES: string[];
222
+ declare function mergeConfig(base: Config, override: Partial<Config>): Config;
223
+ declare function parseConfig(json: string, label?: string): Config;
224
+ declare function isDimensionEnabled(config: Config, dim: Dimension): boolean;
225
+
226
+ export { type AnalyzeMeta, CONFIG_FILENAMES, type Config, DEFAULT_CONFIG, DIMENSIONS, DIMENSION_LABELS, type Dimension, type DocsReport, type Finding, type Frontmatter, type Heading, type InputDoc, type MarkdownModel, type Page, type PageReport, type ReadabilitySummary, type Severity, analyzeDocs, asString, buildDuplicateIndex, gradeFor, isDimensionEnabled, linkFindings, mergeConfig, pageTitle, parseConfig, parseFrontmatter, parseMarkdown, parsePage, readabilityCheck, scorePage, seoChecks, structureChecks };
@@ -0,0 +1,226 @@
1
+ /** Core types for docsanity. */
2
+ type Severity = "error" | "warning" | "info" | "pass";
3
+ /** The four health dimensions docsanity unifies into one report. */
4
+ type Dimension = "links" | "seo" | "readability" | "structure";
5
+ declare const DIMENSIONS: Dimension[];
6
+ declare const DIMENSION_LABELS: Record<Dimension, string>;
7
+ interface Finding {
8
+ dimension: Dimension;
9
+ /** Stable id, e.g. "seo.missing-description". */
10
+ rule: string;
11
+ severity: Severity;
12
+ message: string;
13
+ /** 1-based line number, when known. */
14
+ line?: number;
15
+ detail?: string;
16
+ fix?: string;
17
+ }
18
+ interface ReadabilitySummary {
19
+ grade: number;
20
+ gradeLabel: string;
21
+ ease: string;
22
+ words: number;
23
+ readingTimeSeconds: number;
24
+ }
25
+ interface PageReport {
26
+ /** Path relative to the scanned root. */
27
+ path: string;
28
+ title: string | null;
29
+ score: number;
30
+ grade: string;
31
+ counts: {
32
+ error: number;
33
+ warning: number;
34
+ info: number;
35
+ };
36
+ findings: Finding[];
37
+ readability?: ReadabilitySummary;
38
+ }
39
+ interface DocsReport {
40
+ tool: "docsanity";
41
+ version: string;
42
+ generatedAt: string;
43
+ root: string;
44
+ summary: {
45
+ pages: number;
46
+ score: number;
47
+ grade: string;
48
+ errors: number;
49
+ warnings: number;
50
+ infos: number;
51
+ byDimension: Record<Dimension, {
52
+ errors: number;
53
+ warnings: number;
54
+ infos: number;
55
+ }>;
56
+ };
57
+ pages: PageReport[];
58
+ }
59
+ interface Config {
60
+ /** File extensions to scan. */
61
+ extensions: string[];
62
+ /** Frontmatter keys every page must have (non-empty). */
63
+ requireFrontmatter: string[];
64
+ /** Recommended meta-description length window. */
65
+ descriptionMin: number;
66
+ descriptionMax: number;
67
+ /** Warn when a title is longer than this (characters). */
68
+ titleMax: number;
69
+ /** Warn when a page's reading grade exceeds this. */
70
+ maxGrade: number;
71
+ /** Minimum word count before readability is scored. */
72
+ minWordsForReadability: number;
73
+ /** Dimensions to skip entirely. */
74
+ disable: Dimension[];
75
+ /** Rule ids to ignore (e.g. "structure.code-language"). */
76
+ ignore: string[];
77
+ /** CI gate: overall score floor. */
78
+ minScore: number;
79
+ }
80
+
81
+ /**
82
+ * The orchestrator: parse every page once, run all four dimensions over the
83
+ * shared page set (so site-wide checks like duplicate titles and orphan pages
84
+ * work), and fold everything into one unified report with a combined score.
85
+ */
86
+
87
+ interface InputDoc {
88
+ absPath: string;
89
+ relPath: string;
90
+ content: string;
91
+ }
92
+ interface AnalyzeMeta {
93
+ version: string;
94
+ generatedAt: string;
95
+ }
96
+ /** Analyze a set of documentation pages into a unified {@link DocsReport}. */
97
+ declare function analyzeDocs(root: string, inputs: InputDoc[], config: Config, meta: AnalyzeMeta): DocsReport;
98
+
99
+ /**
100
+ * A small, dependency-free YAML-frontmatter parser. Docs sites (Docusaurus,
101
+ * Astro, Nextra, Hugo, Jekyll, …) put metadata in a `---` fenced block at the
102
+ * top of each Markdown file. We only need scalars and simple lists, so we parse
103
+ * those directly rather than pulling in a full YAML engine.
104
+ */
105
+ type FrontmatterValue = string | string[] | boolean | number;
106
+ interface Frontmatter {
107
+ data: Record<string, FrontmatterValue>;
108
+ /** The document body with the frontmatter block removed. */
109
+ body: string;
110
+ /** Number of lines the frontmatter occupied (so body line numbers can be offset). */
111
+ offset: number;
112
+ present: boolean;
113
+ }
114
+ /** Parse a leading `---` frontmatter block. Returns empty data when absent. */
115
+ declare function parseFrontmatter(content: string): Frontmatter;
116
+ /** Get a frontmatter value as a trimmed string (lists join with ", "). */
117
+ declare function asString(value: FrontmatterValue | undefined): string;
118
+
119
+ /**
120
+ * Lightweight Markdown extraction — headings, images, code fences and a plain
121
+ * text projection — without a full Markdown parser. We track fenced code blocks
122
+ * so `#` inside a code sample is never mistaken for a heading.
123
+ */
124
+ interface Heading {
125
+ level: number;
126
+ text: string;
127
+ line: number;
128
+ }
129
+ interface MdImage {
130
+ alt: string;
131
+ src: string;
132
+ line: number;
133
+ }
134
+ interface CodeFence {
135
+ lang: string | null;
136
+ line: number;
137
+ }
138
+ interface MarkdownModel {
139
+ headings: Heading[];
140
+ images: MdImage[];
141
+ fences: CodeFence[];
142
+ /** Plain-text projection suitable for readability scoring. */
143
+ text: string;
144
+ }
145
+ declare function parseMarkdown(body: string, lineOffset?: number): MarkdownModel;
146
+
147
+ /** A parsed documentation page: frontmatter + extracted Markdown model. */
148
+
149
+ interface Page {
150
+ /** Absolute path (needed for cross-file link resolution). */
151
+ absPath: string;
152
+ /** Path relative to the scanned root (for display). */
153
+ relPath: string;
154
+ content: string;
155
+ frontmatter: Frontmatter;
156
+ md: MarkdownModel;
157
+ }
158
+ declare function parsePage(absPath: string, relPath: string, content: string): Page;
159
+
160
+ /**
161
+ * SEO & frontmatter checks. Beyond per-page frontmatter completeness, this
162
+ * includes the **site-wide** checks no single-file linter can do: duplicate
163
+ * titles and duplicate meta descriptions across the whole docs set (both hurt
164
+ * search ranking). That cross-page view is the point of a suite.
165
+ */
166
+
167
+ interface DuplicateIndex {
168
+ titles: Map<string, string[]>;
169
+ descriptions: Map<string, string[]>;
170
+ }
171
+ /** Resolve a page's effective title: explicit frontmatter, else the first H1. */
172
+ declare function pageTitle(page: Page): {
173
+ value: string;
174
+ source: "frontmatter" | "h1" | null;
175
+ };
176
+ /** Build the cross-page duplicate index (value → list of page paths). */
177
+ declare function buildDuplicateIndex(pages: Page[]): DuplicateIndex;
178
+ declare function seoChecks(page: Page, config: Config, dup: DuplicateIndex): Finding[];
179
+
180
+ /**
181
+ * Structure & accessibility checks on Markdown — the docs-relevant subset of an
182
+ * HTML a11y linter: a single H1, no skipped heading levels, images with alt
183
+ * text, and fenced code blocks that declare a language (for correct syntax
184
+ * highlighting and screen-reader hints).
185
+ */
186
+
187
+ declare function structureChecks(page: Page, _config: Config): Finding[];
188
+
189
+ /**
190
+ * Readability check — leverages the `readlevel` engine on each page's prose and
191
+ * flags pages that read harder than the configured grade ceiling. Docs that are
192
+ * too dense lose readers (and AI answer engines that prefer clear sources).
193
+ */
194
+
195
+ interface ReadabilityResult {
196
+ findings: Finding[];
197
+ summary?: ReadabilitySummary;
198
+ }
199
+ declare function readabilityCheck(page: Page, config: Config): ReadabilityResult;
200
+
201
+ /**
202
+ * Links & references — leverages the `linklint` engine, which already builds a
203
+ * cross-file project graph and finds broken relative links, dead `#anchors`,
204
+ * missing images, undefined references and **orphan pages** (nothing links to
205
+ * them). docsanity runs it once over the whole set and folds its findings into
206
+ * the unified per-page report, keyed by each page's root-relative path.
207
+ */
208
+
209
+ /** Run linklint over all pages; return findings keyed by root-relative path. */
210
+ declare function linkFindings(root: string, pages: Page[]): Map<string, Finding[]>;
211
+
212
+ /** Weighted scoring: turn findings into a 0–100 score and an A–F grade. */
213
+
214
+ /** Page score: 100 minus capped, weighted penalties for its findings. */
215
+ declare function scorePage(findings: Finding[]): number;
216
+ declare function gradeFor(score: number): string;
217
+
218
+ /** Configuration defaults, parsing, and resolution (pure — no file I/O). */
219
+
220
+ declare const DEFAULT_CONFIG: Config;
221
+ declare const CONFIG_FILENAMES: string[];
222
+ declare function mergeConfig(base: Config, override: Partial<Config>): Config;
223
+ declare function parseConfig(json: string, label?: string): Config;
224
+ declare function isDimensionEnabled(config: Config, dim: Dimension): boolean;
225
+
226
+ export { type AnalyzeMeta, CONFIG_FILENAMES, type Config, DEFAULT_CONFIG, DIMENSIONS, DIMENSION_LABELS, type Dimension, type DocsReport, type Finding, type Frontmatter, type Heading, type InputDoc, type MarkdownModel, type Page, type PageReport, type ReadabilitySummary, type Severity, analyzeDocs, asString, buildDuplicateIndex, gradeFor, isDimensionEnabled, linkFindings, mergeConfig, pageTitle, parseConfig, parseFrontmatter, parseMarkdown, parsePage, readabilityCheck, scorePage, seoChecks, structureChecks };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { CONFIG_FILENAMES, DEFAULT_CONFIG, DIMENSIONS, DIMENSION_LABELS, analyzeDocs, asString, buildDuplicateIndex, gradeFor, isDimensionEnabled, linkFindings, mergeConfig, pageTitle, parseConfig, parseFrontmatter, parseMarkdown, parsePage, readabilityCheck, scorePage, seoChecks, structureChecks } from './chunk-DQY4MKSJ.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "docsanity",
3
+ "version": "0.1.0",
4
+ "description": "One command for docs-site health: broken links & dead anchors, orphan pages, SEO frontmatter (with site-wide duplicate title/description detection), readability grade, and Markdown structure/alt checks — unified into one score and report. Local, deterministic, no API key.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "bin": {
17
+ "docsanity": "./dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "tsc --noEmit",
31
+ "example": "node dist/cli.js scan examples/docs",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "keywords": [
35
+ "docs",
36
+ "documentation",
37
+ "docs-site",
38
+ "markdown",
39
+ "mdx",
40
+ "docusaurus",
41
+ "astro",
42
+ "nextra",
43
+ "broken-links",
44
+ "orphan-pages",
45
+ "frontmatter",
46
+ "seo",
47
+ "readability",
48
+ "link-checker",
49
+ "docs-linter",
50
+ "static-site",
51
+ "cli"
52
+ ],
53
+ "author": "didrod205 (https://github.com/didrod205)",
54
+ "license": "MIT",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/didrod205/docsanity.git"
58
+ },
59
+ "bugs": {
60
+ "url": "https://github.com/didrod205/docsanity/issues"
61
+ },
62
+ "homepage": "https://github.com/didrod205/docsanity#readme",
63
+ "dependencies": {
64
+ "@didrod2539/linklint": "^0.1.0",
65
+ "@didrod2539/readlevel": "^0.2.0",
66
+ "cac": "^6.7.14",
67
+ "picocolors": "^1.1.1"
68
+ },
69
+ "devDependencies": {
70
+ "@types/node": "^22.10.0",
71
+ "tsup": "^8.3.5",
72
+ "typescript": "^5.7.2",
73
+ "vitest": "^2.1.8"
74
+ }
75
+ }