@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 CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/detect
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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;
@@ -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
+ }
@@ -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
+ }
@@ -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
+ ];
@@ -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
+ }
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }