@dogsbay/detect 0.0.0-stage → 0.2.0-beta.132
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 +27 -2
- package/dist/gitbook.d.ts +24 -0
- package/dist/gitbook.js +130 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +29 -0
- package/dist/order.d.ts +25 -0
- package/dist/order.js +66 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +45 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/dist/vitepress.d.ts +18 -0
- package/dist/vitepress.js +60 -0
- package/package.json +31 -3
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dogsbay/detect
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Which documentation tool made a folder, from its file paths alone. No Node, no dependencies:
|
|
4
|
+
it runs in a browser on a repository's tree before any importer is loaded.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { detect, FORMAT_ORDER } from "@dogsbay/detect";
|
|
8
|
+
|
|
9
|
+
detect(["README.md", "docs/gitbook/SUMMARY.md", "docs/gitbook/.gitbook/assets/a.png", …]);
|
|
10
|
+
// { format: "gitbook", root: "docs/gitbook", confidence: "high", pages: 158, fidelity: "full" }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `FORMAT_ORDER` is the import formats' detection precedence. The `dogsbay` CLI builds its
|
|
14
|
+
format registry in this order, so a folder is the same format in the CLI and in the browser.
|
|
15
|
+
- `detect(paths, read?)` runs the path detectors in that order. `read` may supply a small
|
|
16
|
+
config file (`.gitbook.yaml`) by path.
|
|
17
|
+
- A detection names the corpus `root`, how sure it is, how many pages it would convert, the
|
|
18
|
+
choices to offer (`spaces`, `versions`, `locales`) and whether the browser converts with
|
|
19
|
+
less fidelity than the CLI.
|
|
20
|
+
|
|
21
|
+
Path detectors so far: GitBook and VitePress (`.vitepress/config.*` or
|
|
22
|
+
`.vitepress/config/index.*`, outside `node_modules`; the shallowest project wins, then `docs/`,
|
|
23
|
+
`site/`, `website/`). Other formats are detected on disk by the CLI, in the same order, until
|
|
24
|
+
their browser import lands.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitBook (Git Sync) from paths. A folder holding `SUMMARY.md` or `.gitbook.yaml` is a space;
|
|
3
|
+
* a folder with no marker whose subfolders are spaces is a multi-space repo (snyk/user-docs).
|
|
4
|
+
* `@dogsbay/format-gitbook` reads the same rules: its importer through `gitbookVisible` and
|
|
5
|
+
* `gitbookSpaces`, its CLI detector through `detectGitbook` over a folder's top two levels.
|
|
6
|
+
*/
|
|
7
|
+
import type { Detection, ReadText } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Does GitBook read this path? Not under `node_modules`, and not under a hidden folder other
|
|
10
|
+
* than `.gitbook` (assets and includes live there). Hidden FILES are kept: `.gitbook.yaml`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function gitbookVisible(path: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The spaces under `root` (paths relative to it): `["."]` when `root` itself has a marker,
|
|
15
|
+
* else every direct subfolder with a SUMMARY.md, else none. A nested SUMMARY.md under a
|
|
16
|
+
* space is NOT a second space (review #8 of the CLI importer).
|
|
17
|
+
*/
|
|
18
|
+
export declare function gitbookSpaces(paths: string[]): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Find a GitBook corpus among `paths` (a repo's whole tree, or one folder's listing). The
|
|
21
|
+
* shallowest marker folders win; several at that depth under one parent are that parent's
|
|
22
|
+
* spaces. `read` (optional) supplies `.gitbook.yaml` so a relocated `root:` is counted.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectGitbook(paths: string[], read?: ReadText): Detection | undefined;
|
package/dist/gitbook.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { depthOf, dirOf, joinPath, normalizePath, within } from "./paths.js";
|
|
2
|
+
/**
|
|
3
|
+
* Does GitBook read this path? Not under `node_modules`, and not under a hidden folder other
|
|
4
|
+
* than `.gitbook` (assets and includes live there). Hidden FILES are kept: `.gitbook.yaml`.
|
|
5
|
+
*/
|
|
6
|
+
export function gitbookVisible(path) {
|
|
7
|
+
const segs = path.split("/");
|
|
8
|
+
for (let i = 0; i < segs.length; i++) {
|
|
9
|
+
const s = segs[i];
|
|
10
|
+
if (s === "node_modules")
|
|
11
|
+
return false;
|
|
12
|
+
if (i < segs.length - 1 && s.startsWith(".") && s !== ".gitbook")
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const MARKERS = new Set(["SUMMARY.md", ".gitbook.yaml"]);
|
|
18
|
+
/** Folders holding a GitBook marker, outside `.gitbook/` and mdBook projects. */
|
|
19
|
+
function markerDirs(paths) {
|
|
20
|
+
const all = new Set(paths);
|
|
21
|
+
const dirs = new Set();
|
|
22
|
+
for (const p of paths) {
|
|
23
|
+
if (!gitbookVisible(p))
|
|
24
|
+
continue;
|
|
25
|
+
const name = p.slice(p.lastIndexOf("/") + 1);
|
|
26
|
+
if (!MARKERS.has(name))
|
|
27
|
+
continue;
|
|
28
|
+
const dir = dirOf(p);
|
|
29
|
+
if (dir.split("/").includes(".gitbook"))
|
|
30
|
+
continue;
|
|
31
|
+
// mdBook writes src/SUMMARY.md with book.toml beside src/: not GitBook
|
|
32
|
+
if (all.has(joinPath(dir, "book.toml")) || all.has(joinPath(dirOf(dir), "book.toml")))
|
|
33
|
+
continue;
|
|
34
|
+
dirs.add(dir);
|
|
35
|
+
}
|
|
36
|
+
return [...dirs].sort();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The spaces under `root` (paths relative to it): `["."]` when `root` itself has a marker,
|
|
40
|
+
* else every direct subfolder with a SUMMARY.md, else none. A nested SUMMARY.md under a
|
|
41
|
+
* space is NOT a second space (review #8 of the CLI importer).
|
|
42
|
+
*/
|
|
43
|
+
export function gitbookSpaces(paths) {
|
|
44
|
+
const all = new Set(paths.filter(gitbookVisible));
|
|
45
|
+
if (all.has("SUMMARY.md") || all.has(".gitbook.yaml"))
|
|
46
|
+
return ["."];
|
|
47
|
+
const out = new Set();
|
|
48
|
+
for (const p of all) {
|
|
49
|
+
const m = /^([^/]+)\/SUMMARY\.md$/.exec(p);
|
|
50
|
+
if (m && !m[1].startsWith(".") && m[1] !== "node_modules")
|
|
51
|
+
out.add(m[1]);
|
|
52
|
+
}
|
|
53
|
+
return [...out].sort();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `root:` in a `.gitbook.yaml`, read without a YAML parser (detection must stay tiny). Plain
|
|
57
|
+
* string work, no backtracking regex: the file is someone else's, and a lazy group between two
|
|
58
|
+
* whitespace runs took 34 s on 4 KB of spaces.
|
|
59
|
+
*/
|
|
60
|
+
function configRoot(text) {
|
|
61
|
+
if (!text)
|
|
62
|
+
return "";
|
|
63
|
+
for (const line of text.slice(0, 65536).split("\n")) {
|
|
64
|
+
if (!line.startsWith("root:"))
|
|
65
|
+
continue;
|
|
66
|
+
let v = line.slice(5);
|
|
67
|
+
const hash = v.indexOf("#");
|
|
68
|
+
if (hash >= 0)
|
|
69
|
+
v = v.slice(0, hash);
|
|
70
|
+
v = v.trim();
|
|
71
|
+
if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v[v.length - 1] === v[0])
|
|
72
|
+
v = v.slice(1, -1).trim();
|
|
73
|
+
return normalizePath(v) ?? "";
|
|
74
|
+
}
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find a GitBook corpus among `paths` (a repo's whole tree, or one folder's listing). The
|
|
79
|
+
* shallowest marker folders win; several at that depth under one parent are that parent's
|
|
80
|
+
* spaces. `read` (optional) supplies `.gitbook.yaml` so a relocated `root:` is counted.
|
|
81
|
+
*/
|
|
82
|
+
export function detectGitbook(paths, read) {
|
|
83
|
+
const markers = markerDirs(paths);
|
|
84
|
+
if (!markers.length)
|
|
85
|
+
return undefined;
|
|
86
|
+
const depth = Math.min(...markers.map(depthOf));
|
|
87
|
+
const byParent = new Map();
|
|
88
|
+
for (const d of markers.filter((m) => depthOf(m) === depth)) {
|
|
89
|
+
const parent = d ? dirOf(d) : "";
|
|
90
|
+
byParent.set(parent, [...(byParent.get(parent) ?? []), d]);
|
|
91
|
+
}
|
|
92
|
+
// The parent with the most spaces; ties go to the first in path order
|
|
93
|
+
const [parent, group] = [...byParent].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))[0];
|
|
94
|
+
const root = group.length > 1 ? parent : group[0];
|
|
95
|
+
const inRoot = paths.filter((p) => within(p, root)).map((p) => (root ? p.slice(root.length + 1) : p));
|
|
96
|
+
const spaces = gitbookSpaces(inRoot);
|
|
97
|
+
if (!spaces.length)
|
|
98
|
+
return undefined;
|
|
99
|
+
let pages = 0;
|
|
100
|
+
let gitbookDir = false;
|
|
101
|
+
for (const space of spaces) {
|
|
102
|
+
const spaceDir = space === "." ? "" : space;
|
|
103
|
+
const yaml = joinPath(spaceDir, ".gitbook.yaml");
|
|
104
|
+
if (inRoot.includes(yaml))
|
|
105
|
+
gitbookDir = true;
|
|
106
|
+
const docs = normalizePath(joinPath(spaceDir, configRoot(read?.(joinPath(root, yaml))))) ?? spaceDir;
|
|
107
|
+
for (const p of inRoot) {
|
|
108
|
+
if (!within(p, docs) || !gitbookVisible(p))
|
|
109
|
+
continue;
|
|
110
|
+
const rel = docs ? p.slice(docs.length + 1) : p;
|
|
111
|
+
if (rel.startsWith(".gitbook/"))
|
|
112
|
+
gitbookDir = true;
|
|
113
|
+
if (!/\.md$/i.test(rel) || rel.includes(".gitbook/includes/"))
|
|
114
|
+
continue;
|
|
115
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
116
|
+
if (name === "SUMMARY.md" || name === "GLOSSARY.md")
|
|
117
|
+
continue;
|
|
118
|
+
pages++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
format: "gitbook",
|
|
123
|
+
root,
|
|
124
|
+
// SUMMARY.md alone is also how HonKit and other book tools start; `.gitbook/` is GitBook's own
|
|
125
|
+
confidence: gitbookDir ? "high" : "low",
|
|
126
|
+
pages,
|
|
127
|
+
fidelity: "full",
|
|
128
|
+
...(spaces.length > 1 ? { choices: { spaces } } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FormatName } from "./order.js";
|
|
2
|
+
import type { Detection, ReadText } from "./types.js";
|
|
3
|
+
export { FORMAT_ORDER, type FormatName } from "./order.js";
|
|
4
|
+
export type { Detection, ReadText } from "./types.js";
|
|
5
|
+
export { detectGitbook, gitbookSpaces, gitbookVisible } from "./gitbook.js";
|
|
6
|
+
export { detectVitepress, vitepressConfigPath, vitepressPage, vitepressRoots, VITEPRESS_SKIP_DIRS } from "./vitepress.js";
|
|
7
|
+
export { dirOf, isPlainPath, joinPath, normalizePath, within } from "./paths.js";
|
|
8
|
+
/** The formats with a path detector, by name. */
|
|
9
|
+
export declare const PATH_DETECTORS: Partial<Record<FormatName, (paths: string[], read?: ReadText) => Detection | undefined>>;
|
|
10
|
+
/** The first format, in `FORMAT_ORDER`, whose path detector claims `paths`. */
|
|
11
|
+
export declare function detect(paths: string[], read?: ReadText): Detection | undefined;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dogsbay/detect — which documentation tool made a folder, from its paths. No Node, no
|
|
3
|
+
* dependencies: the playground runs it on a repo's tree before loading any importer, and the
|
|
4
|
+
* dogsbay CLI takes its format order from here (`FORMAT_ORDER`).
|
|
5
|
+
*
|
|
6
|
+
* Formats get a path detector here as their browser import lands (GitBook, VitePress); until then
|
|
7
|
+
* the CLI keeps detecting them on disk, in this same order.
|
|
8
|
+
*/
|
|
9
|
+
import { detectGitbook } from "./gitbook.js";
|
|
10
|
+
import { detectVitepress } from "./vitepress.js";
|
|
11
|
+
import { FORMAT_ORDER } from "./order.js";
|
|
12
|
+
export { FORMAT_ORDER } from "./order.js";
|
|
13
|
+
export { detectGitbook, gitbookSpaces, gitbookVisible } from "./gitbook.js";
|
|
14
|
+
export { detectVitepress, vitepressConfigPath, vitepressPage, vitepressRoots, VITEPRESS_SKIP_DIRS } from "./vitepress.js";
|
|
15
|
+
export { dirOf, isPlainPath, joinPath, normalizePath, within } from "./paths.js";
|
|
16
|
+
/** The formats with a path detector, by name. */
|
|
17
|
+
export const PATH_DETECTORS = {
|
|
18
|
+
gitbook: detectGitbook,
|
|
19
|
+
vitepress: (paths) => detectVitepress(paths),
|
|
20
|
+
};
|
|
21
|
+
/** The first format, in `FORMAT_ORDER`, whose path detector claims `paths`. */
|
|
22
|
+
export function detect(paths, read) {
|
|
23
|
+
for (const name of FORMAT_ORDER) {
|
|
24
|
+
const found = PATH_DETECTORS[name]?.(paths, read);
|
|
25
|
+
if (found)
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
package/dist/order.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE detection order of the import formats, by plugin name. The dogsbay CLI
|
|
3
|
+
* builds its plugin registry (`packages/cli/src/formats.ts`) in this order and
|
|
4
|
+
* refuses to start if the two name different formats; the playground runs the
|
|
5
|
+
* path detectors below in the same order. One list, so a browser import and
|
|
6
|
+
* `dogsbay convert` cannot disagree about what a folder is.
|
|
7
|
+
*
|
|
8
|
+
* ORDER IS DETECTION PRECEDENCE: the first importable format that claims a
|
|
9
|
+
* folder wins.
|
|
10
|
+
*
|
|
11
|
+
* Config-anchored detectors (mkdocs.yml, docusaurus.config.*, hugo.*, …) come
|
|
12
|
+
* first because they cannot false-positive. The two CATCH-ALL detectors go
|
|
13
|
+
* last-but-one and last-ish, and their relative order is load-bearing:
|
|
14
|
+
*
|
|
15
|
+
* asciidoc → a corpus whose `.adoc` files outnumber its `.md` files
|
|
16
|
+
* dogsbay-md → any `.md` anywhere under the source
|
|
17
|
+
*
|
|
18
|
+
* asciidoc goes FIRST of the two: a real AsciiDoc corpus almost always carries
|
|
19
|
+
* a `README.md` (openshift-docs does), and markdown-first would claim it and
|
|
20
|
+
* import that one file instead of 20k pages. The inverse mistake is prevented
|
|
21
|
+
* inside asciidoc's own detector, which requires `.adoc` DOMINANCE rather than
|
|
22
|
+
* mere presence (`format-asciidoc/src/loaders/plain.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export declare const FORMAT_ORDER: readonly ["mkdocs", "astro", "obsidian", "openknowledge", "mdx", "starlight", "docusaurus", "gitbook", "hugo", "vitepress", "rst", "myst", "asciidoc", "dogsbay-md", "gatsby", "openapi"];
|
|
25
|
+
export type FormatName = (typeof FORMAT_ORDER)[number];
|
package/dist/order.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE detection order of the import formats, by plugin name. The dogsbay CLI
|
|
3
|
+
* builds its plugin registry (`packages/cli/src/formats.ts`) in this order and
|
|
4
|
+
* refuses to start if the two name different formats; the playground runs the
|
|
5
|
+
* path detectors below in the same order. One list, so a browser import and
|
|
6
|
+
* `dogsbay convert` cannot disagree about what a folder is.
|
|
7
|
+
*
|
|
8
|
+
* ORDER IS DETECTION PRECEDENCE: the first importable format that claims a
|
|
9
|
+
* folder wins.
|
|
10
|
+
*
|
|
11
|
+
* Config-anchored detectors (mkdocs.yml, docusaurus.config.*, hugo.*, …) come
|
|
12
|
+
* first because they cannot false-positive. The two CATCH-ALL detectors go
|
|
13
|
+
* last-but-one and last-ish, and their relative order is load-bearing:
|
|
14
|
+
*
|
|
15
|
+
* asciidoc → a corpus whose `.adoc` files outnumber its `.md` files
|
|
16
|
+
* dogsbay-md → any `.md` anywhere under the source
|
|
17
|
+
*
|
|
18
|
+
* asciidoc goes FIRST of the two: a real AsciiDoc corpus almost always carries
|
|
19
|
+
* a `README.md` (openshift-docs does), and markdown-first would claim it and
|
|
20
|
+
* import that one file instead of 20k pages. The inverse mistake is prevented
|
|
21
|
+
* inside asciidoc's own detector, which requires `.adoc` DOMINANCE rather than
|
|
22
|
+
* mere presence (`format-asciidoc/src/loaders/plain.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export const FORMAT_ORDER = [
|
|
25
|
+
"mkdocs",
|
|
26
|
+
"astro",
|
|
27
|
+
"obsidian",
|
|
28
|
+
"openknowledge",
|
|
29
|
+
"mdx",
|
|
30
|
+
"starlight",
|
|
31
|
+
"docusaurus",
|
|
32
|
+
// gitbook BEFORE dogsbay-md: GitBook corpora are markdown, so the generic
|
|
33
|
+
// dogsbay-md detector would claim them first and skip the {% %} tag
|
|
34
|
+
// translation. gitbook's detector requires SUMMARY.md / .gitbook.yaml, so it
|
|
35
|
+
// can't false-positive on a real dogsbay-md site.
|
|
36
|
+
"gitbook",
|
|
37
|
+
// hugo BEFORE dogsbay-md: Hugo content is markdown, so the generic
|
|
38
|
+
// dogsbay-md detector would claim it first and skip every shortcode. Hugo's
|
|
39
|
+
// detector requires a hugo.*/config.* file AND a content directory, so it
|
|
40
|
+
// cannot false-positive on a plain markdown corpus.
|
|
41
|
+
"hugo",
|
|
42
|
+
// vitepress BEFORE dogsbay-md: VitePress content is markdown, so the generic
|
|
43
|
+
// dogsbay-md detector would claim it first and skip every `:::` container,
|
|
44
|
+
// code-group and `<<<` snippet. Its detector requires an actual
|
|
45
|
+
// `.vitepress/config.*`, so it cannot false-positive on a plain markdown,
|
|
46
|
+
// MkDocs or GitBook corpus.
|
|
47
|
+
"vitepress",
|
|
48
|
+
"rst",
|
|
49
|
+
// myst BEFORE dogsbay-md, AFTER gitbook/rst: MyST corpora are markdown, so
|
|
50
|
+
// the generic dogsbay-md detector would claim them first and skip the
|
|
51
|
+
// directive/role translation. myst's detector requires an explicit MyST
|
|
52
|
+
// config (myst.yml / _toc.yml) or MyST-specific syntax, so it can't
|
|
53
|
+
// false-positive on a plain dogsbay-md or GitBook site.
|
|
54
|
+
"myst",
|
|
55
|
+
// asciidoc BEFORE dogsbay-md: see the precedence note above.
|
|
56
|
+
"asciidoc",
|
|
57
|
+
"dogsbay-md",
|
|
58
|
+
"gatsby",
|
|
59
|
+
// openapi LAST. Its directory probe matches a top-level
|
|
60
|
+
// `openapi.{yaml,yml,json}` / `spec.*`, and plenty of docs corpora ship
|
|
61
|
+
// exactly that file alongside their pages (dub and ollama both do). In that
|
|
62
|
+
// case the DOCS format should win and the spec is imported deliberately with
|
|
63
|
+
// `--from openapi`; auto-detection only claims a bare spec file or a
|
|
64
|
+
// spec-only directory, where nothing else matches.
|
|
65
|
+
"openapi",
|
|
66
|
+
];
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Posix path helpers over corpus-relative paths ("" is the root). No Node. */
|
|
2
|
+
/** The folder of a path: `a/b/c.md` → `a/b`, `c.md` → "". */
|
|
3
|
+
export declare function dirOf(path: string): string;
|
|
4
|
+
/** `a` + `b/c` → `a/b/c`; an empty side is dropped. */
|
|
5
|
+
export declare function joinPath(a: string, b: string): string;
|
|
6
|
+
/** Folder depth: "" → 0, `a` → 1, `a/b` → 2. */
|
|
7
|
+
export declare function depthOf(dir: string): number;
|
|
8
|
+
/** Is `path` the folder `dir` or inside it? */
|
|
9
|
+
export declare function within(path: string, dir: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `.` and `..` segments and drop a leading `./` or `/`. Undefined when the path leads
|
|
12
|
+
* above the root: a path from someone else's repo must not reach outside it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizePath(path: string): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Is `path` a plain corpus path: posix, relative, no `.`/`..`/empty segment, no backslash,
|
|
17
|
+
* no control character? Anything else is left out of a corpus: a file named `b\..\..\x.md` is
|
|
18
|
+
* one segment on Linux and three levels up once a slug turns `\` into `/`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isPlainPath(path: string): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Posix path helpers over corpus-relative paths ("" is the root). No Node. */
|
|
2
|
+
/** The folder of a path: `a/b/c.md` → `a/b`, `c.md` → "". */
|
|
3
|
+
export function dirOf(path) {
|
|
4
|
+
const i = path.lastIndexOf("/");
|
|
5
|
+
return i < 0 ? "" : path.slice(0, i);
|
|
6
|
+
}
|
|
7
|
+
/** `a` + `b/c` → `a/b/c`; an empty side is dropped. */
|
|
8
|
+
export function joinPath(a, b) {
|
|
9
|
+
return a && b ? `${a}/${b}` : a || b;
|
|
10
|
+
}
|
|
11
|
+
/** Folder depth: "" → 0, `a` → 1, `a/b` → 2. */
|
|
12
|
+
export function depthOf(dir) {
|
|
13
|
+
return dir ? dir.split("/").length : 0;
|
|
14
|
+
}
|
|
15
|
+
/** Is `path` the folder `dir` or inside it? */
|
|
16
|
+
export function within(path, dir) {
|
|
17
|
+
return !dir || path === dir || path.startsWith(`${dir}/`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve `.` and `..` segments and drop a leading `./` or `/`. Undefined when the path leads
|
|
21
|
+
* above the root: a path from someone else's repo must not reach outside it.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizePath(path) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const seg of path.replace(/\\/g, "/").split("/")) {
|
|
26
|
+
if (seg === "" || seg === ".")
|
|
27
|
+
continue;
|
|
28
|
+
if (seg === "..") {
|
|
29
|
+
if (!out.length)
|
|
30
|
+
return undefined;
|
|
31
|
+
out.pop();
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
out.push(seg);
|
|
35
|
+
}
|
|
36
|
+
return out.join("/");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Is `path` a plain corpus path: posix, relative, no `.`/`..`/empty segment, no backslash,
|
|
40
|
+
* no control character? Anything else is left out of a corpus: a file named `b\..\..\x.md` is
|
|
41
|
+
* one segment on Linux and three levels up once a slug turns `\` into `/`.
|
|
42
|
+
*/
|
|
43
|
+
export function isPlainPath(path) {
|
|
44
|
+
return path !== "" && !/[\\\0-\x1f]/.test(path) && normalizePath(path) === path;
|
|
45
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormatName } from "./order.js";
|
|
2
|
+
/** What a detector found: enough for a "convert it?" step, from paths alone. */
|
|
3
|
+
export interface Detection {
|
|
4
|
+
format: FormatName;
|
|
5
|
+
/** The corpus folder, relative to the paths given ("" for their root). */
|
|
6
|
+
root: string;
|
|
7
|
+
/** "low" when the signal is shared with other tools (a bare SUMMARY.md). */
|
|
8
|
+
confidence: "high" | "low";
|
|
9
|
+
/** Pages the import would convert, as counted from paths. */
|
|
10
|
+
pages: number;
|
|
11
|
+
/** What the person may choose before converting. */
|
|
12
|
+
choices?: {
|
|
13
|
+
spaces?: string[];
|
|
14
|
+
versions?: string[];
|
|
15
|
+
locales?: string[];
|
|
16
|
+
};
|
|
17
|
+
/** "translate-only": the browser converts with less fidelity than the CLI (MyST without Sphinx). */
|
|
18
|
+
fidelity: "full" | "translate-only";
|
|
19
|
+
}
|
|
20
|
+
/** Reads one small text file (a config), by path relative to the paths' root; undefined if absent. */
|
|
21
|
+
export type ReadText = (path: string) => string | undefined;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VitePress from paths. `.vitepress/config.*` (or the split layout `.vitepress/config/index.*`)
|
|
3
|
+
* is the only unambiguous marker: requiring it is what keeps VitePress from claiming a
|
|
4
|
+
* plain-Markdown, MkDocs or GitBook corpus. The project root is the folder holding `.vitepress`.
|
|
5
|
+
* `@dogsbay/format-vitepress` finds its config through `vitepressConfigPath`, so detection and
|
|
6
|
+
* import cannot disagree.
|
|
7
|
+
*/
|
|
8
|
+
import type { Detection } from "./types.js";
|
|
9
|
+
/** Folders a VitePress project keeps beside its pages, never pages themselves. */
|
|
10
|
+
export declare const VITEPRESS_SKIP_DIRS: Set<string>;
|
|
11
|
+
/** The config file of the project at `root`, as VitePress looks for it; undefined if none. */
|
|
12
|
+
export declare function vitepressConfigPath(paths: Iterable<string>, root: string): string | undefined;
|
|
13
|
+
/** Project roots among `paths`: the folders holding a `.vitepress` with a config, best first. */
|
|
14
|
+
export declare function vitepressRoots(paths: string[]): string[];
|
|
15
|
+
/** Is `path` (relative to a project root) a page VitePress would build, before its config is read? */
|
|
16
|
+
export declare function vitepressPage(rel: string): boolean;
|
|
17
|
+
/** Find a VitePress project among `paths` (a repo's whole tree, or one folder's listing). */
|
|
18
|
+
export declare function detectVitepress(paths: string[]): Detection | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { depthOf, joinPath, within } from "./paths.js";
|
|
2
|
+
const CONFIG_EXTS = [".ts", ".mts", ".mjs", ".js", ".cts", ".cjs"];
|
|
3
|
+
/** Folders a VitePress project keeps beside its pages, never pages themselves. */
|
|
4
|
+
export const VITEPRESS_SKIP_DIRS = new Set([".vitepress", "node_modules", "public", "dist", ".git", "cache", ".cache"]);
|
|
5
|
+
/** Where a repo usually keeps its docs, preferred among folders at the same depth. */
|
|
6
|
+
const PREFERRED = ["docs", "site", "website"];
|
|
7
|
+
/** The config file of the project at `root`, as VitePress looks for it; undefined if none. */
|
|
8
|
+
export function vitepressConfigPath(paths, root) {
|
|
9
|
+
const all = paths instanceof Set ? paths : new Set(paths);
|
|
10
|
+
const vp = joinPath(root, ".vitepress");
|
|
11
|
+
for (const ext of CONFIG_EXTS)
|
|
12
|
+
if (all.has(`${vp}/config${ext}`))
|
|
13
|
+
return `${vp}/config${ext}`;
|
|
14
|
+
for (const ext of CONFIG_EXTS)
|
|
15
|
+
if (all.has(`${vp}/config/index${ext}`))
|
|
16
|
+
return `${vp}/config/index${ext}`;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Project roots among `paths`: the folders holding a `.vitepress` with a config, best first. */
|
|
20
|
+
export function vitepressRoots(paths) {
|
|
21
|
+
const all = new Set(paths);
|
|
22
|
+
const roots = new Set();
|
|
23
|
+
for (const p of paths) {
|
|
24
|
+
const m = /(^|\/)\.vitepress\/config(\/index)?\.[cm]?[jt]s$/.exec(p);
|
|
25
|
+
if (!m)
|
|
26
|
+
continue;
|
|
27
|
+
const root = p.slice(0, m.index);
|
|
28
|
+
// Not inside node_modules or another hidden folder (a vendored copy, a cache)
|
|
29
|
+
if (root.split("/").some((s) => s === "node_modules" || (s.startsWith(".") && s !== "")))
|
|
30
|
+
continue;
|
|
31
|
+
if (vitepressConfigPath(all, root))
|
|
32
|
+
roots.add(root);
|
|
33
|
+
}
|
|
34
|
+
const rank = (r) => {
|
|
35
|
+
const i = PREFERRED.indexOf(r.slice(r.lastIndexOf("/") + 1));
|
|
36
|
+
return i < 0 ? PREFERRED.length : i;
|
|
37
|
+
};
|
|
38
|
+
return [...roots].sort((a, b) => depthOf(a) - depthOf(b) || rank(a) - rank(b) || a.localeCompare(b));
|
|
39
|
+
}
|
|
40
|
+
/** Is `path` (relative to a project root) a page VitePress would build, before its config is read? */
|
|
41
|
+
export function vitepressPage(rel) {
|
|
42
|
+
const segs = rel.split("/");
|
|
43
|
+
if (segs.slice(0, -1).some((s) => VITEPRESS_SKIP_DIRS.has(s) || s.startsWith(".")))
|
|
44
|
+
return false;
|
|
45
|
+
return /\.md$/i.test(segs[segs.length - 1]);
|
|
46
|
+
}
|
|
47
|
+
/** Find a VitePress project among `paths` (a repo's whole tree, or one folder's listing). */
|
|
48
|
+
export function detectVitepress(paths) {
|
|
49
|
+
const [root] = vitepressRoots(paths);
|
|
50
|
+
if (root === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
let pages = 0;
|
|
53
|
+
for (const p of paths) {
|
|
54
|
+
if (!within(p, root))
|
|
55
|
+
continue;
|
|
56
|
+
if (vitepressPage(root ? p.slice(root.length + 1) : p))
|
|
57
|
+
pages++;
|
|
58
|
+
}
|
|
59
|
+
return { format: "vitepress", root, confidence: "high", pages, fidelity: "full" };
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/detect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-beta.132",
|
|
4
|
+
"description": "Which documentation tool made this folder: the import formats' detection order (shared by the dogsbay CLI and the playground) and detectors that read paths only, with no Node and no dependencies",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"esbuild": "^0.25.0",
|
|
20
|
+
"typescript": "^5.9.3",
|
|
21
|
+
"vitest": "^4.1.10"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/dogsbay/dogsbay.git",
|
|
27
|
+
"directory": "packages/detect"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
}
|
|
6
34
|
}
|