@devp0nt/doc0 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/cache.d.ts +13 -0
- package/dist/cache.js +50 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +136 -0
- package/dist/docs.d.ts +42 -0
- package/dist/docs.js +100 -0
- package/dist/export.d.ts +29 -0
- package/dist/export.js +76 -0
- package/dist/generate.d.ts +7 -0
- package/dist/generate.js +12 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +160 -0
- package/dist/load.d.ts +10 -0
- package/dist/load.js +17 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/tools.d.ts +39 -0
- package/dist/mcp/tools.js +72 -0
- package/dist/normalize.d.ts +21 -0
- package/dist/normalize.js +25 -0
- package/dist/parsers/code.d.ts +7 -0
- package/dist/parsers/code.js +73 -0
- package/dist/parsers/index.d.ts +11 -0
- package/dist/parsers/index.js +15 -0
- package/dist/parsers/markdown.d.ts +7 -0
- package/dist/parsers/markdown.js +36 -0
- package/dist/search/embedder.d.ts +12 -0
- package/dist/search/embedder.js +30 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +80 -0
- package/dist/select.d.ts +19 -0
- package/dist/select.js +25 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +27 -0
- package/dist/utils.js +74 -0
- package/dist/web/index.d.ts +145 -0
- package/dist/web/index.js +43 -0
- package/dist/web/render.d.ts +12 -0
- package/dist/web/render.js +129 -0
- package/package.json +164 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Doc0Source, DocRelated, DocSource, Docs, RelatedRef, ServeWebOptions } from "../types.js";
|
|
2
|
+
import { stripLeadingHeading } from "../utils.js";
|
|
3
|
+
import { DocsPage } from "../mcp/tools.js";
|
|
4
|
+
import { PAGE_HTML, renderMarkdown } from "./render.js";
|
|
5
|
+
import { Elysia } from "elysia";
|
|
6
|
+
|
|
7
|
+
//#region src/web/index.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Build the Elysia app (JSON API + the UI page) without listening — handy for tests via `app.handle(request)`.
|
|
10
|
+
*/
|
|
11
|
+
declare const createWebApp: (getDocs: () => Promise<Docs>) => Elysia<"", {
|
|
12
|
+
decorator: {};
|
|
13
|
+
store: {};
|
|
14
|
+
derive: {};
|
|
15
|
+
resolve: {};
|
|
16
|
+
}, {
|
|
17
|
+
typebox: {};
|
|
18
|
+
error: {};
|
|
19
|
+
}, {
|
|
20
|
+
schema: {};
|
|
21
|
+
standaloneSchema: {};
|
|
22
|
+
macro: {};
|
|
23
|
+
macroFn: {};
|
|
24
|
+
parser: {};
|
|
25
|
+
response: {};
|
|
26
|
+
}, {
|
|
27
|
+
get: {
|
|
28
|
+
body: unknown;
|
|
29
|
+
params: {};
|
|
30
|
+
query: unknown;
|
|
31
|
+
headers: unknown;
|
|
32
|
+
response: {
|
|
33
|
+
200: Response;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
} & {
|
|
37
|
+
api: {
|
|
38
|
+
docs: {
|
|
39
|
+
get: {
|
|
40
|
+
body: unknown;
|
|
41
|
+
params: {};
|
|
42
|
+
query: unknown;
|
|
43
|
+
headers: unknown;
|
|
44
|
+
response: {
|
|
45
|
+
200: DocsPage;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
} & {
|
|
51
|
+
api: {
|
|
52
|
+
search: {
|
|
53
|
+
get: {
|
|
54
|
+
body: unknown;
|
|
55
|
+
params: {};
|
|
56
|
+
query: unknown;
|
|
57
|
+
headers: unknown;
|
|
58
|
+
response: {
|
|
59
|
+
200: {
|
|
60
|
+
hits: (Omit<Partial<{
|
|
61
|
+
source: DocSource;
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
description: string;
|
|
65
|
+
tags: string[];
|
|
66
|
+
category: string[];
|
|
67
|
+
related: DocRelated[];
|
|
68
|
+
content: string;
|
|
69
|
+
}>, "related"> & {
|
|
70
|
+
related?: RelatedRef[];
|
|
71
|
+
} & {
|
|
72
|
+
snippet: string;
|
|
73
|
+
})[];
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
} & {
|
|
80
|
+
api: {
|
|
81
|
+
doc: {
|
|
82
|
+
":id": {
|
|
83
|
+
get: {
|
|
84
|
+
body: unknown;
|
|
85
|
+
params: {
|
|
86
|
+
id: string;
|
|
87
|
+
} & {};
|
|
88
|
+
query: unknown;
|
|
89
|
+
headers: unknown;
|
|
90
|
+
response: {
|
|
91
|
+
200: {
|
|
92
|
+
error: string;
|
|
93
|
+
id?: undefined;
|
|
94
|
+
title?: undefined;
|
|
95
|
+
description?: undefined;
|
|
96
|
+
tags?: undefined;
|
|
97
|
+
category?: undefined;
|
|
98
|
+
source?: undefined;
|
|
99
|
+
related?: undefined;
|
|
100
|
+
html?: undefined;
|
|
101
|
+
} | {
|
|
102
|
+
id: string;
|
|
103
|
+
title: string;
|
|
104
|
+
description: string;
|
|
105
|
+
tags: string[];
|
|
106
|
+
category: string[];
|
|
107
|
+
source: DocSource;
|
|
108
|
+
related: RelatedRef[];
|
|
109
|
+
html: string;
|
|
110
|
+
error?: undefined;
|
|
111
|
+
};
|
|
112
|
+
422: {
|
|
113
|
+
type: "validation";
|
|
114
|
+
on: string;
|
|
115
|
+
summary?: string;
|
|
116
|
+
message?: string;
|
|
117
|
+
found?: unknown;
|
|
118
|
+
property?: string;
|
|
119
|
+
expected?: string;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
}, {
|
|
127
|
+
derive: {};
|
|
128
|
+
resolve: {};
|
|
129
|
+
schema: {};
|
|
130
|
+
standaloneSchema: {};
|
|
131
|
+
response: {};
|
|
132
|
+
}, {
|
|
133
|
+
derive: {};
|
|
134
|
+
resolve: {};
|
|
135
|
+
schema: {};
|
|
136
|
+
standaloneSchema: {};
|
|
137
|
+
response: {};
|
|
138
|
+
}>;
|
|
139
|
+
/**
|
|
140
|
+
* Serve a doc0 collection as a local web UI with search. Pass a `Doc0` instance or a path to a config module. Handlers
|
|
141
|
+
* call `collect()`, which self-revalidates, so the served data stays fresh.
|
|
142
|
+
*/
|
|
143
|
+
declare const serveWeb: (src: Doc0Source, options?: ServeWebOptions) => Promise<ReturnType<typeof createWebApp>>;
|
|
144
|
+
//#endregion
|
|
145
|
+
export { PAGE_HTML, createWebApp, renderMarkdown, serveWeb, stripLeadingHeading };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadDoc0 } from "../load.js";
|
|
2
|
+
import { stripLeadingHeading } from "../utils.js";
|
|
3
|
+
import { listDocs, searchDocs } from "../mcp/tools.js";
|
|
4
|
+
import { PAGE_HTML, renderMarkdown } from "./render.js";
|
|
5
|
+
import { Elysia } from "elysia";
|
|
6
|
+
//#region src/web/index.ts
|
|
7
|
+
const html = (body) => new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
8
|
+
/**
|
|
9
|
+
* Build the Elysia app (JSON API + the UI page) without listening — handy for tests via `app.handle(request)`.
|
|
10
|
+
*/
|
|
11
|
+
const createWebApp = (getDocs) => new Elysia().get("/", () => html(PAGE_HTML)).get("/api/docs", async () => listDocs(await getDocs())).get("/api/search", async ({ query }) => {
|
|
12
|
+
const term = typeof query.q === "string" ? query.q : "";
|
|
13
|
+
return { hits: term ? await searchDocs(await getDocs(), term) : [] };
|
|
14
|
+
}).get("/api/doc/:id", async ({ params, set }) => {
|
|
15
|
+
const docs = await getDocs();
|
|
16
|
+
const doc = docs.get(params.id);
|
|
17
|
+
if (!doc) {
|
|
18
|
+
set.status = 404;
|
|
19
|
+
return { error: `No doc with id "${params.id}"` };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
id: doc.id,
|
|
23
|
+
title: doc.title,
|
|
24
|
+
description: doc.description,
|
|
25
|
+
tags: doc.tags,
|
|
26
|
+
category: doc.category,
|
|
27
|
+
source: doc.source,
|
|
28
|
+
related: docs.related(doc.id),
|
|
29
|
+
html: renderMarkdown(stripLeadingHeading(doc.content))
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Serve a doc0 collection as a local web UI with search. Pass a `Doc0` instance or a path to a config module. Handlers
|
|
34
|
+
* call `collect()`, which self-revalidates, so the served data stays fresh.
|
|
35
|
+
*/
|
|
36
|
+
const serveWeb = async (src, options = {}) => {
|
|
37
|
+
const doc0 = await loadDoc0(src);
|
|
38
|
+
const app = createWebApp(() => doc0.collect());
|
|
39
|
+
app.listen(options.port ?? 4e3);
|
|
40
|
+
return app;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { PAGE_HTML, createWebApp, renderMarkdown, serveWeb, stripLeadingHeading };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stripLeadingHeading } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
//#region src/web/render.d.ts
|
|
4
|
+
/** Render Markdown to HTML, highlighting code blocks with highlight.js (server-side). */
|
|
5
|
+
declare const renderMarkdown: (content: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* The single-page UI shell. Static — it fetches `/api/docs`, `/api/search`, and `/api/doc/:id` at runtime, so search
|
|
8
|
+
* stays server-side (orama hybrid).
|
|
9
|
+
*/
|
|
10
|
+
declare const PAGE_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>doc0</title>\n<style>\n :root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --muted:#8b949e; --accent:#58a6ff; }\n * { box-sizing: border-box; }\n body { margin:0; font:14px/1.55 system-ui, sans-serif; color:var(--fg); background:var(--bg); display:flex; height:100vh; }\n aside { width:300px; min-width:300px; border-right:1px solid var(--border); background:var(--panel); display:flex; flex-direction:column; }\n header { padding:12px; border-bottom:1px solid var(--border); }\n header strong { font-size:16px; }\n #q { width:100%; margin-top:8px; padding:8px 10px; border-radius:6px; border:1px solid var(--border); background:var(--bg); color:var(--fg); }\n #list { overflow:auto; padding:8px; }\n .cat { color:var(--muted); text-transform:uppercase; font-size:11px; letter-spacing:.05em; margin:12px 8px 4px; }\n .item { padding:6px 8px; border-radius:6px; cursor:pointer; }\n .item:hover { background:var(--bg); }\n .item .t { font-weight:600; }\n .item .d { color:var(--muted); font-size:12px; }\n main { flex:1; overflow:auto; padding:32px 40px; }\n main :where(h1,h2,h3) { line-height:1.25; }\n main h1:first-child { margin-top:0; }\n .src { color:var(--muted); font-family: ui-monospace, monospace; font-size:12px; margin:-8px 0 14px; }\n main pre { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:14px; overflow:auto; }\n main code { font-family: ui-monospace, monospace; }\n main :not(pre) > code { background:var(--panel); padding:1px 5px; border-radius:4px; }\n a { color:var(--accent); text-decoration:none; }\n .tag { display:inline-block; font-size:11px; color:var(--muted); border:1px solid var(--border); border-radius:10px; padding:0 7px; margin:0 4px 4px 0; }\n .related { margin-top:32px; border-top:1px solid var(--border); padding-top:16px; }\n .related h2 { font-size:13px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin:0 0 10px; }\n .rel { margin:0 0 10px; }\n .rel a { font-weight:600; }\n .rel .d { color:var(--muted); font-size:12px; margin-top:2px; }\n .rel .req { color:#d2a8ff; font-size:11px; margin-left:6px; }\n .hljs-keyword,.hljs-built_in,.hljs-literal { color:#ff7b72; }\n .hljs-string,.hljs-attr { color:#a5d6ff; }\n .hljs-comment { color:var(--muted); font-style:italic; }\n .hljs-number,.hljs-title,.hljs-type { color:#79c0ff; }\n .hljs-function,.hljs-name { color:#d2a8ff; }\n</style>\n</head>\n<body>\n<aside>\n <header><strong>doc0</strong><input id=\"q\" placeholder=\"Search docs\u2026\" autocomplete=\"off\" /></header>\n <div id=\"list\"></div>\n</aside>\n<main id=\"content\"><p style=\"color:var(--muted)\">Select a doc on the left, or search.</p></main>\n<script>\n var contentEl = document.getElementById('content');\n var listEl = document.getElementById('list');\n var qEl = document.getElementById('q');\n\n function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }\n\n function renderList(items) {\n listEl.innerHTML = '';\n var groups = {};\n items.forEach(function (it) {\n var cat = (it.category && it.category[0]) || 'docs';\n (groups[cat] = groups[cat] || []).push(it);\n });\n Object.keys(groups).forEach(function (cat) {\n var h = document.createElement('div'); h.className = 'cat'; h.textContent = cat; listEl.appendChild(h);\n groups[cat].forEach(function (it) {\n var el = document.createElement('div'); el.className = 'item';\n el.innerHTML = '<div class=\"t\">' + esc(it.title) + '</div><div class=\"d\">' + esc(it.description) + '</div>';\n el.onclick = function () { openDoc(it.id); };\n listEl.appendChild(el);\n });\n });\n }\n\n function renderRelated(related) {\n if (!related || !related.length) { return ''; }\n var items = related.map(function (r) {\n var req = r.required ? '<span class=\"req\">must-read</span>' : '';\n var note = r.reason || r.description || '';\n var d = note ? '<div class=\"d\">' + esc(note) + '</div>' : '';\n return '<div class=\"rel\"><a href=\"#' + esc(r.id) + '\" data-id=\"' + esc(r.id) + '\">' + esc(r.title) + '</a>' + req + d + '</div>';\n }).join('');\n return '<div class=\"related\"><h2>Related</h2>' + items + '</div>';\n }\n\n function openDoc(id) {\n fetch('/api/doc/' + encodeURIComponent(id)).then(function (r) { return r.json(); }).then(function (doc) {\n if (!doc || doc.error) { contentEl.innerHTML = '<p>Not found.</p>'; return; }\n var tags = (doc.tags || []).map(function (t) { return '<span class=\"tag\">' + esc(t) + '</span>'; }).join('');\n var src = doc.source ? '<div class=\"src\">' + esc(doc.source.file) + ':' + esc(String(doc.source.lineStart)) + '</div>' : '';\n contentEl.innerHTML = '<h1>' + esc(doc.title) + '</h1>' + src + tags + doc.html + renderRelated(doc.related);\n Array.prototype.forEach.call(contentEl.querySelectorAll('.rel a'), function (a) {\n a.onclick = function (e) { e.preventDefault(); openDoc(a.getAttribute('data-id')); };\n });\n contentEl.scrollTop = 0;\n });\n }\n\n function loadAll() { fetch('/api/docs').then(function (r) { return r.json(); }).then(function (d) { renderList(d.docs || []); }); }\n\n var timer;\n qEl.addEventListener('input', function () {\n clearTimeout(timer);\n var q = qEl.value.trim();\n timer = setTimeout(function () {\n if (!q) { loadAll(); return; }\n fetch('/api/search?q=' + encodeURIComponent(q)).then(function (r) { return r.json(); }).then(function (d) { renderList(d.hits || []); });\n }, 150);\n });\n\n loadAll();\n</script>\n</body>\n</html>";
|
|
11
|
+
//#endregion
|
|
12
|
+
export { PAGE_HTML, renderMarkdown, stripLeadingHeading };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { stripLeadingHeading } from "../utils.js";
|
|
2
|
+
import hljs from "highlight.js";
|
|
3
|
+
import { Marked } from "marked";
|
|
4
|
+
//#region src/web/render.ts
|
|
5
|
+
const marked = new Marked({ renderer: { code({ text, lang }) {
|
|
6
|
+
const language = lang?.split(/\s+/)[0] ?? "";
|
|
7
|
+
return `<pre><code class="hljs language-${language}">${language && hljs.getLanguage(language) ? hljs.highlight(text, { language }).value : hljs.highlightAuto(text).value}</code></pre>\n`;
|
|
8
|
+
} } });
|
|
9
|
+
/** Render Markdown to HTML, highlighting code blocks with highlight.js (server-side). */
|
|
10
|
+
const renderMarkdown = (content) => marked.parse(content);
|
|
11
|
+
/**
|
|
12
|
+
* The single-page UI shell. Static — it fetches `/api/docs`, `/api/search`, and `/api/doc/:id` at runtime, so search
|
|
13
|
+
* stays server-side (orama hybrid).
|
|
14
|
+
*/
|
|
15
|
+
const PAGE_HTML = `<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>doc0</title>
|
|
21
|
+
<style>
|
|
22
|
+
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --muted:#8b949e; --accent:#58a6ff; }
|
|
23
|
+
* { box-sizing: border-box; }
|
|
24
|
+
body { margin:0; font:14px/1.55 system-ui, sans-serif; color:var(--fg); background:var(--bg); display:flex; height:100vh; }
|
|
25
|
+
aside { width:300px; min-width:300px; border-right:1px solid var(--border); background:var(--panel); display:flex; flex-direction:column; }
|
|
26
|
+
header { padding:12px; border-bottom:1px solid var(--border); }
|
|
27
|
+
header strong { font-size:16px; }
|
|
28
|
+
#q { width:100%; margin-top:8px; padding:8px 10px; border-radius:6px; border:1px solid var(--border); background:var(--bg); color:var(--fg); }
|
|
29
|
+
#list { overflow:auto; padding:8px; }
|
|
30
|
+
.cat { color:var(--muted); text-transform:uppercase; font-size:11px; letter-spacing:.05em; margin:12px 8px 4px; }
|
|
31
|
+
.item { padding:6px 8px; border-radius:6px; cursor:pointer; }
|
|
32
|
+
.item:hover { background:var(--bg); }
|
|
33
|
+
.item .t { font-weight:600; }
|
|
34
|
+
.item .d { color:var(--muted); font-size:12px; }
|
|
35
|
+
main { flex:1; overflow:auto; padding:32px 40px; }
|
|
36
|
+
main :where(h1,h2,h3) { line-height:1.25; }
|
|
37
|
+
main h1:first-child { margin-top:0; }
|
|
38
|
+
.src { color:var(--muted); font-family: ui-monospace, monospace; font-size:12px; margin:-8px 0 14px; }
|
|
39
|
+
main pre { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:14px; overflow:auto; }
|
|
40
|
+
main code { font-family: ui-monospace, monospace; }
|
|
41
|
+
main :not(pre) > code { background:var(--panel); padding:1px 5px; border-radius:4px; }
|
|
42
|
+
a { color:var(--accent); text-decoration:none; }
|
|
43
|
+
.tag { display:inline-block; font-size:11px; color:var(--muted); border:1px solid var(--border); border-radius:10px; padding:0 7px; margin:0 4px 4px 0; }
|
|
44
|
+
.related { margin-top:32px; border-top:1px solid var(--border); padding-top:16px; }
|
|
45
|
+
.related h2 { font-size:13px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin:0 0 10px; }
|
|
46
|
+
.rel { margin:0 0 10px; }
|
|
47
|
+
.rel a { font-weight:600; }
|
|
48
|
+
.rel .d { color:var(--muted); font-size:12px; margin-top:2px; }
|
|
49
|
+
.rel .req { color:#d2a8ff; font-size:11px; margin-left:6px; }
|
|
50
|
+
.hljs-keyword,.hljs-built_in,.hljs-literal { color:#ff7b72; }
|
|
51
|
+
.hljs-string,.hljs-attr { color:#a5d6ff; }
|
|
52
|
+
.hljs-comment { color:var(--muted); font-style:italic; }
|
|
53
|
+
.hljs-number,.hljs-title,.hljs-type { color:#79c0ff; }
|
|
54
|
+
.hljs-function,.hljs-name { color:#d2a8ff; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body>
|
|
58
|
+
<aside>
|
|
59
|
+
<header><strong>doc0</strong><input id="q" placeholder="Search docs…" autocomplete="off" /></header>
|
|
60
|
+
<div id="list"></div>
|
|
61
|
+
</aside>
|
|
62
|
+
<main id="content"><p style="color:var(--muted)">Select a doc on the left, or search.</p></main>
|
|
63
|
+
<script>
|
|
64
|
+
var contentEl = document.getElementById('content');
|
|
65
|
+
var listEl = document.getElementById('list');
|
|
66
|
+
var qEl = document.getElementById('q');
|
|
67
|
+
|
|
68
|
+
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }
|
|
69
|
+
|
|
70
|
+
function renderList(items) {
|
|
71
|
+
listEl.innerHTML = '';
|
|
72
|
+
var groups = {};
|
|
73
|
+
items.forEach(function (it) {
|
|
74
|
+
var cat = (it.category && it.category[0]) || 'docs';
|
|
75
|
+
(groups[cat] = groups[cat] || []).push(it);
|
|
76
|
+
});
|
|
77
|
+
Object.keys(groups).forEach(function (cat) {
|
|
78
|
+
var h = document.createElement('div'); h.className = 'cat'; h.textContent = cat; listEl.appendChild(h);
|
|
79
|
+
groups[cat].forEach(function (it) {
|
|
80
|
+
var el = document.createElement('div'); el.className = 'item';
|
|
81
|
+
el.innerHTML = '<div class="t">' + esc(it.title) + '</div><div class="d">' + esc(it.description) + '</div>';
|
|
82
|
+
el.onclick = function () { openDoc(it.id); };
|
|
83
|
+
listEl.appendChild(el);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderRelated(related) {
|
|
89
|
+
if (!related || !related.length) { return ''; }
|
|
90
|
+
var items = related.map(function (r) {
|
|
91
|
+
var req = r.required ? '<span class="req">must-read</span>' : '';
|
|
92
|
+
var note = r.reason || r.description || '';
|
|
93
|
+
var d = note ? '<div class="d">' + esc(note) + '</div>' : '';
|
|
94
|
+
return '<div class="rel"><a href="#' + esc(r.id) + '" data-id="' + esc(r.id) + '">' + esc(r.title) + '</a>' + req + d + '</div>';
|
|
95
|
+
}).join('');
|
|
96
|
+
return '<div class="related"><h2>Related</h2>' + items + '</div>';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function openDoc(id) {
|
|
100
|
+
fetch('/api/doc/' + encodeURIComponent(id)).then(function (r) { return r.json(); }).then(function (doc) {
|
|
101
|
+
if (!doc || doc.error) { contentEl.innerHTML = '<p>Not found.</p>'; return; }
|
|
102
|
+
var tags = (doc.tags || []).map(function (t) { return '<span class="tag">' + esc(t) + '</span>'; }).join('');
|
|
103
|
+
var src = doc.source ? '<div class="src">' + esc(doc.source.file) + ':' + esc(String(doc.source.lineStart)) + '</div>' : '';
|
|
104
|
+
contentEl.innerHTML = '<h1>' + esc(doc.title) + '</h1>' + src + tags + doc.html + renderRelated(doc.related);
|
|
105
|
+
Array.prototype.forEach.call(contentEl.querySelectorAll('.rel a'), function (a) {
|
|
106
|
+
a.onclick = function (e) { e.preventDefault(); openDoc(a.getAttribute('data-id')); };
|
|
107
|
+
});
|
|
108
|
+
contentEl.scrollTop = 0;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadAll() { fetch('/api/docs').then(function (r) { return r.json(); }).then(function (d) { renderList(d.docs || []); }); }
|
|
113
|
+
|
|
114
|
+
var timer;
|
|
115
|
+
qEl.addEventListener('input', function () {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
var q = qEl.value.trim();
|
|
118
|
+
timer = setTimeout(function () {
|
|
119
|
+
if (!q) { loadAll(); return; }
|
|
120
|
+
fetch('/api/search?q=' + encodeURIComponent(q)).then(function (r) { return r.json(); }).then(function (d) { renderList(d.hits || []); });
|
|
121
|
+
}, 150);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
loadAll();
|
|
125
|
+
<\/script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>`;
|
|
128
|
+
//#endregion
|
|
129
|
+
export { PAGE_HTML, renderMarkdown, stripLeadingHeading };
|
package/package.json
CHANGED
|
@@ -1,6 +1,167 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devp0nt/doc0",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn the docs you already write — JSDoc, comments, and Markdown — into one normalized, navigable, searchable collection: serve it over MCP, a web UI, and generated files.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"documentation",
|
|
7
|
+
"docs",
|
|
8
|
+
"jsdoc",
|
|
9
|
+
"markdown",
|
|
10
|
+
"mcp",
|
|
11
|
+
"code-navigation",
|
|
12
|
+
"knowledge-graph",
|
|
13
|
+
"search",
|
|
14
|
+
"orama",
|
|
15
|
+
"embeddings",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Sergei Dmitriev",
|
|
21
|
+
"url": "https://p0nt.dev"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/devp0nt/doc0#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/devp0nt/doc0.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/devp0nt/doc0/issues"
|
|
30
|
+
},
|
|
31
|
+
"funding": "https://p0nt.dev/support",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"module": "./dist/index.js",
|
|
36
|
+
"bin": {
|
|
37
|
+
"doc0": "./dist/cli.js"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./parsers": {
|
|
45
|
+
"types": "./dist/parsers/index.d.ts",
|
|
46
|
+
"import": "./dist/parsers/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./mcp": {
|
|
49
|
+
"types": "./dist/mcp/index.d.ts",
|
|
50
|
+
"import": "./dist/mcp/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./search": {
|
|
53
|
+
"types": "./dist/search/index.d.ts",
|
|
54
|
+
"import": "./dist/search/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./web": {
|
|
57
|
+
"types": "./dist/web/index.d.ts",
|
|
58
|
+
"import": "./dist/web/index.js"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"typesVersions": {
|
|
62
|
+
"*": {
|
|
63
|
+
"": [
|
|
64
|
+
"./dist/index.d.ts"
|
|
65
|
+
],
|
|
66
|
+
"parsers": [
|
|
67
|
+
"./dist/parsers/index.d.ts"
|
|
68
|
+
],
|
|
69
|
+
"mcp": [
|
|
70
|
+
"./dist/mcp/index.d.ts"
|
|
71
|
+
],
|
|
72
|
+
"search": [
|
|
73
|
+
"./dist/search/index.d.ts"
|
|
74
|
+
],
|
|
75
|
+
"web": [
|
|
76
|
+
"./dist/web/index.d.ts"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"files": [
|
|
81
|
+
"dist/**/*",
|
|
82
|
+
"README.md",
|
|
83
|
+
"LICENSE"
|
|
84
|
+
],
|
|
85
|
+
"scripts": {
|
|
86
|
+
"dev": "tsc-watch --preserveWatchOutput --project tsconfig.build.json",
|
|
87
|
+
"dev:npm": "tsc-watch --preserveWatchOutput --project tsconfig.build.json --onSuccess 'bun run pack:dist'",
|
|
88
|
+
"build": "tsdown",
|
|
89
|
+
"test": "bun test ./src",
|
|
90
|
+
"test:us": "bun test ./src --update-snapshots",
|
|
91
|
+
"test:watch": "bun test --watch",
|
|
92
|
+
"test:coverage": "bun test ./src --coverage",
|
|
93
|
+
"smoke": "node scripts/smoke.mjs",
|
|
94
|
+
"lint": "bun run lint:fix .",
|
|
95
|
+
"lint:base": "eslint --cache --cache-location node_modules/.cache/eslint/.eslintcache",
|
|
96
|
+
"lint:fix": "bun run lint:base --fix",
|
|
97
|
+
"format": "bun run format:fix .",
|
|
98
|
+
"format:base": "prettier --cache --cache-location node_modules/.cache/prettier/.prettiercache --ignore-path .gitignore --ignore-path .prettierignore",
|
|
99
|
+
"format:fix": "bun run format:base --write",
|
|
100
|
+
"types:build": "tsc --noEmit --project tsconfig.build.json",
|
|
101
|
+
"types:dev": "tsc --noEmit",
|
|
102
|
+
"types": "bun run types:dev",
|
|
103
|
+
"typesgo": "tsgo --noEmit",
|
|
104
|
+
"pack:dry": "npm pack --dry-run",
|
|
105
|
+
"pack:dist": "mkdir -p dist-npm && npm pack --pack-destination dist-npm --silent && cd dist-npm && tar -xzf *.tgz && cp -r package/* . && rm -rf package && rm *.tgz",
|
|
106
|
+
"pack:dist:hard": "rimraf dist-npm && bun run pack:dist",
|
|
107
|
+
"prepare": "husky || true",
|
|
108
|
+
"clean": "rimraf dist && rimraf dist-npm && rimraf node_modules/.cache"
|
|
109
|
+
},
|
|
110
|
+
"dependencies": {
|
|
111
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
112
|
+
"@orama/orama": "^3.1.18",
|
|
113
|
+
"@standard-schema/spec": "^1.0.0",
|
|
114
|
+
"chokidar": "^5.0.0",
|
|
115
|
+
"commander": "^15.0.0",
|
|
116
|
+
"comment-parser": "^1.4.7",
|
|
117
|
+
"elysia": "^1.4.0",
|
|
118
|
+
"gray-matter": "^4.0.3",
|
|
119
|
+
"highlight.js": "^11.11.1",
|
|
120
|
+
"marked": "^18.0.0",
|
|
121
|
+
"tinyglobby": "^0.2.16",
|
|
122
|
+
"yaml": "^2.9.0",
|
|
123
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
124
|
+
},
|
|
125
|
+
"peerDependencies": {
|
|
126
|
+
"@huggingface/transformers": "^4.2.0"
|
|
127
|
+
},
|
|
128
|
+
"peerDependenciesMeta": {
|
|
129
|
+
"@huggingface/transformers": {
|
|
130
|
+
"optional": true
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"devDependencies": {
|
|
134
|
+
"@commitlint/cli": "^19.8.1",
|
|
135
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
136
|
+
"@huggingface/transformers": "^4.2.0",
|
|
137
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
138
|
+
"@semantic-release/git": "^10.0.1",
|
|
139
|
+
"@semantic-release/github": "^12.0.2",
|
|
140
|
+
"@semantic-release/npm": "^13.1.3",
|
|
141
|
+
"@types/bun": "^1.2.22",
|
|
142
|
+
"@types/node": "^20.0.0",
|
|
143
|
+
"@typescript/native-preview": "^7.0.0-dev.20260219.1",
|
|
144
|
+
"cross-env": "^10.0.0",
|
|
145
|
+
"eslint": "^10.0.0",
|
|
146
|
+
"eslint-config-prettier": "^10.1.8",
|
|
147
|
+
"eslint-plugin-no-only-tests": "^3.3.0",
|
|
148
|
+
"husky": "^9.1.7",
|
|
149
|
+
"lint-staged": "^16.2.1",
|
|
150
|
+
"prettier": "^3.6.2",
|
|
151
|
+
"prettier-plugin-jsdoc": "^1.8.0",
|
|
152
|
+
"rimraf": "^6.0.1",
|
|
153
|
+
"semantic-release": "^25.0.2",
|
|
154
|
+
"tsc-watch": "^7.1.1",
|
|
155
|
+
"tsdown": "^0.22.0",
|
|
156
|
+
"typescript": "^5.9.3",
|
|
157
|
+
"typescript-eslint": "^8.56.0"
|
|
158
|
+
},
|
|
159
|
+
"engines": {
|
|
160
|
+
"node": ">=20.0.0",
|
|
161
|
+
"bun": ">=1.0.0"
|
|
162
|
+
},
|
|
163
|
+
"publishConfig": {
|
|
164
|
+
"access": "public",
|
|
165
|
+
"provenance": true
|
|
166
|
+
}
|
|
6
167
|
}
|