living-ai-documentation 1.0.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 +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +157 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +206 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +196 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- package/package.json +72 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// ββ Export modal (Markdown / HTML / PDF) + exportAllPDF ββββββββββββββββββββ
|
|
2
|
+
// Depends on globals from state.js (allDocs) and utils.js (esc).
|
|
3
|
+
|
|
4
|
+
async function exportAllPDF() {
|
|
5
|
+
const btn = document.getElementById("export-all-pdf-btn");
|
|
6
|
+
const originalHtml = btn.innerHTML;
|
|
7
|
+
btn.innerHTML = "β³";
|
|
8
|
+
btn.disabled = true;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const docs =
|
|
12
|
+
allDocs && allDocs.length
|
|
13
|
+
? allDocs
|
|
14
|
+
: await fetch("/api/documents").then((r) => r.json());
|
|
15
|
+
|
|
16
|
+
// Batch-fetch full HTML content (5 at a time), keyed by id
|
|
17
|
+
const htmlById = {};
|
|
18
|
+
const batchSize = 5;
|
|
19
|
+
for (let i = 0; i < docs.length; i += batchSize) {
|
|
20
|
+
const batch = docs.slice(i, i + batchSize);
|
|
21
|
+
const results = await Promise.all(
|
|
22
|
+
batch.map((d) =>
|
|
23
|
+
fetch("/api/documents/" + d.id)
|
|
24
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
25
|
+
.catch(() => null),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
for (const r of results) {
|
|
29
|
+
if (r) htmlById[r.id ?? docs[i]?.id] = r;
|
|
30
|
+
}
|
|
31
|
+
// re-key by id using the batch metadata
|
|
32
|
+
batch.forEach((d, j) => {
|
|
33
|
+
if (results[j]) htmlById[d.id] = results[j];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build folder tree identical to sidebar's buildFolderTree
|
|
38
|
+
function buildTree(list) {
|
|
39
|
+
const root = { categories: {}, children: {} };
|
|
40
|
+
for (const doc of list) {
|
|
41
|
+
let node = root;
|
|
42
|
+
for (const seg of doc.folder || []) {
|
|
43
|
+
if (!node.children[seg])
|
|
44
|
+
node.children[seg] = { categories: {}, children: {} };
|
|
45
|
+
node = node.children[seg];
|
|
46
|
+
}
|
|
47
|
+
if (!node.categories[doc.category])
|
|
48
|
+
node.categories[doc.category] = [];
|
|
49
|
+
node.categories[doc.category].push(doc);
|
|
50
|
+
}
|
|
51
|
+
return root;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Traverse in sidebar order: General β subfolders (alpha) β other cats (alpha)
|
|
55
|
+
function orderedDocs(node) {
|
|
56
|
+
const result = [];
|
|
57
|
+
if (node.categories["General"])
|
|
58
|
+
result.push(...node.categories["General"]);
|
|
59
|
+
const childKeys = Object.keys(node.children).sort((a, b) =>
|
|
60
|
+
a.localeCompare(b),
|
|
61
|
+
);
|
|
62
|
+
for (const key of childKeys)
|
|
63
|
+
result.push(...orderedDocs(node.children[key]));
|
|
64
|
+
const otherCats = Object.keys(node.categories)
|
|
65
|
+
.filter((c) => c !== "General")
|
|
66
|
+
.sort((a, b) => a.localeCompare(b));
|
|
67
|
+
for (const cat of otherCats) result.push(...node.categories[cat]);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Strip leading numeric prefix, same as folderLabel()
|
|
72
|
+
function fLabel(seg) {
|
|
73
|
+
return seg
|
|
74
|
+
.replace(/^\d+_/, "")
|
|
75
|
+
.replace(/[_-]+/g, " ")
|
|
76
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build TOC HTML mirroring sidebar structure
|
|
80
|
+
function buildTocNode(node, depth) {
|
|
81
|
+
let html = "";
|
|
82
|
+
const indent = depth * 1.25;
|
|
83
|
+
const renderCatItems = (cat, catDocs) => {
|
|
84
|
+
const catLabel =
|
|
85
|
+
depth === 0 && cat === "General" ? "General" : cat;
|
|
86
|
+
html += `<li style="margin-left:${indent}rem;margin-top:${depth === 0 ? "0.75" : "0.4"}rem;">`;
|
|
87
|
+
html += `<span style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#6b7280;">${catLabel}</span>`;
|
|
88
|
+
html += `<ul style="margin:0.2rem 0 0 0;padding:0;list-style:none;">`;
|
|
89
|
+
for (const doc of catDocs) {
|
|
90
|
+
html += `<li style="margin-left:${indent + 0.75}rem;padding:0.15rem 0;">`;
|
|
91
|
+
html += `<a href="#doc-${doc.id}" style="color:#1d4ed8;text-decoration:none;font-size:0.875rem;">${esc(doc.title)}</a>`;
|
|
92
|
+
if (doc.formattedDate)
|
|
93
|
+
html += `<span style="color:#9ca3af;font-size:0.7rem;margin-left:0.5rem;">${esc(doc.formattedDate)}</span>`;
|
|
94
|
+
html += `</li>`;
|
|
95
|
+
}
|
|
96
|
+
html += `</ul></li>`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (node.categories["General"])
|
|
100
|
+
renderCatItems("General", node.categories["General"]);
|
|
101
|
+
|
|
102
|
+
const childKeys = Object.keys(node.children).sort((a, b) =>
|
|
103
|
+
a.localeCompare(b),
|
|
104
|
+
);
|
|
105
|
+
for (const key of childKeys) {
|
|
106
|
+
html += `<li style="margin-left:${indent}rem;margin-top:0.75rem;">`;
|
|
107
|
+
html += `<span style="font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#7c3aed;">📁 ${fLabel(key)}</span>`;
|
|
108
|
+
html += `<ul style="margin:0.2rem 0 0 0;padding:0;list-style:none;">`;
|
|
109
|
+
html += buildTocNode(node.children[key], depth + 1);
|
|
110
|
+
html += `</ul></li>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const otherCats = Object.keys(node.categories)
|
|
114
|
+
.filter((c) => c !== "General")
|
|
115
|
+
.sort((a, b) => a.localeCompare(b));
|
|
116
|
+
for (const cat of otherCats)
|
|
117
|
+
renderCatItems(cat, node.categories[cat]);
|
|
118
|
+
|
|
119
|
+
return html;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Rewrite ?doc=X links to internal anchors #doc-X
|
|
123
|
+
function rewriteDocLinks(html) {
|
|
124
|
+
return html.replace(
|
|
125
|
+
/href="[^"]*\?doc=([^"&#]+)(?:#[^"]*)?"/g,
|
|
126
|
+
(match, docId) => `href="#doc-${docId}"`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const appTitle = esc(document.title || "Living Documentation");
|
|
131
|
+
const tree = buildTree(docs);
|
|
132
|
+
const ordered = orderedDocs(tree);
|
|
133
|
+
|
|
134
|
+
const tocHtml = `
|
|
135
|
+
<div style="page-break-after:always;">
|
|
136
|
+
<h2 style="font-size:1.25rem;font-weight:700;color:#111827;margin-bottom:1.5rem;padding-bottom:0.5rem;border-bottom:2px solid #e5e7eb;">${window.t('pdf.toc_title')}</h2>
|
|
137
|
+
<ul style="list-style:none;padding:0;margin:0;">
|
|
138
|
+
${buildTocNode(tree, 0)}
|
|
139
|
+
</ul>
|
|
140
|
+
</div>`;
|
|
141
|
+
|
|
142
|
+
const sectionsHtml = ordered
|
|
143
|
+
.map((doc, idx) => {
|
|
144
|
+
const full = htmlById[doc.id];
|
|
145
|
+
if (!full) return "";
|
|
146
|
+
const html = rewriteDocLinks(full.html);
|
|
147
|
+
const folderPath = Array.isArray(doc.folder)
|
|
148
|
+
? doc.folder.map((f) => esc(fLabel(f))).join(" βΊ ") + " βΊ "
|
|
149
|
+
: "";
|
|
150
|
+
const meta = `${folderPath}${esc(doc.category)}${doc.formattedDate ? " Β· " + esc(doc.formattedDate) : ""}`;
|
|
151
|
+
const pageBreak =
|
|
152
|
+
idx > 0
|
|
153
|
+
? '<div style="page-break-before:always;height:0;"></div>'
|
|
154
|
+
: "";
|
|
155
|
+
return `${pageBreak}
|
|
156
|
+
<section id="doc-${doc.id}" style="padding:2rem 0;">
|
|
157
|
+
<div style="border-bottom:2px solid #e5e7eb;padding-bottom:0.75rem;margin-bottom:1.5rem;">
|
|
158
|
+
<div style="font-size:0.7rem;color:#6b7280;margin-bottom:0.25rem;">${meta}</div>
|
|
159
|
+
<h1 style="font-size:1.75rem;font-weight:800;color:#111827;margin:0;">${esc(doc.title)}</h1>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="prose max-w-none">${html}</div>
|
|
162
|
+
</section>`;
|
|
163
|
+
})
|
|
164
|
+
.join("\n");
|
|
165
|
+
|
|
166
|
+
const fullHtml = `<!DOCTYPE html>
|
|
167
|
+
<html>
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="UTF-8">
|
|
170
|
+
<title>${appTitle} β Export PDF</title>
|
|
171
|
+
<script src="https://cdn.tailwindcss.com?plugins=typography"><\/script>
|
|
172
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
173
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"><\/script>
|
|
174
|
+
<style>
|
|
175
|
+
body { font-family: system-ui,sans-serif; max-width: 860px; margin: 0 auto; padding: 2rem; color: #111827; background: #fff; }
|
|
176
|
+
.prose pre { border-radius: 0.5rem; }
|
|
177
|
+
.prose img { max-width: 100%; }
|
|
178
|
+
@media print {
|
|
179
|
+
body { padding: 0; }
|
|
180
|
+
a { color: #1d4ed8; text-decoration: underline; }
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<div style="text-align:center;margin-bottom:3rem;padding-bottom:2rem;border-bottom:3px solid #e5e7eb;page-break-after:avoid;">
|
|
186
|
+
<h1 style="font-size:2rem;font-weight:900;color:#111827;margin:0;">${appTitle}</h1>
|
|
187
|
+
</div>
|
|
188
|
+
${tocHtml}
|
|
189
|
+
${sectionsHtml}
|
|
190
|
+
<script>
|
|
191
|
+
document.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
|
192
|
+
setTimeout(() => window.print(), 800);
|
|
193
|
+
<\/script>
|
|
194
|
+
</body>
|
|
195
|
+
</html>`;
|
|
196
|
+
|
|
197
|
+
const w = window.open("", "_blank");
|
|
198
|
+
if (!w) {
|
|
199
|
+
alert(window.t('error.pdf_popup'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
w.document.write(fullHtml);
|
|
203
|
+
w.document.close();
|
|
204
|
+
} catch (err) {
|
|
205
|
+
alert(window.t('error.pdf_export') + err.message);
|
|
206
|
+
} finally {
|
|
207
|
+
btn.innerHTML = originalHtml;
|
|
208
|
+
btn.disabled = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ββ Export modal (tabs + download actions) ββββββββββββββββββββββββββββββββββ
|
|
213
|
+
let _exportCurrentTab = 'markdown';
|
|
214
|
+
|
|
215
|
+
function switchExportTab(tab) {
|
|
216
|
+
_exportCurrentTab = tab;
|
|
217
|
+
['markdown', 'html', 'pdf'].forEach((t) => {
|
|
218
|
+
const tabBtn = document.getElementById(`export-tab-${t}`);
|
|
219
|
+
const content = document.getElementById(`export-content-${t}`);
|
|
220
|
+
const isActive = t === tab;
|
|
221
|
+
tabBtn.classList.toggle('border-b-2', isActive);
|
|
222
|
+
tabBtn.classList.toggle('border-blue-500', isActive);
|
|
223
|
+
tabBtn.classList.toggle('text-blue-600', isActive);
|
|
224
|
+
tabBtn.classList.toggle('dark:text-blue-400', isActive);
|
|
225
|
+
tabBtn.classList.toggle('font-semibold', isActive);
|
|
226
|
+
tabBtn.classList.toggle('text-gray-500', !isActive);
|
|
227
|
+
tabBtn.classList.toggle('dark:text-gray-400', !isActive);
|
|
228
|
+
content.classList.toggle('hidden', !isActive);
|
|
229
|
+
});
|
|
230
|
+
// Folder list: visible for html only (lazy-loaded on first switch)
|
|
231
|
+
document.getElementById('export-folder-section').classList.toggle('hidden', tab !== 'html');
|
|
232
|
+
if (tab === 'html') loadExportFolders();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let _exportFoldersLoaded = false;
|
|
236
|
+
|
|
237
|
+
async function loadExportFolders() {
|
|
238
|
+
if (_exportFoldersLoaded) return;
|
|
239
|
+
const list = document.getElementById('export-folder-list');
|
|
240
|
+
list.innerHTML = `<p class="text-sm text-gray-400 py-2">${window.t('common.loading')}</p>`;
|
|
241
|
+
try {
|
|
242
|
+
const docs = await fetch('/api/documents').then((r) => r.json());
|
|
243
|
+
const groups = new Set();
|
|
244
|
+
docs.forEach((doc) => groups.add(doc.folder?.[0] ?? doc.category ?? 'General'));
|
|
245
|
+
const sorted = [...groups].sort((a, b) => {
|
|
246
|
+
if (a === 'General') return -1;
|
|
247
|
+
if (b === 'General') return 1;
|
|
248
|
+
return a.localeCompare(b);
|
|
249
|
+
});
|
|
250
|
+
list.innerHTML = sorted.map((g) => `
|
|
251
|
+
<label class="flex items-center gap-3 px-1 py-1.5 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
|
252
|
+
<input type="checkbox" value="${g}"
|
|
253
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
254
|
+
<span class="text-sm text-gray-700 dark:text-gray-300">${g}</span>
|
|
255
|
+
</label>`).join('');
|
|
256
|
+
_exportFoldersLoaded = true;
|
|
257
|
+
} catch {
|
|
258
|
+
list.innerHTML = `<p class="text-sm text-red-500">${window.t('common.error_prefix')}failed to load folders</p>`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function openExportModal() {
|
|
263
|
+
document.getElementById('export-modal').classList.remove('hidden');
|
|
264
|
+
switchExportTab('markdown');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function closeExportModal() {
|
|
268
|
+
document.getElementById('export-modal').classList.add('hidden');
|
|
269
|
+
_exportFoldersLoaded = false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function exportMarkdown() {
|
|
273
|
+
const btn = document.getElementById('export-md-btn');
|
|
274
|
+
const orig = btn.innerHTML;
|
|
275
|
+
btn.innerHTML = 'β³';
|
|
276
|
+
btn.disabled = true;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const res = await fetch('/api/export/markdown', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: '{}',
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) throw new Error(await res.text());
|
|
285
|
+
const blob = await res.blob();
|
|
286
|
+
const url = URL.createObjectURL(blob);
|
|
287
|
+
const a = document.createElement('a');
|
|
288
|
+
a.href = url;
|
|
289
|
+
a.download = 'export-markdown.zip';
|
|
290
|
+
a.click();
|
|
291
|
+
URL.revokeObjectURL(url);
|
|
292
|
+
closeExportModal();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
alert(window.t('common.error_prefix') + err.message);
|
|
295
|
+
} finally {
|
|
296
|
+
btn.innerHTML = orig;
|
|
297
|
+
btn.disabled = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function exportHtml(mode) {
|
|
302
|
+
const checked = [...document.querySelectorAll('#export-folder-list input:checked')];
|
|
303
|
+
const folders = checked.map((cb) => cb.value);
|
|
304
|
+
if (!folders.length) return;
|
|
305
|
+
|
|
306
|
+
const btnId = mode === 'confluence' ? 'export-confluence-btn' : 'export-notion-btn';
|
|
307
|
+
const btn = document.getElementById(btnId);
|
|
308
|
+
const orig = btn.innerHTML;
|
|
309
|
+
btn.innerHTML = 'β³';
|
|
310
|
+
btn.disabled = true;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch('/api/export/html', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({ folders, mode }),
|
|
317
|
+
});
|
|
318
|
+
if (!res.ok) throw new Error(await res.text());
|
|
319
|
+
const blob = await res.blob();
|
|
320
|
+
const url = URL.createObjectURL(blob);
|
|
321
|
+
const a = document.createElement('a');
|
|
322
|
+
a.href = url;
|
|
323
|
+
a.download = `export-${mode}.zip`;
|
|
324
|
+
a.click();
|
|
325
|
+
URL.revokeObjectURL(url);
|
|
326
|
+
closeExportModal();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
alert(window.t('common.error_prefix') + err.message);
|
|
329
|
+
} finally {
|
|
330
|
+
btn.innerHTML = orig;
|
|
331
|
+
btn.disabled = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function exportAllPdfFromModal() {
|
|
336
|
+
closeExportModal();
|
|
337
|
+
exportAllPDF();
|
|
338
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ββ File attachments ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
2
|
+
// Attach arbitrary files (pdf, docx, zip, β¦) to a document via drag & drop,
|
|
3
|
+
// paste or the Snippets panel. Files are saved under DOCS_FOLDER/files/ and
|
|
4
|
+
// inserted as `[π name.ext](./files/<server-name>.<ext>)` in the markdown.
|
|
5
|
+
//
|
|
6
|
+
// Client-side size guard matches server-side MAX_FILE_BYTES (19 MB) so the
|
|
7
|
+
// user gets an immediate toast when Express would otherwise reject the body.
|
|
8
|
+
|
|
9
|
+
const FILE_MAX_BYTES = 19 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function _fileAttachNotify(message, isError) {
|
|
12
|
+
const msgEl = document.getElementById("edit-save-msg");
|
|
13
|
+
if (msgEl) {
|
|
14
|
+
msgEl.textContent = message;
|
|
15
|
+
msgEl.className = isError
|
|
16
|
+
? "text-xs text-red-500 dark:text-red-400"
|
|
17
|
+
: "text-xs text-gray-400";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _fileAttachReadAsBase64(file) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const reader = new FileReader();
|
|
24
|
+
reader.onload = () => resolve(reader.result);
|
|
25
|
+
reader.onerror = reject;
|
|
26
|
+
reader.readAsDataURL(file);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _fileAttachInsertAtCursor(markdown, start, end) {
|
|
31
|
+
const editor = document.getElementById("doc-editor");
|
|
32
|
+
if (!editor) return;
|
|
33
|
+
const s = typeof start === "number" ? start : editor.selectionStart;
|
|
34
|
+
const e = typeof end === "number" ? end : editor.selectionEnd;
|
|
35
|
+
const before = editor.value.slice(0, s);
|
|
36
|
+
const after = editor.value.slice(e);
|
|
37
|
+
editor.value = before + markdown + after;
|
|
38
|
+
editor.selectionStart = editor.selectionEnd = s + markdown.length;
|
|
39
|
+
editor.focus();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function uploadAttachedFile(file, cursorStart, cursorEnd) {
|
|
43
|
+
if (!file) return;
|
|
44
|
+
|
|
45
|
+
if (file.size > FILE_MAX_BYTES) {
|
|
46
|
+
const mb = (file.size / 1024 / 1024).toFixed(1);
|
|
47
|
+
_fileAttachNotify(
|
|
48
|
+
(window.t ? window.t("doc.file_too_large") : "File too large") +
|
|
49
|
+
` (${mb} MB, max 19 MB)`,
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_fileAttachNotify(
|
|
56
|
+
window.t ? window.t("doc.uploading_file") : "Uploading fileβ¦",
|
|
57
|
+
false,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const base64 = await _fileAttachReadAsBase64(file);
|
|
62
|
+
const res = await fetch("/api/files/upload", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ data: base64, name: file.name }),
|
|
66
|
+
});
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
let msg = "";
|
|
69
|
+
try {
|
|
70
|
+
const body = await res.json();
|
|
71
|
+
msg = body.error || "";
|
|
72
|
+
} catch {
|
|
73
|
+
msg = await res.text();
|
|
74
|
+
}
|
|
75
|
+
throw new Error(msg || `HTTP ${res.status}`);
|
|
76
|
+
}
|
|
77
|
+
const { url, originalName } = await res.json();
|
|
78
|
+
const label = originalName || file.name;
|
|
79
|
+
const markdown = `[π ${label}](${url})`;
|
|
80
|
+
_fileAttachInsertAtCursor(markdown, cursorStart, cursorEnd);
|
|
81
|
+
_fileAttachNotify("", false);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
_fileAttachNotify(
|
|
84
|
+
(window.t ? window.t("doc.file_upload_failed") : "File upload failed: ") +
|
|
85
|
+
(err.message || String(err)),
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _fileAttachHandleDrop(e) {
|
|
92
|
+
const editor = document.getElementById("doc-editor");
|
|
93
|
+
if (!editor || editor.classList.contains("hidden")) return;
|
|
94
|
+
const files = Array.from(e.dataTransfer?.files ?? []);
|
|
95
|
+
if (files.length === 0) return;
|
|
96
|
+
|
|
97
|
+
// Images keep flowing through image-paste.js β ignore them here.
|
|
98
|
+
const nonImage = files.filter((f) => !f.type.startsWith("image/"));
|
|
99
|
+
if (nonImage.length === 0) return;
|
|
100
|
+
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
|
|
104
|
+
// Drop uses the current caret, not the drop coordinates β keeps things simple.
|
|
105
|
+
const start = editor.selectionStart;
|
|
106
|
+
const end = editor.selectionEnd;
|
|
107
|
+
(async () => {
|
|
108
|
+
let offset = 0;
|
|
109
|
+
for (const file of nonImage) {
|
|
110
|
+
await uploadAttachedFile(file, start + offset, end + offset);
|
|
111
|
+
// After insertion the caret is positioned AFTER the inserted markdown;
|
|
112
|
+
// re-read it for the next file so multiple drops append correctly.
|
|
113
|
+
offset = 0;
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _fileAttachHandlePaste(e) {
|
|
119
|
+
const editor = document.getElementById("doc-editor");
|
|
120
|
+
if (!editor) return;
|
|
121
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
122
|
+
if (files.length === 0) return;
|
|
123
|
+
const nonImage = files.filter((f) => !f.type.startsWith("image/"));
|
|
124
|
+
if (nonImage.length === 0) return;
|
|
125
|
+
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
const start = editor.selectionStart;
|
|
128
|
+
const end = editor.selectionEnd;
|
|
129
|
+
(async () => {
|
|
130
|
+
for (const file of nonImage) {
|
|
131
|
+
await uploadAttachedFile(file, start, end);
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _fileAttachHandleDragOver(e) {
|
|
137
|
+
const editor = document.getElementById("doc-editor");
|
|
138
|
+
if (!editor || editor.classList.contains("hidden")) return;
|
|
139
|
+
if (e.dataTransfer?.types?.includes("Files")) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
e.dataTransfer.dropEffect = "copy";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function initFileAttach() {
|
|
146
|
+
const editor = document.getElementById("doc-editor");
|
|
147
|
+
if (!editor) return;
|
|
148
|
+
editor.addEventListener("drop", _fileAttachHandleDrop);
|
|
149
|
+
editor.addEventListener("dragover", _fileAttachHandleDragOver);
|
|
150
|
+
editor.addEventListener("paste", _fileAttachHandlePaste);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Triggered by the π snippet button β opens the native file picker.
|
|
154
|
+
function openFilePicker() {
|
|
155
|
+
const editor = document.getElementById("doc-editor");
|
|
156
|
+
if (!editor) return;
|
|
157
|
+
const start = editor.selectionStart;
|
|
158
|
+
const end = editor.selectionEnd;
|
|
159
|
+
|
|
160
|
+
let input = document.getElementById("file-attach-picker");
|
|
161
|
+
if (!input) {
|
|
162
|
+
input = document.createElement("input");
|
|
163
|
+
input.type = "file";
|
|
164
|
+
input.id = "file-attach-picker";
|
|
165
|
+
input.style.display = "none";
|
|
166
|
+
document.body.appendChild(input);
|
|
167
|
+
}
|
|
168
|
+
input.value = "";
|
|
169
|
+
input.onchange = async () => {
|
|
170
|
+
const file = input.files && input.files[0];
|
|
171
|
+
if (file) await uploadAttachedFile(file, start, end);
|
|
172
|
+
};
|
|
173
|
+
input.click();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
window.initFileAttach = initFileAttach;
|
|
177
|
+
window.openFilePicker = openFilePicker;
|
|
178
|
+
window.uploadAttachedFile = uploadAttachedFile;
|