@marlinjai/clearify 1.10.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/node/{chunk-QIAMD2Q6.js → chunk-EKN6DWXT.js} +28 -3
- package/dist/node/{chunk-ZNURJXMI.js → chunk-JU53JE4P.js} +26 -7
- package/dist/node/chunk-NGWAMBXS.js +264 -0
- package/dist/node/{chunk-2RAA6477.js → chunk-PFX7LHRZ.js} +17 -8
- package/dist/node/{chunk-QNC5QRIO.js → chunk-QTGT76RY.js} +35 -14
- package/dist/node/cli/index.js +19 -3
- package/dist/node/core/navigation.d.ts +1 -1
- package/dist/node/core/navigation.js +1 -1
- package/dist/node/core/search.d.ts +6 -2
- package/dist/node/node/build.js +3 -3
- package/dist/node/node/check.d.ts +9 -2
- package/dist/node/node/check.js +4 -2
- package/dist/node/node/index.d.ts +1 -1
- package/dist/node/node/index.js +26 -8
- package/dist/node/node/init.js +1 -1
- package/dist/node/types/index.d.ts +8 -1
- package/dist/node/vite-plugin/index.js +2 -2
- package/package.json +10 -2
- package/src/theme/Sidebar.tsx +13 -11
- package/dist/node/chunk-ELRDUSZS.js +0 -136
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Clearify
|
|
3
|
+
summary: README for Clearify, an open-source documentation site generator that turns markdown into beautiful docs with zero config, MDX support, Mermaid diagrams, full-text search, OpenAPI rendering, and SSG with SEO.
|
|
4
|
+
category: documentation
|
|
5
|
+
tags: [clearify, documentation, mdx, vite, ssg]
|
|
6
|
+
status: active
|
|
7
|
+
date: 2026-02-07
|
|
8
|
+
---
|
|
9
|
+
|
|
1
10
|
# Clearify
|
|
2
11
|
|
|
3
12
|
An open-source documentation site generator. Turn markdown into beautiful docs.
|
|
@@ -171,6 +171,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
171
171
|
} else {
|
|
172
172
|
console.log(" CHANGELOG.md already exists, skipping");
|
|
173
173
|
}
|
|
174
|
+
const roadmapPath = resolve(cwd, "ROADMAP.md");
|
|
175
|
+
if (!existsSync(roadmapPath)) {
|
|
176
|
+
writeFileSync(
|
|
177
|
+
roadmapPath,
|
|
178
|
+
`# Roadmap
|
|
179
|
+
|
|
180
|
+
## Planned
|
|
181
|
+
|
|
182
|
+
<!-- Decided features, ready to be worked on -->
|
|
183
|
+
|
|
184
|
+
## In Progress
|
|
185
|
+
|
|
186
|
+
<!-- Currently being implemented -->
|
|
187
|
+
|
|
188
|
+
## Completed
|
|
189
|
+
|
|
190
|
+
<!-- Done \u2014 move to CHANGELOG.md on release -->
|
|
191
|
+
`
|
|
192
|
+
);
|
|
193
|
+
console.log(" Created ROADMAP.md");
|
|
194
|
+
} else {
|
|
195
|
+
console.log(" ROADMAP.md already exists, skipping");
|
|
196
|
+
}
|
|
174
197
|
if (!existsSync(configPath)) {
|
|
175
198
|
if (options.noInternal) {
|
|
176
199
|
writeFileSync(
|
|
@@ -222,11 +245,13 @@ When making changes to this project, keep the documentation in sync:
|
|
|
222
245
|
|
|
223
246
|
1. **Changelog** \u2014 After any user-facing change, add a bullet under \`## [Unreleased]\` in \`CHANGELOG.md\` using [Keep a Changelog](https://keepachangelog.com/) format (\`Added\`, \`Changed\`, \`Fixed\`, \`Removed\`).
|
|
224
247
|
|
|
225
|
-
2. **
|
|
248
|
+
2. **Roadmap** \u2014 When a planned feature is implemented, move it from \`Planned\` or \`In Progress\` to \`Completed\` in \`ROADMAP.md\`. When new work is decided, add it to \`Planned\`.
|
|
249
|
+
|
|
250
|
+
3. **Docs pages** \u2014 When a feature or behavior changes, update the relevant page in \`docs/public/\`. If a new feature has no page, create one.
|
|
226
251
|
|
|
227
|
-
|
|
252
|
+
4. **Config docs** \u2014 When config options are added or changed, update \`docs/public/configuration.md\` with the new option, type, default, and description.
|
|
228
253
|
${hasInternal ? `
|
|
229
|
-
|
|
254
|
+
5. **Architecture decisions** \u2014 For significant design decisions, add an entry to \`docs/internal/\` explaining the context, decision, and trade-offs.
|
|
230
255
|
` : ""}`;
|
|
231
256
|
writeFileSync(claudeRulesPath, rulesContent);
|
|
232
257
|
console.log(" Created .claude/rules/clearify-docs.md");
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-SCZZB7OE.js";
|
|
9
9
|
import {
|
|
10
10
|
clearifyPlugin
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PFX7LHRZ.js";
|
|
12
12
|
import {
|
|
13
13
|
loadUserConfig,
|
|
14
14
|
resolveConfig,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "./chunk-PVTUVUBK.js";
|
|
17
17
|
import {
|
|
18
18
|
buildSectionData
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-QTGT76RY.js";
|
|
20
20
|
|
|
21
21
|
// src/node/build.ts
|
|
22
22
|
import { build as viteBuild } from "vite";
|
|
@@ -27,10 +27,11 @@ import remarkFrontmatter from "remark-frontmatter";
|
|
|
27
27
|
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
28
28
|
import rehypeShiki from "@shikijs/rehype";
|
|
29
29
|
import tailwindcss from "@tailwindcss/vite";
|
|
30
|
-
import { resolve } from "path";
|
|
30
|
+
import { resolve, dirname } from "path";
|
|
31
31
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync, copyFileSync } from "fs";
|
|
32
32
|
import { basename } from "path";
|
|
33
33
|
import { fileURLToPath } from "url";
|
|
34
|
+
import { createRequire } from "module";
|
|
34
35
|
function findPackageRoot() {
|
|
35
36
|
let dir = fileURLToPath(new URL(".", import.meta.url));
|
|
36
37
|
for (let i = 0; i < 10; i++) {
|
|
@@ -45,6 +46,18 @@ function resolveClientPath(...segments) {
|
|
|
45
46
|
const packageRoot = findPackageRoot();
|
|
46
47
|
return resolve(packageRoot, "src", "client", ...segments);
|
|
47
48
|
}
|
|
49
|
+
function dedupeResolve() {
|
|
50
|
+
const require2 = createRequire(import.meta.url);
|
|
51
|
+
const dedupePkgs = ["@mdx-js/react", "react", "react-dom", "react-router-dom"];
|
|
52
|
+
const alias = {};
|
|
53
|
+
for (const pkg of dedupePkgs) {
|
|
54
|
+
try {
|
|
55
|
+
alias[pkg] = dirname(require2.resolve(`${pkg}/package.json`));
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { alias, dedupe: dedupePkgs };
|
|
60
|
+
}
|
|
48
61
|
function escapeHtml(str) {
|
|
49
62
|
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
50
63
|
}
|
|
@@ -142,9 +155,12 @@ async function buildSite() {
|
|
|
142
155
|
const ssrOutDir = resolve(outDir, ".ssr");
|
|
143
156
|
const allSections = await resolveSections(config, userRoot);
|
|
144
157
|
const productionSections = allSections.filter((s) => !s.draft);
|
|
145
|
-
const
|
|
158
|
+
const rootFiles = {
|
|
159
|
+
changelogPath: resolve(userRoot, "CHANGELOG.md"),
|
|
160
|
+
roadmapPath: resolve(userRoot, "ROADMAP.md")
|
|
161
|
+
};
|
|
146
162
|
const sectionDataList = productionSections.map(
|
|
147
|
-
(section) => buildSectionData(section,
|
|
163
|
+
(section) => buildSectionData(section, rootFiles, config.navigation)
|
|
148
164
|
);
|
|
149
165
|
const allRoutes = sectionDataList.flatMap((sd) => sd.routes);
|
|
150
166
|
if (config.openapi?.spec && config.openapi.generatePages !== false) {
|
|
@@ -207,11 +223,13 @@ async function buildSite() {
|
|
|
207
223
|
mermaidSvgs,
|
|
208
224
|
mermaidStrategy
|
|
209
225
|
};
|
|
226
|
+
const { alias: dedupeAlias, dedupe } = dedupeResolve();
|
|
210
227
|
const clientConfig = {
|
|
211
228
|
root,
|
|
212
229
|
plugins: getSharedPlugins(userRoot, sharedPluginOptions),
|
|
213
230
|
resolve: {
|
|
214
|
-
alias: { "@clearify": resolve(root, "..") }
|
|
231
|
+
alias: { "@clearify": resolve(root, ".."), ...dedupeAlias },
|
|
232
|
+
dedupe
|
|
215
233
|
},
|
|
216
234
|
build: {
|
|
217
235
|
outDir,
|
|
@@ -225,7 +243,8 @@ async function buildSite() {
|
|
|
225
243
|
root,
|
|
226
244
|
plugins: getSharedPlugins(userRoot, sharedPluginOptions),
|
|
227
245
|
resolve: {
|
|
228
|
-
alias: { "@clearify": resolve(root, "..") }
|
|
246
|
+
alias: { "@clearify": resolve(root, "..") },
|
|
247
|
+
dedupe
|
|
229
248
|
},
|
|
230
249
|
build: {
|
|
231
250
|
outDir: ssrOutDir,
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadUserConfig,
|
|
3
|
+
resolveConfig,
|
|
4
|
+
resolveSections
|
|
5
|
+
} from "./chunk-PVTUVUBK.js";
|
|
6
|
+
import {
|
|
7
|
+
scanDocs
|
|
8
|
+
} from "./chunk-QTGT76RY.js";
|
|
9
|
+
|
|
10
|
+
// src/node/check.ts
|
|
11
|
+
import { resolve, relative } from "path";
|
|
12
|
+
import { readFileSync, existsSync } from "fs";
|
|
13
|
+
import matter from "gray-matter";
|
|
14
|
+
var VALID_CATEGORIES = [
|
|
15
|
+
"documentation",
|
|
16
|
+
"internal",
|
|
17
|
+
"plan",
|
|
18
|
+
"research",
|
|
19
|
+
"decision",
|
|
20
|
+
"roadmap",
|
|
21
|
+
"changelog"
|
|
22
|
+
];
|
|
23
|
+
var VALID_STATUSES = ["active", "superseded", "archived"];
|
|
24
|
+
function isStrictPath(filePath) {
|
|
25
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
26
|
+
return /\/docs\/plans(\/|$)/.test(normalized) || /\/docs\/internal(\/|$)/.test(normalized);
|
|
27
|
+
}
|
|
28
|
+
function validateFrontmatter(doc, userRoot) {
|
|
29
|
+
const issues = [];
|
|
30
|
+
const rel = relative(userRoot, doc.filePath);
|
|
31
|
+
const strict = isStrictPath(doc.filePath);
|
|
32
|
+
let rawFm = {};
|
|
33
|
+
try {
|
|
34
|
+
const content = readFileSync(doc.filePath, "utf-8");
|
|
35
|
+
rawFm = matter(content).data;
|
|
36
|
+
} catch {
|
|
37
|
+
rawFm = {};
|
|
38
|
+
}
|
|
39
|
+
if (strict) {
|
|
40
|
+
if (!rawFm.title) {
|
|
41
|
+
issues.push({ file: rel, field: "title", message: "missing required field", severity: "error" });
|
|
42
|
+
}
|
|
43
|
+
if (!rawFm.summary) {
|
|
44
|
+
issues.push({ file: rel, field: "summary", message: "missing required field", severity: "error" });
|
|
45
|
+
}
|
|
46
|
+
if (!rawFm.category) {
|
|
47
|
+
issues.push({ file: rel, field: "category", message: "missing required field", severity: "error" });
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
if (!rawFm.title) {
|
|
51
|
+
issues.push({ file: rel, field: "title", message: "missing required field", severity: "error" });
|
|
52
|
+
}
|
|
53
|
+
if (!rawFm.summary) {
|
|
54
|
+
issues.push({ file: rel, field: "summary", message: "recommended field missing", severity: "warning" });
|
|
55
|
+
}
|
|
56
|
+
if (!rawFm.category) {
|
|
57
|
+
issues.push({ file: rel, field: "category", message: "recommended field missing", severity: "warning" });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (rawFm.category !== void 0) {
|
|
61
|
+
if (!VALID_CATEGORIES.includes(rawFm.category)) {
|
|
62
|
+
issues.push({
|
|
63
|
+
file: rel,
|
|
64
|
+
field: "category",
|
|
65
|
+
message: `invalid value "${rawFm.category}" (allowed: ${VALID_CATEGORIES.join(", ")})`,
|
|
66
|
+
severity: "error"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (rawFm.status !== void 0) {
|
|
71
|
+
if (!VALID_STATUSES.includes(rawFm.status)) {
|
|
72
|
+
issues.push({
|
|
73
|
+
file: rel,
|
|
74
|
+
field: "status",
|
|
75
|
+
message: `invalid value "${rawFm.status}" (allowed: ${VALID_STATUSES.join(", ")})`,
|
|
76
|
+
severity: "error"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (rawFm.tags !== void 0 && !Array.isArray(rawFm.tags)) {
|
|
81
|
+
issues.push({ file: rel, field: "tags", message: "must be an array", severity: "error" });
|
|
82
|
+
}
|
|
83
|
+
if (rawFm.projects !== void 0 && !Array.isArray(rawFm.projects)) {
|
|
84
|
+
issues.push({ file: rel, field: "projects", message: "must be an array", severity: "error" });
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
async function checkFrontmatter() {
|
|
89
|
+
const userRoot = process.cwd();
|
|
90
|
+
const userConfig = await loadUserConfig(userRoot);
|
|
91
|
+
const config = resolveConfig(userConfig, userRoot);
|
|
92
|
+
const sections = await resolveSections(config, userRoot);
|
|
93
|
+
const allDocs = [];
|
|
94
|
+
for (const section of sections) {
|
|
95
|
+
const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
|
|
96
|
+
allDocs.push(...docs);
|
|
97
|
+
}
|
|
98
|
+
const allIssues = [];
|
|
99
|
+
for (const doc of allDocs) {
|
|
100
|
+
const issues = validateFrontmatter(doc, userRoot);
|
|
101
|
+
allIssues.push(...issues);
|
|
102
|
+
}
|
|
103
|
+
const errors = allIssues.filter((i) => i.severity === "error");
|
|
104
|
+
const warnings = allIssues.filter((i) => i.severity === "warning");
|
|
105
|
+
if (allIssues.length === 0) {
|
|
106
|
+
console.log("\n Frontmatter: all files valid.\n");
|
|
107
|
+
return { errors: 0, warnings: 0 };
|
|
108
|
+
}
|
|
109
|
+
console.log(`
|
|
110
|
+
Frontmatter validation: ${errors.length} error(s), ${warnings.length} warning(s)
|
|
111
|
+
`);
|
|
112
|
+
const fileCol = Math.max(4, ...allIssues.map((i) => i.file.length));
|
|
113
|
+
const fieldCol = Math.max(5, ...allIssues.map((i) => i.field.length));
|
|
114
|
+
const msgCol = Math.max(7, ...allIssues.map((i) => i.message.length));
|
|
115
|
+
const sevCol = 7;
|
|
116
|
+
const header = ` ${"File".padEnd(fileCol)} ${"Field".padEnd(fieldCol)} ${"Severity".padEnd(sevCol)} ${"Message".padEnd(msgCol)}`;
|
|
117
|
+
const separator = ` ${"\u2500".repeat(fileCol)} ${"\u2500".repeat(fieldCol)} ${"\u2500".repeat(sevCol)} ${"\u2500".repeat(msgCol)}`;
|
|
118
|
+
console.log(header);
|
|
119
|
+
console.log(separator);
|
|
120
|
+
for (const issue of allIssues) {
|
|
121
|
+
const sev = issue.severity === "error" ? "ERROR " : "warning";
|
|
122
|
+
console.log(` ${issue.file.padEnd(fileCol)} ${issue.field.padEnd(fieldCol)} ${sev} ${issue.message}`);
|
|
123
|
+
}
|
|
124
|
+
console.log();
|
|
125
|
+
return { errors: errors.length, warnings: warnings.length };
|
|
126
|
+
}
|
|
127
|
+
async function checkLinks(options = {}) {
|
|
128
|
+
const fmResult = await checkFrontmatter();
|
|
129
|
+
if (options.frontmatterOnly) {
|
|
130
|
+
if (fmResult.errors > 0) {
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const userRoot = process.cwd();
|
|
136
|
+
const userConfig = await loadUserConfig(userRoot);
|
|
137
|
+
const config = resolveConfig(userConfig, userRoot);
|
|
138
|
+
const sections = await resolveSections(config, userRoot);
|
|
139
|
+
const validRoutes = /* @__PURE__ */ new Set();
|
|
140
|
+
const allDocs = [];
|
|
141
|
+
for (const section of sections) {
|
|
142
|
+
const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
|
|
143
|
+
for (const doc of docs) {
|
|
144
|
+
validRoutes.add(doc.routePath);
|
|
145
|
+
allDocs.push(doc);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
validRoutes.add("/");
|
|
149
|
+
const changelogPath = resolve(userRoot, "CHANGELOG.md");
|
|
150
|
+
if (existsSync(changelogPath)) {
|
|
151
|
+
validRoutes.add("/changelog");
|
|
152
|
+
}
|
|
153
|
+
const roadmapPath = resolve(userRoot, "ROADMAP.md");
|
|
154
|
+
if (existsSync(roadmapPath)) {
|
|
155
|
+
validRoutes.add("/roadmap");
|
|
156
|
+
}
|
|
157
|
+
const expandedRoutes = new Set(validRoutes);
|
|
158
|
+
for (const route of validRoutes) {
|
|
159
|
+
expandedRoutes.add(route.replace(/\/$/, ""));
|
|
160
|
+
if (route !== "/") expandedRoutes.add(route + "/");
|
|
161
|
+
}
|
|
162
|
+
const mdLinkRegex = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
|
|
163
|
+
const hrefRegex = /href=["']([^"']+)["']/g;
|
|
164
|
+
const brokenLinks = [];
|
|
165
|
+
for (const doc of allDocs) {
|
|
166
|
+
if (!existsSync(doc.filePath)) continue;
|
|
167
|
+
const content = readFileSync(doc.filePath, "utf-8");
|
|
168
|
+
const lines = content.split("\n");
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
const lineNum = i + 1;
|
|
172
|
+
if (line.trimStart().startsWith("```")) continue;
|
|
173
|
+
for (const regex of [mdLinkRegex, hrefRegex]) {
|
|
174
|
+
regex.lastIndex = 0;
|
|
175
|
+
let match;
|
|
176
|
+
while ((match = regex.exec(line)) !== null) {
|
|
177
|
+
const rawLink = match[1].split(/[\s"'#]/)[0];
|
|
178
|
+
if (!rawLink || rawLink.startsWith("http://") || rawLink.startsWith("https://") || rawLink.startsWith("#") || rawLink.startsWith("mailto:") || rawLink.startsWith("tel:") || rawLink.startsWith("//") || rawLink.startsWith("{") || rawLink.startsWith("data:")) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (/\.(png|jpe?g|gif|svg|webp|ico|pdf|zip|tar|gz)$/i.test(rawLink)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
let normalized = rawLink;
|
|
185
|
+
normalized = normalized.replace(/\.(mdx?|html?)$/, "");
|
|
186
|
+
if (!normalized.startsWith("/")) {
|
|
187
|
+
const docDir = doc.routePath.substring(0, doc.routePath.lastIndexOf("/")) || "/";
|
|
188
|
+
normalized = docDir === "/" ? "/" + normalized : docDir + "/" + normalized;
|
|
189
|
+
}
|
|
190
|
+
const check = normalized === "/" ? "/" : normalized.replace(/\/$/, "");
|
|
191
|
+
const checkNoIndex = check.replace(/\/index$/, "") || "/";
|
|
192
|
+
if (!expandedRoutes.has(check) && !expandedRoutes.has(checkNoIndex)) {
|
|
193
|
+
const suggestion = findClosest(check, validRoutes);
|
|
194
|
+
brokenLinks.push({
|
|
195
|
+
file: doc.filePath,
|
|
196
|
+
line: lineNum,
|
|
197
|
+
link: rawLink,
|
|
198
|
+
...suggestion ? { suggestion } : {}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (brokenLinks.length === 0) {
|
|
206
|
+
console.log("\n No broken links found.\n");
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`
|
|
209
|
+
Found ${brokenLinks.length} broken link${brokenLinks.length === 1 ? "" : "s"}:
|
|
210
|
+
`);
|
|
211
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const bl of brokenLinks) {
|
|
213
|
+
const rel = bl.file.replace(userRoot + "/", "");
|
|
214
|
+
if (!byFile.has(rel)) byFile.set(rel, []);
|
|
215
|
+
byFile.get(rel).push(bl);
|
|
216
|
+
}
|
|
217
|
+
for (const [file, links] of byFile) {
|
|
218
|
+
console.log(` ${file}`);
|
|
219
|
+
for (const link of links) {
|
|
220
|
+
const suggestion = link.suggestion ? ` (did you mean ${link.suggestion}?)` : "";
|
|
221
|
+
console.log(` line ${link.line}: ${link.link}${suggestion}`);
|
|
222
|
+
}
|
|
223
|
+
console.log();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (brokenLinks.length > 0 || fmResult.errors > 0) {
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function findClosest(target, routes) {
|
|
231
|
+
let best;
|
|
232
|
+
let bestScore = Infinity;
|
|
233
|
+
const targetSegments = target.split("/").filter(Boolean);
|
|
234
|
+
for (const route of routes) {
|
|
235
|
+
const routeSegments = route.split("/").filter(Boolean);
|
|
236
|
+
const dist = levenshtein(target, route);
|
|
237
|
+
const common = targetSegments.filter((s) => routeSegments.includes(s)).length;
|
|
238
|
+
const score = dist - common * 2;
|
|
239
|
+
if (score < bestScore) {
|
|
240
|
+
bestScore = score;
|
|
241
|
+
best = route;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (best && bestScore < target.length * 0.6) return best;
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
function levenshtein(a, b) {
|
|
248
|
+
const m = a.length;
|
|
249
|
+
const n = b.length;
|
|
250
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
251
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
252
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
253
|
+
for (let i = 1; i <= m; i++) {
|
|
254
|
+
for (let j = 1; j <= n; j++) {
|
|
255
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return dp[m][n];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
checkFrontmatter,
|
|
263
|
+
checkLinks
|
|
264
|
+
};
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./chunk-PVTUVUBK.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSectionData
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-QTGT76RY.js";
|
|
13
13
|
|
|
14
14
|
// src/vite-plugin/index.ts
|
|
15
15
|
import { resolve } from "path";
|
|
@@ -49,10 +49,16 @@ function clearifyPlugin(options = {}) {
|
|
|
49
49
|
let allSectionNavigations;
|
|
50
50
|
let allSearchEntries;
|
|
51
51
|
let mermaidSvgData = options.mermaidSvgs ?? {};
|
|
52
|
+
function getRootFiles() {
|
|
53
|
+
return {
|
|
54
|
+
changelogPath: resolve(userRoot, "CHANGELOG.md"),
|
|
55
|
+
roadmapPath: resolve(userRoot, "ROADMAP.md")
|
|
56
|
+
};
|
|
57
|
+
}
|
|
52
58
|
async function refreshDocs() {
|
|
53
|
-
const
|
|
59
|
+
const rootFiles = getRootFiles();
|
|
54
60
|
sectionDataList = visibleSections.map(
|
|
55
|
-
(section) => buildSectionData(section,
|
|
61
|
+
(section) => buildSectionData(section, rootFiles, config.navigation)
|
|
56
62
|
);
|
|
57
63
|
allRoutes = sectionDataList.flatMap((sd) => sd.routes);
|
|
58
64
|
allSectionNavigations = sectionDataList.map((sd) => sd.navigation);
|
|
@@ -266,9 +272,11 @@ ${routeEntries.join(",\n")}
|
|
|
266
272
|
for (const section of visibleSections) {
|
|
267
273
|
server.watcher.add(section.docsDir);
|
|
268
274
|
}
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
const rootFiles = getRootFiles();
|
|
276
|
+
for (const rootFile of [rootFiles.changelogPath, rootFiles.roadmapPath]) {
|
|
277
|
+
if (rootFile && existsSync(rootFile)) {
|
|
278
|
+
server.watcher.add(rootFile);
|
|
279
|
+
}
|
|
272
280
|
}
|
|
273
281
|
if (config.openapi?.spec) {
|
|
274
282
|
const openapiPath = resolve(userRoot, config.openapi.spec);
|
|
@@ -385,9 +393,10 @@ ${routeEntries.join(",\n")}
|
|
|
385
393
|
});
|
|
386
394
|
}
|
|
387
395
|
server.watcher.on("all", (event, path) => {
|
|
388
|
-
const isChangelog = path === changelogPath;
|
|
396
|
+
const isChangelog = path === rootFiles.changelogPath;
|
|
397
|
+
const isRoadmap = path === rootFiles.roadmapPath;
|
|
389
398
|
const isInSection = visibleSections.some((s) => path.startsWith(s.docsDir));
|
|
390
|
-
if (!isChangelog && !isInSection) return;
|
|
399
|
+
if (!isChangelog && !isRoadmap && !isInSection) return;
|
|
391
400
|
if (!path.endsWith(".md") && !path.endsWith(".mdx")) return;
|
|
392
401
|
if (event === "add" || event === "unlink" || event === "change") {
|
|
393
402
|
refreshDocs().then(() => {
|
|
@@ -40,7 +40,13 @@ function scanDocs(docsDir, exclude = [], basePath = "/") {
|
|
|
40
40
|
title: data.title ?? toTitleCase(basename(filePath, extname(filePath))),
|
|
41
41
|
description: data.description,
|
|
42
42
|
icon: data.icon,
|
|
43
|
-
order: data.order
|
|
43
|
+
order: data.order,
|
|
44
|
+
summary: data.summary,
|
|
45
|
+
category: data.category,
|
|
46
|
+
tags: data.tags,
|
|
47
|
+
projects: data.projects,
|
|
48
|
+
status: data.status,
|
|
49
|
+
date: data.date
|
|
44
50
|
}
|
|
45
51
|
};
|
|
46
52
|
});
|
|
@@ -127,20 +133,35 @@ function buildRoutes(docs, sectionId) {
|
|
|
127
133
|
...sectionId ? { sectionId } : {}
|
|
128
134
|
}));
|
|
129
135
|
}
|
|
130
|
-
function buildSectionData(section,
|
|
136
|
+
function buildSectionData(section, rootFiles, customNavigation) {
|
|
131
137
|
const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
|
|
132
|
-
if (section.basePath === "/"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
if (section.basePath === "/") {
|
|
139
|
+
if (rootFiles?.changelogPath && existsSync(rootFiles.changelogPath)) {
|
|
140
|
+
const content = readFileSync(rootFiles.changelogPath, "utf-8");
|
|
141
|
+
const { data } = matter(content);
|
|
142
|
+
docs.push({
|
|
143
|
+
filePath: rootFiles.changelogPath,
|
|
144
|
+
routePath: "/changelog",
|
|
145
|
+
frontmatter: {
|
|
146
|
+
title: data.title ?? "Changelog",
|
|
147
|
+
description: data.description ?? "Release history",
|
|
148
|
+
order: 9999
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (rootFiles?.roadmapPath && existsSync(rootFiles.roadmapPath)) {
|
|
153
|
+
const content = readFileSync(rootFiles.roadmapPath, "utf-8");
|
|
154
|
+
const { data } = matter(content);
|
|
155
|
+
docs.push({
|
|
156
|
+
filePath: rootFiles.roadmapPath,
|
|
157
|
+
routePath: "/roadmap",
|
|
158
|
+
frontmatter: {
|
|
159
|
+
title: data.title ?? "Roadmap",
|
|
160
|
+
description: data.description ?? "Planned and upcoming features",
|
|
161
|
+
order: 9998
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
144
165
|
}
|
|
145
166
|
const nav = section.basePath === "/" && customNavigation ? customNavigation : buildNavigation(docs, section.basePath);
|
|
146
167
|
const navigation = {
|
package/dist/node/cli/index.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// src/cli/index.ts
|
|
2
2
|
import cac from "cac";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { readFileSync, existsSync } from "fs";
|
|
6
|
+
function findPackageVersion() {
|
|
7
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
for (let i = 0; i < 5; i++) {
|
|
9
|
+
const pkgPath = join(dir, "package.json");
|
|
10
|
+
if (existsSync(pkgPath)) {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
12
|
+
if (pkg.name === "clearify" || pkg.name === "@marlinjai/clearify") return pkg.version;
|
|
13
|
+
}
|
|
14
|
+
dir = dirname(dir);
|
|
15
|
+
}
|
|
16
|
+
return "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
var version = findPackageVersion();
|
|
3
19
|
var cli = cac("clearify");
|
|
4
20
|
cli.command("dev", "Start development server").option("--port <port>", "Port to listen on").option("--host", "Expose to network").action(async (options) => {
|
|
5
21
|
const { createServer } = await import("../node/index.js");
|
|
@@ -22,9 +38,9 @@ cli.command("init", "Scaffold a docs folder").option("--no-internal", "Skip crea
|
|
|
22
38
|
const { init } = await import("../node/index.js");
|
|
23
39
|
await init({ noInternal: options.noInternal, noClaudeRules: options.noClaudeRules });
|
|
24
40
|
});
|
|
25
|
-
cli.command("check", "Check for broken internal links").action(async () => {
|
|
41
|
+
cli.command("check", "Check for broken internal links and frontmatter issues").option("--frontmatter", "Only run frontmatter validation (skip broken link detection)").action(async (options) => {
|
|
26
42
|
const { checkLinks } = await import("../node/check.js");
|
|
27
|
-
await checkLinks();
|
|
43
|
+
await checkLinks({ frontmatterOnly: options.frontmatter });
|
|
28
44
|
});
|
|
29
45
|
cli.command("openapi:generate", "Generate OpenAPI spec from NestJS app").option("--module <path>", "Path to NestJS app module", { default: "./src/app.module.ts" }).option("--output <path>", "Output path for spec file", { default: "./docs/openapi.json" }).option("--title <title>", "API documentation title").option("--description <desc>", "API documentation description").option("--version <version>", "API version").action(async (options) => {
|
|
30
46
|
const { generateSpec } = await import("../presets/nestjs.js");
|
|
@@ -37,5 +53,5 @@ cli.command("openapi:generate", "Generate OpenAPI spec from NestJS app").option(
|
|
|
37
53
|
});
|
|
38
54
|
});
|
|
39
55
|
cli.help();
|
|
40
|
-
cli.version(
|
|
56
|
+
cli.version(version);
|
|
41
57
|
cli.parse();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '../types/index.js';
|
|
2
|
-
export { D as DocFile, S as SectionData, b as buildNavigation, a as buildRoutes, c as buildSectionData, s as scanDocs } from './search.js';
|
|
2
|
+
export { D as DocFile, R as RootFileOptions, S as SectionData, b as buildNavigation, a as buildRoutes, c as buildSectionData, s as scanDocs } from './search.js';
|
|
@@ -26,6 +26,10 @@ interface SectionData {
|
|
|
26
26
|
routes: RouteEntry[];
|
|
27
27
|
searchEntries: SearchEntry[];
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
interface RootFileOptions {
|
|
30
|
+
changelogPath?: string;
|
|
31
|
+
roadmapPath?: string;
|
|
32
|
+
}
|
|
33
|
+
declare function buildSectionData(section: ResolvedSection, rootFiles?: RootFileOptions, customNavigation?: NavigationItem[] | null): SectionData;
|
|
30
34
|
|
|
31
|
-
export { type DocFile as D, type SectionData as S, type SearchEntry, buildRoutes as a, buildNavigation as b, buildSearchIndex, buildSectionData as c, scanDocs as s };
|
|
35
|
+
export { type DocFile as D, type RootFileOptions as R, type SectionData as S, type SearchEntry, buildRoutes as a, buildNavigation as b, buildSearchIndex, buildSectionData as c, scanDocs as s };
|
package/dist/node/node/build.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-JU53JE4P.js";
|
|
4
4
|
import "../chunk-PRTER35L.js";
|
|
5
5
|
import "../chunk-SCZZB7OE.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-PFX7LHRZ.js";
|
|
7
7
|
import "../chunk-B2Q23JW3.js";
|
|
8
8
|
import "../chunk-PVTUVUBK.js";
|
|
9
9
|
import "../chunk-AJBCHKBP.js";
|
|
10
10
|
import "../chunk-V7LLYIRO.js";
|
|
11
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-QTGT76RY.js";
|
|
12
12
|
import "../chunk-5TD7NQIW.js";
|
|
13
13
|
export {
|
|
14
14
|
buildSite
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
interface CheckOptions {
|
|
2
|
+
frontmatterOnly?: boolean;
|
|
3
|
+
}
|
|
4
|
+
declare function checkFrontmatter(): Promise<{
|
|
5
|
+
errors: number;
|
|
6
|
+
warnings: number;
|
|
7
|
+
}>;
|
|
8
|
+
declare function checkLinks(options?: CheckOptions): Promise<void>;
|
|
2
9
|
|
|
3
|
-
export { checkLinks };
|
|
10
|
+
export { type CheckOptions, checkFrontmatter, checkLinks };
|
package/dist/node/node/check.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
checkFrontmatter,
|
|
2
3
|
checkLinks
|
|
3
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-NGWAMBXS.js";
|
|
4
5
|
import "../chunk-PVTUVUBK.js";
|
|
5
6
|
import "../chunk-AJBCHKBP.js";
|
|
6
7
|
import "../chunk-V7LLYIRO.js";
|
|
7
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-QTGT76RY.js";
|
|
8
9
|
import "../chunk-5TD7NQIW.js";
|
|
9
10
|
export {
|
|
11
|
+
checkFrontmatter,
|
|
10
12
|
checkLinks
|
|
11
13
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as vite from 'vite';
|
|
2
2
|
export { buildSite as build } from './build.js';
|
|
3
3
|
export { init } from './init.js';
|
|
4
|
-
export { checkLinks } from './check.js';
|
|
4
|
+
export { checkFrontmatter, checkLinks } from './check.js';
|
|
5
5
|
|
|
6
6
|
declare function createServer(options?: {
|
|
7
7
|
port?: number;
|
package/dist/node/node/index.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-JU53JE4P.js";
|
|
4
4
|
import {
|
|
5
5
|
remarkMermaidToComponent
|
|
6
6
|
} from "../chunk-PRTER35L.js";
|
|
7
7
|
import "../chunk-SCZZB7OE.js";
|
|
8
8
|
import {
|
|
9
9
|
clearifyPlugin
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-PFX7LHRZ.js";
|
|
11
11
|
import "../chunk-B2Q23JW3.js";
|
|
12
12
|
import {
|
|
13
13
|
init
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-EKN6DWXT.js";
|
|
15
15
|
import {
|
|
16
|
+
checkFrontmatter,
|
|
16
17
|
checkLinks
|
|
17
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-NGWAMBXS.js";
|
|
18
19
|
import {
|
|
19
20
|
loadUserConfig,
|
|
20
21
|
resolveConfig
|
|
21
22
|
} from "../chunk-PVTUVUBK.js";
|
|
22
23
|
import "../chunk-AJBCHKBP.js";
|
|
23
24
|
import "../chunk-V7LLYIRO.js";
|
|
24
|
-
import "../chunk-
|
|
25
|
+
import "../chunk-QTGT76RY.js";
|
|
25
26
|
import "../chunk-5TD7NQIW.js";
|
|
26
27
|
|
|
27
28
|
// src/node/index.ts
|
|
@@ -32,8 +33,9 @@ import remarkGfm from "remark-gfm";
|
|
|
32
33
|
import remarkFrontmatter from "remark-frontmatter";
|
|
33
34
|
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
34
35
|
import rehypeShiki from "@shikijs/rehype";
|
|
35
|
-
import { resolve } from "path";
|
|
36
|
+
import { resolve, dirname } from "path";
|
|
36
37
|
import { fileURLToPath } from "url";
|
|
38
|
+
import { createRequire } from "module";
|
|
37
39
|
import tailwindcss from "@tailwindcss/vite";
|
|
38
40
|
import { existsSync } from "fs";
|
|
39
41
|
function findPackageRoot() {
|
|
@@ -50,9 +52,22 @@ function resolveClientPath(...segments) {
|
|
|
50
52
|
const packageRoot = findPackageRoot();
|
|
51
53
|
return resolve(packageRoot, "src", "client", ...segments);
|
|
52
54
|
}
|
|
55
|
+
function dedupeResolve() {
|
|
56
|
+
const require2 = createRequire(import.meta.url);
|
|
57
|
+
const dedupePkgs = ["@mdx-js/react", "react", "react-dom", "react-router-dom"];
|
|
58
|
+
const alias = {};
|
|
59
|
+
for (const pkg of dedupePkgs) {
|
|
60
|
+
try {
|
|
61
|
+
alias[pkg] = dirname(require2.resolve(`${pkg}/package.json`));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { alias, dedupe: dedupePkgs };
|
|
66
|
+
}
|
|
53
67
|
function createDevConfig(overrides = {}) {
|
|
54
68
|
const root = resolveClientPath();
|
|
55
69
|
const { userRoot, mermaidStrategy, ...viteOverrides } = overrides;
|
|
70
|
+
const { alias: dedupeAlias, dedupe } = dedupeResolve();
|
|
56
71
|
return {
|
|
57
72
|
root,
|
|
58
73
|
plugins: [
|
|
@@ -79,8 +94,10 @@ function createDevConfig(overrides = {}) {
|
|
|
79
94
|
},
|
|
80
95
|
resolve: {
|
|
81
96
|
alias: {
|
|
82
|
-
"@clearify": resolve(root, "..")
|
|
83
|
-
|
|
97
|
+
"@clearify": resolve(root, ".."),
|
|
98
|
+
...dedupeAlias
|
|
99
|
+
},
|
|
100
|
+
dedupe
|
|
84
101
|
},
|
|
85
102
|
...viteOverrides
|
|
86
103
|
};
|
|
@@ -103,6 +120,7 @@ async function createServer(options = {}) {
|
|
|
103
120
|
}
|
|
104
121
|
export {
|
|
105
122
|
buildSite as build,
|
|
123
|
+
checkFrontmatter,
|
|
106
124
|
checkLinks,
|
|
107
125
|
createServer,
|
|
108
126
|
init
|
package/dist/node/node/init.js
CHANGED
|
@@ -70,11 +70,18 @@ interface NavigationItem {
|
|
|
70
70
|
badgeColor?: string;
|
|
71
71
|
children?: NavigationItem[];
|
|
72
72
|
}
|
|
73
|
+
type DocCategory = 'documentation' | 'internal' | 'plan' | 'research' | 'decision' | 'roadmap' | 'changelog';
|
|
73
74
|
interface PageFrontmatter {
|
|
74
75
|
title?: string;
|
|
75
76
|
description?: string;
|
|
76
77
|
icon?: string;
|
|
77
78
|
order?: number;
|
|
79
|
+
summary?: string;
|
|
80
|
+
category?: DocCategory;
|
|
81
|
+
tags?: string[];
|
|
82
|
+
projects?: string[];
|
|
83
|
+
status?: 'active' | 'superseded' | 'archived';
|
|
84
|
+
date?: string;
|
|
78
85
|
}
|
|
79
86
|
interface RouteEntry {
|
|
80
87
|
path: string;
|
|
@@ -106,4 +113,4 @@ interface HubConfig {
|
|
|
106
113
|
}
|
|
107
114
|
declare function defineConfig(config: Partial<ClearifyConfig>): Partial<ClearifyConfig>;
|
|
108
115
|
|
|
109
|
-
export { type ClearifyConfig, type HubConfig, type HubProject, type NavigationItem, type PageFrontmatter, type RemoteGitSource, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
|
|
116
|
+
export { type ClearifyConfig, type DocCategory, type HubConfig, type HubProject, type NavigationItem, type PageFrontmatter, type RemoteGitSource, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clearifyPlugin
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-PFX7LHRZ.js";
|
|
4
4
|
import "../chunk-B2Q23JW3.js";
|
|
5
5
|
import "../chunk-PVTUVUBK.js";
|
|
6
6
|
import "../chunk-AJBCHKBP.js";
|
|
7
7
|
import "../chunk-V7LLYIRO.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-QTGT76RY.js";
|
|
9
9
|
import "../chunk-5TD7NQIW.js";
|
|
10
10
|
export {
|
|
11
11
|
clearifyPlugin
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marlinjai/clearify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup",
|
|
27
|
-
"dev:self": "node bin/clearify.js dev"
|
|
27
|
+
"dev:self": "node bin/clearify.js dev",
|
|
28
|
+
"prepare": "husky",
|
|
29
|
+
"release": "semantic-release"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@mdx-js/react": "^3.1.1",
|
|
@@ -63,12 +65,18 @@
|
|
|
63
65
|
}
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
68
|
+
"@commitlint/cli": "^20.4.3",
|
|
69
|
+
"@commitlint/config-conventional": "^20.4.3",
|
|
70
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
71
|
+
"@semantic-release/git": "^10.0.1",
|
|
66
72
|
"@types/mdast": "^4.0.4",
|
|
67
73
|
"@types/node": "^25.2.2",
|
|
68
74
|
"@types/react": "^18.3.18",
|
|
69
75
|
"@types/react-dom": "^18.3.5",
|
|
70
76
|
"esbuild": "^0.27.3",
|
|
77
|
+
"husky": "^9.1.7",
|
|
71
78
|
"puppeteer": "^24.37.2",
|
|
79
|
+
"semantic-release": "^25.0.3",
|
|
72
80
|
"tsup": "^8.3.6",
|
|
73
81
|
"typescript": "^5.7.3"
|
|
74
82
|
},
|
package/src/theme/Sidebar.tsx
CHANGED
|
@@ -449,25 +449,26 @@ function NavItem({ item, depth, onNavigate, expandedGroups, onToggleGroup, group
|
|
|
449
449
|
display: 'flex',
|
|
450
450
|
alignItems: 'center',
|
|
451
451
|
width: '100%',
|
|
452
|
-
padding: '0.4rem 0.75rem',
|
|
452
|
+
padding: depth === 0 ? '0.4rem 0.75rem' : '0.35rem 0.75rem',
|
|
453
|
+
paddingLeft: `${0.75 + depth * 0.75}rem`,
|
|
453
454
|
background: 'none',
|
|
454
455
|
border: 'none',
|
|
455
456
|
cursor: 'pointer',
|
|
456
|
-
fontSize: '0.6875rem',
|
|
457
|
-
fontWeight: 600,
|
|
458
|
-
textTransform: 'uppercase',
|
|
459
|
-
letterSpacing: '0.06em',
|
|
460
|
-
color: 'var(--clearify-text-tertiary)',
|
|
461
|
-
marginTop: depth === 0 ? '1.25rem' : 0,
|
|
457
|
+
fontSize: depth === 0 ? '0.6875rem' : '0.75rem',
|
|
458
|
+
fontWeight: depth === 0 ? 600 : 500,
|
|
459
|
+
textTransform: depth === 0 ? 'uppercase' : 'none',
|
|
460
|
+
letterSpacing: depth === 0 ? '0.06em' : '0.01em',
|
|
461
|
+
color: depth === 0 ? 'var(--clearify-text-tertiary)' : 'var(--clearify-text-secondary)',
|
|
462
|
+
marginTop: depth === 0 ? '1.25rem' : '0.375rem',
|
|
462
463
|
borderRadius: 'var(--clearify-radius-sm)',
|
|
463
464
|
transition: 'color 0.15s',
|
|
464
465
|
}}
|
|
465
|
-
onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--clearify-text-secondary)')}
|
|
466
|
-
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--clearify-text-tertiary)')}
|
|
466
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = depth === 0 ? 'var(--clearify-text-secondary)' : 'var(--clearify-text)')}
|
|
467
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = depth === 0 ? 'var(--clearify-text-tertiary)' : 'var(--clearify-text-secondary)')}
|
|
467
468
|
>
|
|
468
469
|
<svg
|
|
469
|
-
width=
|
|
470
|
-
height=
|
|
470
|
+
width={depth === 0 ? '12' : '10'}
|
|
471
|
+
height={depth === 0 ? '12' : '10'}
|
|
471
472
|
viewBox="0 0 24 24"
|
|
472
473
|
fill="none"
|
|
473
474
|
stroke="currentColor"
|
|
@@ -479,6 +480,7 @@ function NavItem({ item, depth, onNavigate, expandedGroups, onToggleGroup, group
|
|
|
479
480
|
transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
480
481
|
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
|
|
481
482
|
flexShrink: 0,
|
|
483
|
+
opacity: depth === 0 ? 1 : 0.7,
|
|
482
484
|
}}
|
|
483
485
|
>
|
|
484
486
|
<polyline points="9 18 15 12 9 6" />
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadUserConfig,
|
|
3
|
-
resolveConfig,
|
|
4
|
-
resolveSections
|
|
5
|
-
} from "./chunk-PVTUVUBK.js";
|
|
6
|
-
import {
|
|
7
|
-
scanDocs
|
|
8
|
-
} from "./chunk-QNC5QRIO.js";
|
|
9
|
-
|
|
10
|
-
// src/node/check.ts
|
|
11
|
-
import { resolve } from "path";
|
|
12
|
-
import { readFileSync, existsSync } from "fs";
|
|
13
|
-
async function checkLinks() {
|
|
14
|
-
const userRoot = process.cwd();
|
|
15
|
-
const userConfig = await loadUserConfig(userRoot);
|
|
16
|
-
const config = resolveConfig(userConfig, userRoot);
|
|
17
|
-
const sections = await resolveSections(config, userRoot);
|
|
18
|
-
const validRoutes = /* @__PURE__ */ new Set();
|
|
19
|
-
const allDocs = [];
|
|
20
|
-
for (const section of sections) {
|
|
21
|
-
const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
|
|
22
|
-
for (const doc of docs) {
|
|
23
|
-
validRoutes.add(doc.routePath);
|
|
24
|
-
allDocs.push(doc);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
validRoutes.add("/");
|
|
28
|
-
const changelogPath = resolve(userRoot, "CHANGELOG.md");
|
|
29
|
-
if (existsSync(changelogPath)) {
|
|
30
|
-
validRoutes.add("/changelog");
|
|
31
|
-
}
|
|
32
|
-
const expandedRoutes = new Set(validRoutes);
|
|
33
|
-
for (const route of validRoutes) {
|
|
34
|
-
expandedRoutes.add(route.replace(/\/$/, ""));
|
|
35
|
-
if (route !== "/") expandedRoutes.add(route + "/");
|
|
36
|
-
}
|
|
37
|
-
const mdLinkRegex = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
|
|
38
|
-
const hrefRegex = /href=["']([^"']+)["']/g;
|
|
39
|
-
const brokenLinks = [];
|
|
40
|
-
for (const doc of allDocs) {
|
|
41
|
-
if (!existsSync(doc.filePath)) continue;
|
|
42
|
-
const content = readFileSync(doc.filePath, "utf-8");
|
|
43
|
-
const lines = content.split("\n");
|
|
44
|
-
for (let i = 0; i < lines.length; i++) {
|
|
45
|
-
const line = lines[i];
|
|
46
|
-
const lineNum = i + 1;
|
|
47
|
-
if (line.trimStart().startsWith("```")) continue;
|
|
48
|
-
for (const regex of [mdLinkRegex, hrefRegex]) {
|
|
49
|
-
regex.lastIndex = 0;
|
|
50
|
-
let match;
|
|
51
|
-
while ((match = regex.exec(line)) !== null) {
|
|
52
|
-
const rawLink = match[1].split(/[\s"'#]/)[0];
|
|
53
|
-
if (!rawLink || rawLink.startsWith("http://") || rawLink.startsWith("https://") || rawLink.startsWith("#") || rawLink.startsWith("mailto:") || rawLink.startsWith("tel:") || rawLink.startsWith("//") || rawLink.startsWith("{") || rawLink.startsWith("data:")) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (/\.(png|jpe?g|gif|svg|webp|ico|pdf|zip|tar|gz)$/i.test(rawLink)) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
let normalized = rawLink;
|
|
60
|
-
normalized = normalized.replace(/\.(mdx?|html?)$/, "");
|
|
61
|
-
if (!normalized.startsWith("/")) {
|
|
62
|
-
const docDir = doc.routePath.substring(0, doc.routePath.lastIndexOf("/")) || "/";
|
|
63
|
-
normalized = docDir === "/" ? "/" + normalized : docDir + "/" + normalized;
|
|
64
|
-
}
|
|
65
|
-
const check = normalized === "/" ? "/" : normalized.replace(/\/$/, "");
|
|
66
|
-
const checkNoIndex = check.replace(/\/index$/, "") || "/";
|
|
67
|
-
if (!expandedRoutes.has(check) && !expandedRoutes.has(checkNoIndex)) {
|
|
68
|
-
const suggestion = findClosest(check, validRoutes);
|
|
69
|
-
brokenLinks.push({
|
|
70
|
-
file: doc.filePath,
|
|
71
|
-
line: lineNum,
|
|
72
|
-
link: rawLink,
|
|
73
|
-
...suggestion ? { suggestion } : {}
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (brokenLinks.length === 0) {
|
|
81
|
-
console.log("\n No broken links found.\n");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
console.log(`
|
|
85
|
-
Found ${brokenLinks.length} broken link${brokenLinks.length === 1 ? "" : "s"}:
|
|
86
|
-
`);
|
|
87
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
88
|
-
for (const bl of brokenLinks) {
|
|
89
|
-
const relative = bl.file.replace(userRoot + "/", "");
|
|
90
|
-
if (!byFile.has(relative)) byFile.set(relative, []);
|
|
91
|
-
byFile.get(relative).push(bl);
|
|
92
|
-
}
|
|
93
|
-
for (const [file, links] of byFile) {
|
|
94
|
-
console.log(` ${file}`);
|
|
95
|
-
for (const link of links) {
|
|
96
|
-
const suggestion = link.suggestion ? ` (did you mean ${link.suggestion}?)` : "";
|
|
97
|
-
console.log(` line ${link.line}: ${link.link}${suggestion}`);
|
|
98
|
-
}
|
|
99
|
-
console.log();
|
|
100
|
-
}
|
|
101
|
-
process.exitCode = 1;
|
|
102
|
-
}
|
|
103
|
-
function findClosest(target, routes) {
|
|
104
|
-
let best;
|
|
105
|
-
let bestScore = Infinity;
|
|
106
|
-
const targetSegments = target.split("/").filter(Boolean);
|
|
107
|
-
for (const route of routes) {
|
|
108
|
-
const routeSegments = route.split("/").filter(Boolean);
|
|
109
|
-
const dist = levenshtein(target, route);
|
|
110
|
-
const common = targetSegments.filter((s) => routeSegments.includes(s)).length;
|
|
111
|
-
const score = dist - common * 2;
|
|
112
|
-
if (score < bestScore) {
|
|
113
|
-
bestScore = score;
|
|
114
|
-
best = route;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (best && bestScore < target.length * 0.6) return best;
|
|
118
|
-
return void 0;
|
|
119
|
-
}
|
|
120
|
-
function levenshtein(a, b) {
|
|
121
|
-
const m = a.length;
|
|
122
|
-
const n = b.length;
|
|
123
|
-
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
124
|
-
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
125
|
-
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
126
|
-
for (let i = 1; i <= m; i++) {
|
|
127
|
-
for (let j = 1; j <= n; j++) {
|
|
128
|
-
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return dp[m][n];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export {
|
|
135
|
-
checkLinks
|
|
136
|
-
};
|