explicai 0.0.0-stage → 0.2.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/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +51 -0
- package/dist/core/annotations.d.ts +7 -0
- package/dist/core/annotations.js +110 -0
- package/dist/core/assets.d.ts +17 -0
- package/dist/core/assets.js +116 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/export/annotationCss.d.ts +5 -0
- package/dist/core/export/annotationCss.js +26 -0
- package/dist/core/export/annotationShapes.d.ts +37 -0
- package/dist/core/export/annotationShapes.js +38 -0
- package/dist/core/export/callout.d.ts +11 -0
- package/dist/core/export/callout.js +26 -0
- package/dist/core/export/collect.d.ts +27 -0
- package/dist/core/export/collect.js +49 -0
- package/dist/core/export/color.d.ts +11 -0
- package/dist/core/export/color.js +33 -0
- package/dist/core/export/fonts.d.ts +35 -0
- package/dist/core/export/fonts.js +40 -0
- package/dist/core/export/headings.d.ts +7 -0
- package/dist/core/export/headings.js +17 -0
- package/dist/core/export/html.d.ts +4 -0
- package/dist/core/export/html.js +242 -0
- package/dist/core/export/imageSize.d.ts +9 -0
- package/dist/core/export/imageSize.js +72 -0
- package/dist/core/export/index.d.ts +29 -0
- package/dist/core/export/index.js +151 -0
- package/dist/core/export/labels.d.ts +17 -0
- package/dist/core/export/labels.js +31 -0
- package/dist/core/export/logo.d.ts +10 -0
- package/dist/core/export/logo.js +29 -0
- package/dist/core/export/markdown.d.ts +8 -0
- package/dist/core/export/markdown.js +83 -0
- package/dist/core/export/pdf.d.ts +6 -0
- package/dist/core/export/pdf.js +68 -0
- package/dist/core/export/refs.d.ts +24 -0
- package/dist/core/export/refs.js +66 -0
- package/dist/core/export/textColors.d.ts +7 -0
- package/dist/core/export/textColors.js +18 -0
- package/dist/core/export/theme.d.ts +28 -0
- package/dist/core/export/theme.js +140 -0
- package/dist/core/frontmatter.d.ts +5 -0
- package/dist/core/frontmatter.js +17 -0
- package/dist/core/fs.d.ts +13 -0
- package/dist/core/fs.js +45 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +15 -0
- package/dist/core/manuals.d.ts +23 -0
- package/dist/core/manuals.js +86 -0
- package/dist/core/pages.d.ts +43 -0
- package/dist/core/pages.js +179 -0
- package/dist/core/redact.d.ts +9 -0
- package/dist/core/redact.js +107 -0
- package/dist/core/text.d.ts +5 -0
- package/dist/core/text.js +28 -0
- package/dist/core/trash.d.ts +25 -0
- package/dist/core/trash.js +84 -0
- package/dist/core/tree.d.ts +13 -0
- package/dist/core/tree.js +28 -0
- package/dist/core/types.d.ts +111 -0
- package/dist/core/types.js +7 -0
- package/dist/core/versions.d.ts +27 -0
- package/dist/core/versions.js +82 -0
- package/dist/server/errorPage.d.ts +9 -0
- package/dist/server/errorPage.js +122 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +132 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +60 -0
- package/dist/server/router.d.ts +30 -0
- package/dist/server/router.js +28 -0
- package/dist/server/routes/assets.d.ts +2 -0
- package/dist/server/routes/assets.js +63 -0
- package/dist/server/routes/export.d.ts +2 -0
- package/dist/server/routes/export.js +43 -0
- package/dist/server/routes/fonts.d.ts +3 -0
- package/dist/server/routes/fonts.js +24 -0
- package/dist/server/routes/manuals.d.ts +2 -0
- package/dist/server/routes/manuals.js +40 -0
- package/dist/server/routes/pages.d.ts +2 -0
- package/dist/server/routes/pages.js +79 -0
- package/dist/server/routes/versions.d.ts +2 -0
- package/dist/server/routes/versions.js +43 -0
- package/dist/server/routes/web.d.ts +3 -0
- package/dist/server/routes/web.js +29 -0
- package/dist/server/security.d.ts +6 -0
- package/dist/server/security.js +17 -0
- package/dist/server/workspace.d.ts +5 -0
- package/dist/server/workspace.js +17 -0
- package/dist/web/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
- package/dist/web/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
- package/dist/web/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-400-normal-DPp7N0cD.woff +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-400-normal-FfmJh7DR.woff2 +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-700-normal-C2qZD_Q9.woff +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-700-normal-Dpiyiu63.woff2 +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-400-normal-BalHKn7d.woff2 +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-400-normal-qxMiU51n.woff +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-700-normal-292QVhS1.woff +0 -0
- package/dist/web/assets/atkinson-hyperlegible-next-latin-ext-700-normal-CBw-mJf_.woff2 +0 -0
- package/dist/web/assets/index-B7Gr83D6.js +262 -0
- package/dist/web/assets/index-DFBrhkII.css +1 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +23 -0
- package/fonts/OFL-archivo.txt +93 -0
- package/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- package/fonts/OFL-inter.txt +93 -0
- package/fonts/OFL-jetbrains-mono.txt +93 -0
- package/fonts/OFL-lora.txt +93 -0
- package/fonts/OFL-montserrat.txt +93 -0
- package/fonts/OFL-playfair-display.txt +93 -0
- package/fonts/OFL-source-sans-3.txt +93 -0
- package/fonts/OFL-source-serif-4.txt +93 -0
- package/fonts/archivo-latin-wght-normal.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-400-italic.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-400-normal.woff2 +0 -0
- package/fonts/atkinson-hyperlegible-next-latin-700-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/fonts/lora-latin-wght-italic.woff2 +0 -0
- package/fonts/lora-latin-wght-normal.woff2 +0 -0
- package/fonts/montserrat-latin-wght-normal.woff2 +0 -0
- package/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/fonts/source-serif-4-latin-wght-italic.woff2 +0 -0
- package/fonts/source-serif-4-latin-wght-normal.woff2 +0 -0
- package/package.json +124 -4
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ExplicaiError } from "../errors.js";
|
|
2
|
+
import { HEADING_SLOTS } from "../types.js";
|
|
3
|
+
import { contrast, contrastWithWhite, HEX, muted, overWhite, withAlpha } from "./color.js";
|
|
4
|
+
import { DEFAULT_HEADINGS_FONT, DEFAULT_TEXT_FONT, FONTS, fontById, fontFaceCss, fontStack, MONO, } from "./fonts.js";
|
|
5
|
+
import { checkLogoPath } from "./logo.js";
|
|
6
|
+
/** Contraste mínimo do texto com o fundo (WCAG AA). */
|
|
7
|
+
export const MIN_CONTRAST = 4.5;
|
|
8
|
+
/** Valores de hoje: o manual sem `theme` exporta com estes. */
|
|
9
|
+
export const DEFAULT_TEXT = "#151a33";
|
|
10
|
+
export const DEFAULT_ACCENT = "#2b4fe0";
|
|
11
|
+
export const DEFAULT_HIGHLIGHT = "#ffd43b";
|
|
12
|
+
/** Opacidade do marca-texto sobre o papel (a mesma do CSS do export). */
|
|
13
|
+
const HIGHLIGHT_ALPHA = 0.55;
|
|
14
|
+
const SLOT_NAMES = {
|
|
15
|
+
cover: "título da capa",
|
|
16
|
+
page: "título da página",
|
|
17
|
+
h1: "título de nível 1",
|
|
18
|
+
h2: "título de nível 2",
|
|
19
|
+
h3: "título de nível 3",
|
|
20
|
+
};
|
|
21
|
+
const num = (n) => n.toFixed(1).replace(".", ",");
|
|
22
|
+
function hexOf(value, label) {
|
|
23
|
+
if (value === null)
|
|
24
|
+
return null;
|
|
25
|
+
if (typeof value !== "string" || !HEX.test(value))
|
|
26
|
+
throw new Error(`Cor inválida em ${label}: use o formato #2b4fe0.`);
|
|
27
|
+
return value.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function readable(hex, label) {
|
|
30
|
+
const c = contrastWithWhite(hex);
|
|
31
|
+
if (c < MIN_CONTRAST) {
|
|
32
|
+
throw new ExplicaiError("low-contrast", `A cor de ${label} tem contraste ${num(c)}:1 com o branco; o mínimo é ${num(MIN_CONTRAST)}:1. Escolha uma cor mais escura.`, { label, ratio: c, min: MIN_CONTRAST });
|
|
33
|
+
}
|
|
34
|
+
return hex;
|
|
35
|
+
}
|
|
36
|
+
/** A fonte da lista com esse nome. O texto precisa de uma com itálico empacotado. */
|
|
37
|
+
function fontOf(id, role) {
|
|
38
|
+
const font = typeof id === "string" ? fontById(id) : undefined;
|
|
39
|
+
const valid = FONTS.filter((f) => role === "headings" || f.italic);
|
|
40
|
+
if (!font || !valid.includes(font)) {
|
|
41
|
+
throw new Error(`Fonte fora da lista: ${String(id)}. Use uma destas: ${valid.map((f) => f.id).join(", ")}.`);
|
|
42
|
+
}
|
|
43
|
+
return font;
|
|
44
|
+
}
|
|
45
|
+
/** As fontes que o tema usa (as da marca, onde não houve escolha). */
|
|
46
|
+
export function themeFonts(theme) {
|
|
47
|
+
return {
|
|
48
|
+
text: fontById(theme?.fonts?.text ?? DEFAULT_TEXT_FONT),
|
|
49
|
+
headings: fontById(theme?.fonts?.headings ?? DEFAULT_HEADINGS_FONT),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Aplica `patch` sobre o tema atual e confere tudo. Devolve o tema novo; o que não mudou continua igual. */
|
|
53
|
+
export function mergeTheme(current, patch) {
|
|
54
|
+
const next = { ...current, headings: { ...current?.headings }, fonts: { ...current?.fonts } };
|
|
55
|
+
const set = (key, label, check) => {
|
|
56
|
+
if (patch[key] === undefined)
|
|
57
|
+
return;
|
|
58
|
+
const hex = hexOf(patch[key], label);
|
|
59
|
+
if (hex === null)
|
|
60
|
+
delete next[key];
|
|
61
|
+
else
|
|
62
|
+
next[key] = check ? readable(hex, label) : hex;
|
|
63
|
+
};
|
|
64
|
+
set("accent", "destaque", true);
|
|
65
|
+
set("text", "texto", true);
|
|
66
|
+
set("highlight", "realce", false);
|
|
67
|
+
if (patch.headings === null)
|
|
68
|
+
next.headings = {};
|
|
69
|
+
else if (patch.headings) {
|
|
70
|
+
for (const slot of HEADING_SLOTS) {
|
|
71
|
+
const value = patch.headings[slot];
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
const hex = hexOf(value, SLOT_NAMES[slot]);
|
|
75
|
+
if (hex === null)
|
|
76
|
+
delete next.headings?.[slot];
|
|
77
|
+
else if (next.headings)
|
|
78
|
+
next.headings[slot] = readable(hex, SLOT_NAMES[slot]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (patch.fonts === null)
|
|
82
|
+
next.fonts = {};
|
|
83
|
+
else if (patch.fonts) {
|
|
84
|
+
for (const role of ["text", "headings"]) {
|
|
85
|
+
const id = patch.fonts[role];
|
|
86
|
+
if (id === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
if (id === null)
|
|
89
|
+
delete next.fonts?.[role];
|
|
90
|
+
else if (next.fonts)
|
|
91
|
+
next.fonts[role] = fontOf(id, role).id;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (patch.logo === null)
|
|
95
|
+
delete next.logo;
|
|
96
|
+
else if (patch.logo !== undefined)
|
|
97
|
+
next.logo = checkLogoPath(patch.logo);
|
|
98
|
+
// O realce é fundo: o texto por cima precisa continuar legível.
|
|
99
|
+
if (next.highlight) {
|
|
100
|
+
const c = contrast(overWhite(next.highlight, HIGHLIGHT_ALPHA), next.text ?? DEFAULT_TEXT);
|
|
101
|
+
if (c < MIN_CONTRAST) {
|
|
102
|
+
throw new ExplicaiError("low-contrast-highlight", `O texto sobre o realce tem contraste ${num(c)}:1; o mínimo é ${num(MIN_CONTRAST)}:1. Escolha um realce mais claro.`, { ratio: c, min: MIN_CONTRAST });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!Object.keys(next.headings ?? {}).length)
|
|
106
|
+
delete next.headings;
|
|
107
|
+
if (!Object.keys(next.fonts ?? {}).length)
|
|
108
|
+
delete next.fonts;
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
/** Variáveis CSS do tema, só as que mudam. Vazio quando o manual usa as cores da marca. */
|
|
112
|
+
export function themeCss(theme) {
|
|
113
|
+
if (!theme)
|
|
114
|
+
return "";
|
|
115
|
+
const v = [];
|
|
116
|
+
if (theme.accent)
|
|
117
|
+
v.push(`--accent:${theme.accent}`, `--anotacao:${theme.accent}`, `--anotacao-fundo:${withAlpha(theme.accent, 0.08)}`);
|
|
118
|
+
if (theme.text)
|
|
119
|
+
v.push(`--tinta:${theme.text}`, `--grafite:${muted(theme.text)}`);
|
|
120
|
+
if (theme.highlight) {
|
|
121
|
+
v.push(`--marca:${theme.highlight}`, `--marca-faixa:${withAlpha(theme.highlight, HIGHLIGHT_ALPHA)}`, `--marca-halo:${withAlpha(theme.highlight, 0.3)}`);
|
|
122
|
+
}
|
|
123
|
+
for (const slot of HEADING_SLOTS)
|
|
124
|
+
if (theme.headings?.[slot])
|
|
125
|
+
v.push(`--t-${slot}:${theme.headings[slot]}`);
|
|
126
|
+
const fonts = themeFonts(theme);
|
|
127
|
+
if (theme.fonts?.text)
|
|
128
|
+
v.push(`--texto:${fontStack(fonts.text)}`);
|
|
129
|
+
if (theme.fonts?.headings)
|
|
130
|
+
v.push(`--display:${fontStack(fonts.headings)}`);
|
|
131
|
+
return v.length ? `:root{${v.join(";")}}` : "";
|
|
132
|
+
}
|
|
133
|
+
/** As fontes que vão no export: texto, títulos (sem repetir) e a de código. */
|
|
134
|
+
export function usedFonts(theme) {
|
|
135
|
+
const { text, headings } = themeFonts(theme);
|
|
136
|
+
return [...new Set([text, headings, MONO])];
|
|
137
|
+
}
|
|
138
|
+
/** `@font-face` das fontes usadas, no mesmo formato do export. */
|
|
139
|
+
export const fontFacesCss = (theme) => usedFonts(theme).map(fontFaceCss).join("\n");
|
|
140
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parse, stringify } from "yaml";
|
|
2
|
+
const FENCE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
3
|
+
export function splitFrontmatter(text) {
|
|
4
|
+
const match = FENCE.exec(text);
|
|
5
|
+
if (!match)
|
|
6
|
+
return { data: {}, body: text };
|
|
7
|
+
const data = parse(match[1] ?? "");
|
|
8
|
+
if (data !== null && (typeof data !== "object" || Array.isArray(data))) {
|
|
9
|
+
throw new Error("Frontmatter inválido: esperado um mapa YAML.");
|
|
10
|
+
}
|
|
11
|
+
// joinFrontmatter põe uma linha em branco depois do bloco; ela não faz parte do corpo.
|
|
12
|
+
return { data: (data ?? {}), body: text.slice(match[0].length).replace(/^\r?\n/, "") };
|
|
13
|
+
}
|
|
14
|
+
export function joinFrontmatter(data, body) {
|
|
15
|
+
return `---\n${stringify(data).trimEnd()}\n---\n\n${body.replace(/^\n+/, "")}`;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Grava num arquivo temporário e renomeia, para um travamento no meio não deixar o arquivo pela metade. */
|
|
2
|
+
export declare function atomicWrite(file: string, content: string): Promise<void>;
|
|
3
|
+
/** Lê UTF-8 sem o BOM que o Bloco de Notas e alguns editores do Windows põem no começo: ele quebraria o JSON e o frontmatter. */
|
|
4
|
+
export declare function readText(file: string): Promise<string>;
|
|
5
|
+
/** Barreira de confiança: caminhos vêm do Agent e do navegador, então nada pode sair da pasta base. */
|
|
6
|
+
export declare function resolveInside(dir: string, relative: string): string;
|
|
7
|
+
/** Como `resolveInside`, mas segue links simbólicos: um link dentro do manual que aponta para fora é recusado. O arquivo precisa existir. */
|
|
8
|
+
export declare function resolveInsideReal(dir: string, relative: string): Promise<string>;
|
|
9
|
+
/** `base.ext`, ou `base-2.ext`, `base-3.ext`… se já existir. */
|
|
10
|
+
export declare function uniquePath(parent: string, base: string, ext: string): Promise<string>;
|
|
11
|
+
export declare function exists(p: string): Promise<boolean>;
|
|
12
|
+
/** Caminho relativo com `/`, igual em qualquer sistema, para gravar em manual.json e no Markdown. */
|
|
13
|
+
export declare function relativePosix(from: string, to: string): string;
|
package/dist/core/fs.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, realpath, rename, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Grava num arquivo temporário e renomeia, para um travamento no meio não deixar o arquivo pela metade. */
|
|
5
|
+
export async function atomicWrite(file, content) {
|
|
6
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
7
|
+
await writeFile(tmp, content, "utf8");
|
|
8
|
+
await rename(tmp, file);
|
|
9
|
+
}
|
|
10
|
+
/** Lê UTF-8 sem o BOM que o Bloco de Notas e alguns editores do Windows põem no começo: ele quebraria o JSON e o frontmatter. */
|
|
11
|
+
export async function readText(file) {
|
|
12
|
+
return (await readFile(file, "utf8")).replace(/^/, "");
|
|
13
|
+
}
|
|
14
|
+
/** Barreira de confiança: caminhos vêm do Agent e do navegador, então nada pode sair da pasta base. */
|
|
15
|
+
export function resolveInside(dir, relative) {
|
|
16
|
+
const root = path.resolve(dir);
|
|
17
|
+
const abs = path.resolve(root, relative);
|
|
18
|
+
if (abs !== root && !abs.startsWith(root + path.sep))
|
|
19
|
+
throw new Error(`Caminho fora do manual: ${relative}`);
|
|
20
|
+
return abs;
|
|
21
|
+
}
|
|
22
|
+
/** Como `resolveInside`, mas segue links simbólicos: um link dentro do manual que aponta para fora é recusado. O arquivo precisa existir. */
|
|
23
|
+
export async function resolveInsideReal(dir, relative) {
|
|
24
|
+
const abs = resolveInside(dir, relative);
|
|
25
|
+
const [real, root] = await Promise.all([realpath(abs), realpath(path.resolve(dir))]);
|
|
26
|
+
if (real !== root && !real.startsWith(root + path.sep))
|
|
27
|
+
throw new Error(`Caminho fora do manual: ${relative}`);
|
|
28
|
+
return abs;
|
|
29
|
+
}
|
|
30
|
+
/** `base.ext`, ou `base-2.ext`, `base-3.ext`… se já existir. */
|
|
31
|
+
export async function uniquePath(parent, base, ext) {
|
|
32
|
+
for (let i = 1;; i++) {
|
|
33
|
+
const candidate = path.join(parent, `${i === 1 ? base : `${base}-${i}`}${ext}`);
|
|
34
|
+
if (!(await exists(candidate)))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function exists(p) {
|
|
39
|
+
return stat(p).then(() => true, () => false);
|
|
40
|
+
}
|
|
41
|
+
/** Caminho relativo com `/`, igual em qualquer sistema, para gravar em manual.json e no Markdown. */
|
|
42
|
+
export function relativePosix(from, to) {
|
|
43
|
+
return path.relative(from, to).split(path.sep).join("/");
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=fs.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { emptyAnnotations, parseAnnotations } from "./annotations.js";
|
|
2
|
+
export { cleanUnusedAssets, findUnusedAssets, readAnnotations, saveAsset, writeAnnotations } from "./assets.js";
|
|
3
|
+
export { ExplicaiError } from "./errors.js";
|
|
4
|
+
export { FONTS, FONTS_DIR, type FontSpec, fontFiles } from "./export/fonts.js";
|
|
5
|
+
export { EXPORT_DIR, EXPORT_FORMATS, type ExportFormat, type ExportResult, exportManual, findBrowser, logoSizeError, setManualLogo, setManualTheme, } from "./export/index.js";
|
|
6
|
+
export { resolveInside, resolveInsideReal } from "./fs.js";
|
|
7
|
+
export { createManual, listManuals, readManual } from "./manuals.js";
|
|
8
|
+
export { addPage, movePage, type PageTreeNode, readPage, readPageTree, updatePage } from "./pages.js";
|
|
9
|
+
export { insideGitRepo, redactAsset } from "./redact.js";
|
|
10
|
+
export { slugify } from "./text.js";
|
|
11
|
+
export { listTrash, removePage, restorePage, TRASH_DIR, type TrashItem } from "./trash.js";
|
|
12
|
+
export { flatten } from "./tree.js";
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export { listVersions, publishVersion, readVersion, restoreVersion, saveVersion, VERSIONS_DIR, type Version, type VersionInfo, } from "./versions.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { emptyAnnotations, parseAnnotations } from "./annotations.js";
|
|
2
|
+
export { cleanUnusedAssets, findUnusedAssets, readAnnotations, saveAsset, writeAnnotations } from "./assets.js";
|
|
3
|
+
export { ExplicaiError } from "./errors.js";
|
|
4
|
+
export { FONTS, FONTS_DIR, fontFiles } from "./export/fonts.js";
|
|
5
|
+
export { EXPORT_DIR, EXPORT_FORMATS, exportManual, findBrowser, logoSizeError, setManualLogo, setManualTheme, } from "./export/index.js";
|
|
6
|
+
export { resolveInside, resolveInsideReal } from "./fs.js";
|
|
7
|
+
export { createManual, listManuals, readManual } from "./manuals.js";
|
|
8
|
+
export { addPage, movePage, readPage, readPageTree, updatePage } from "./pages.js";
|
|
9
|
+
export { insideGitRepo, redactAsset } from "./redact.js";
|
|
10
|
+
export { slugify } from "./text.js";
|
|
11
|
+
export { listTrash, removePage, restorePage, TRASH_DIR } from "./trash.js";
|
|
12
|
+
export { flatten } from "./tree.js";
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export { listVersions, publishVersion, readVersion, restoreVersion, saveVersion, VERSIONS_DIR, } from "./versions.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Manual, type ManualMeta } from "./types.js";
|
|
2
|
+
export declare const PAGES_DIR = "pages";
|
|
3
|
+
export declare const ASSETS_DIR = "assets";
|
|
4
|
+
export declare function createManual(workspace: string, opts: {
|
|
5
|
+
title: string;
|
|
6
|
+
language: string;
|
|
7
|
+
}): Promise<Manual>;
|
|
8
|
+
/**
|
|
9
|
+
* Manuais são as subpastas diretas do workspace que têm manual.json.
|
|
10
|
+
* Manual com manual.json ilegível fica de fora da lista, para um manual estragado não impedir
|
|
11
|
+
* de abrir os outros. Abrir ele pelo endereço mostra o erro de readManual.
|
|
12
|
+
*/
|
|
13
|
+
export declare function listManuals(workspace: string): Promise<Manual[]>;
|
|
14
|
+
export declare function readManual(dir: string): Promise<ManualMeta>;
|
|
15
|
+
export declare function writeManual(dir: string, meta: ManualMeta): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Lê o manual, aplica a mudança e grava de volta. Concentra o ciclo ler-alterar-gravar da árvore.
|
|
18
|
+
* Chamadas da mesma pasta entram numa fila, senão duas leituras simultâneas se atropelam e a última
|
|
19
|
+
* gravação apaga a mudança da outra. `change` não pode chamar updateManual da mesma pasta (travaria).
|
|
20
|
+
*/
|
|
21
|
+
export declare function updateManual<T>(dir: string, change: (meta: ManualMeta) => T): Promise<T>;
|
|
22
|
+
/** Roda `task` na fila da pasta (a mesma do updateManual). `task` não pode entrar na fila da mesma pasta de novo. */
|
|
23
|
+
export declare function inQueue<T>(dir: string, task: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { atomicWrite, exists, readText, uniquePath } from "./fs.js";
|
|
4
|
+
import { required, slugify } from "./text.js";
|
|
5
|
+
import { FORMAT_VERSION } from "./types.js";
|
|
6
|
+
const META_FILE = "manual.json";
|
|
7
|
+
export const PAGES_DIR = "pages";
|
|
8
|
+
export const ASSETS_DIR = "assets";
|
|
9
|
+
export async function createManual(workspace, opts) {
|
|
10
|
+
const title = required(opts.title, "título do manual");
|
|
11
|
+
const dir = await uniquePath(workspace, slugify(title), "");
|
|
12
|
+
const meta = { version: FORMAT_VERSION, title, language: opts.language, pages: [] };
|
|
13
|
+
await mkdir(path.join(dir, PAGES_DIR), { recursive: true });
|
|
14
|
+
await mkdir(path.join(dir, ASSETS_DIR), { recursive: true });
|
|
15
|
+
await writeManual(dir, meta);
|
|
16
|
+
return { dir, meta };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Manuais são as subpastas diretas do workspace que têm manual.json.
|
|
20
|
+
* Manual com manual.json ilegível fica de fora da lista, para um manual estragado não impedir
|
|
21
|
+
* de abrir os outros. Abrir ele pelo endereço mostra o erro de readManual.
|
|
22
|
+
*/
|
|
23
|
+
export async function listManuals(workspace) {
|
|
24
|
+
const entries = await readdir(workspace, { withFileTypes: true });
|
|
25
|
+
const manuals = [];
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
if (!e.isDirectory())
|
|
28
|
+
continue;
|
|
29
|
+
const dir = path.join(workspace, e.name);
|
|
30
|
+
if (!(await exists(path.join(dir, META_FILE))))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
manuals.push({ dir, meta: await readManual(dir) });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// manual.json corrompido: os outros manuais continuam abrindo.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return manuals.sort((a, b) => a.meta.title.localeCompare(b.meta.title));
|
|
40
|
+
}
|
|
41
|
+
export async function readManual(dir) {
|
|
42
|
+
let meta;
|
|
43
|
+
try {
|
|
44
|
+
meta = JSON.parse(await readText(path.join(dir, META_FILE)));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof SyntaxError)
|
|
48
|
+
throw new Error(`O ${META_FILE} do manual "${path.basename(dir)}" não é um JSON válido: ${err.message}`);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
if (meta?.version !== FORMAT_VERSION) {
|
|
52
|
+
throw new Error(`${META_FILE}: versão de formato não suportada (${String(meta?.version)})`);
|
|
53
|
+
}
|
|
54
|
+
return meta;
|
|
55
|
+
}
|
|
56
|
+
export async function writeManual(dir, meta) {
|
|
57
|
+
await atomicWrite(path.join(dir, META_FILE), `${JSON.stringify(meta, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
const queues = new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Lê o manual, aplica a mudança e grava de volta. Concentra o ciclo ler-alterar-gravar da árvore.
|
|
62
|
+
* Chamadas da mesma pasta entram numa fila, senão duas leituras simultâneas se atropelam e a última
|
|
63
|
+
* gravação apaga a mudança da outra. `change` não pode chamar updateManual da mesma pasta (travaria).
|
|
64
|
+
*/
|
|
65
|
+
export function updateManual(dir, change) {
|
|
66
|
+
return inQueue(dir, async () => {
|
|
67
|
+
const meta = await readManual(dir);
|
|
68
|
+
const result = change(meta);
|
|
69
|
+
await writeManual(dir, meta);
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Roda `task` na fila da pasta (a mesma do updateManual). `task` não pode entrar na fila da mesma pasta de novo. */
|
|
74
|
+
export function inQueue(dir, task) {
|
|
75
|
+
const key = path.resolve(dir);
|
|
76
|
+
const run = (queues.get(key) ?? Promise.resolve()).catch(() => { }).then(task);
|
|
77
|
+
queues.set(key, run);
|
|
78
|
+
// Fila vazia: tira a pasta do Map. Falha já chega a quem chamou por `run`.
|
|
79
|
+
const clean = () => {
|
|
80
|
+
if (queues.get(key) === run)
|
|
81
|
+
queues.delete(key);
|
|
82
|
+
};
|
|
83
|
+
run.then(clean, clean);
|
|
84
|
+
return run;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=manuals.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Page, type PageStatus } from "./types.js";
|
|
2
|
+
/** Página pode ficar sem título enquanto é escrita: a árvore mostra "Sem título" e o arquivo vira sem-titulo.md. */
|
|
3
|
+
export declare function addPage(dir: string, opts: {
|
|
4
|
+
title: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
parentId?: string | null;
|
|
7
|
+
index?: number;
|
|
8
|
+
}): Promise<Page>;
|
|
9
|
+
export declare function readPage(dir: string, id: string): Promise<Page>;
|
|
10
|
+
export interface PageTreeNode {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
status: PageStatus;
|
|
14
|
+
file: string;
|
|
15
|
+
/** Preenchido quando o arquivo da página sumiu ou está com erro: a página aparece, com o aviso. */
|
|
16
|
+
problem?: string;
|
|
17
|
+
/** Versão escolhida para o manual final, quando há uma (ver versions.ts). */
|
|
18
|
+
published?: string;
|
|
19
|
+
children: PageTreeNode[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Árvore do manual com o título de cada página (que fica no frontmatter). Lê o manual.json uma vez.
|
|
23
|
+
* Uma página com problema não derruba a árvore: ela vem com `problem` e o resto continua.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readPageTree(dir: string): Promise<PageTreeNode[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Quando o título muda, o arquivo é renomeado para acompanhar, para a pasta continuar legível
|
|
28
|
+
* para pessoas e IAs. O id no frontmatter não muda, e é ele que as áreas clicáveis usam.
|
|
29
|
+
* `fileName`: "keep" grava o título no mesmo arquivo (digitação em andamento); "sync" renomeia
|
|
30
|
+
* se o nome do arquivo não combina com o título (ao parar de digitar ou sair da página).
|
|
31
|
+
*/
|
|
32
|
+
export declare function updatePage(dir: string, id: string, patch: {
|
|
33
|
+
title?: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
status?: PageStatus;
|
|
36
|
+
}, opts?: {
|
|
37
|
+
fileName?: "keep" | "sync" | undefined;
|
|
38
|
+
ifMatch?: string | undefined;
|
|
39
|
+
}): Promise<Page>;
|
|
40
|
+
export declare function movePage(dir: string, id: string, to: {
|
|
41
|
+
parentId: string | null;
|
|
42
|
+
index: number;
|
|
43
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { rm, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ExplicaiError } from "./errors.js";
|
|
5
|
+
import { joinFrontmatter, splitFrontmatter } from "./frontmatter.js";
|
|
6
|
+
import { atomicWrite, readText, relativePosix, resolveInside, uniquePath } from "./fs.js";
|
|
7
|
+
import { inQueue, PAGES_DIR, readManual, updateManual } from "./manuals.js";
|
|
8
|
+
import { slugify } from "./text.js";
|
|
9
|
+
import { childrenOf, clampIndex, findIn, findNode, flatten } from "./tree.js";
|
|
10
|
+
import { PAGE_STATUSES } from "./types.js";
|
|
11
|
+
/** Página pode ficar sem título enquanto é escrita: a árvore mostra "Sem título" e o arquivo vira sem-titulo.md. */
|
|
12
|
+
export async function addPage(dir, opts) {
|
|
13
|
+
const title = opts.title.trim();
|
|
14
|
+
const page = {
|
|
15
|
+
id: randomUUID(),
|
|
16
|
+
title,
|
|
17
|
+
status: "rascunho",
|
|
18
|
+
body: opts.body ?? "",
|
|
19
|
+
file: await newPageFile(dir, title),
|
|
20
|
+
};
|
|
21
|
+
// Arquivo primeiro: se gravar falhar, a árvore não aponta para o que não existe. Se a árvore falhar, desfaz o arquivo.
|
|
22
|
+
await writePageFile(dir, page);
|
|
23
|
+
page.version = await fileVersion(dir, page.file);
|
|
24
|
+
try {
|
|
25
|
+
await updateManual(dir, (meta) => {
|
|
26
|
+
const siblings = childrenOf(meta.pages, opts.parentId);
|
|
27
|
+
siblings.splice(clampIndex(opts.index, siblings.length), 0, { id: page.id, file: page.file, children: [] });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
await rm(resolveInside(dir, page.file), { force: true });
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return page;
|
|
35
|
+
}
|
|
36
|
+
export async function readPage(dir, id) {
|
|
37
|
+
const { node } = findNode((await readManual(dir)).pages, id);
|
|
38
|
+
return { id, ...(await readPageFile(dir, node.file)), file: node.file, version: await fileVersion(dir, node.file) };
|
|
39
|
+
}
|
|
40
|
+
/** Versão do arquivo: mtime + tamanho. Basta para saber se alguém mexeu nele desde a última leitura. */
|
|
41
|
+
async function fileVersion(dir, file) {
|
|
42
|
+
const s = await stat(resolveInside(dir, file));
|
|
43
|
+
return `${s.mtimeMs}-${s.size}`;
|
|
44
|
+
}
|
|
45
|
+
/** Dentro da fila da pasta: recusa a gravação se o arquivo mudou no disco depois de `expected`. */
|
|
46
|
+
async function assertVersion(dir, file, expected) {
|
|
47
|
+
if ((await fileVersion(dir, file)) === expected)
|
|
48
|
+
return;
|
|
49
|
+
throw new ExplicaiError("disk-conflict", "Esta página foi alterada fora do Studio depois que você a abriu. Recarregue do disco ou mantenha a sua versão.");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Árvore do manual com o título de cada página (que fica no frontmatter). Lê o manual.json uma vez.
|
|
53
|
+
* Uma página com problema não derruba a árvore: ela vem com `problem` e o resto continua.
|
|
54
|
+
*/
|
|
55
|
+
export async function readPageTree(dir) {
|
|
56
|
+
const walk = (nodes) => Promise.all(nodes.map(async (n) => {
|
|
57
|
+
const base = {
|
|
58
|
+
id: n.id,
|
|
59
|
+
file: n.file,
|
|
60
|
+
...(n.published ? { published: n.published } : {}),
|
|
61
|
+
children: await walk(n.children),
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
const { title, status } = await readPageFile(dir, n.file);
|
|
65
|
+
return { ...base, title, status };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return { ...base, title: "", status: "rascunho", problem: err instanceof Error ? err.message : String(err) };
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
return walk((await readManual(dir)).pages);
|
|
72
|
+
}
|
|
73
|
+
/** Lê título e corpo de um arquivo de página, com mensagem clara quando ele foi apagado ou estragado. */
|
|
74
|
+
async function readPageFile(dir, file) {
|
|
75
|
+
let text;
|
|
76
|
+
try {
|
|
77
|
+
text = await readText(resolveInside(dir, file));
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err.code === "ENOENT") {
|
|
81
|
+
throw new Error(`O arquivo ${file} não foi encontrado. Ele foi apagado ou renomeado fora do Studio.`);
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const { data, body } = splitFrontmatter(text);
|
|
87
|
+
const status = PAGE_STATUSES.includes(data.status) ? data.status : "rascunho";
|
|
88
|
+
return { title: typeof data.title === "string" ? data.title : "", status, body };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
92
|
+
throw new Error(`O cabeçalho (frontmatter) de ${file} tem um erro: ${detail}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Quando o título muda, o arquivo é renomeado para acompanhar, para a pasta continuar legível
|
|
97
|
+
* para pessoas e IAs. O id no frontmatter não muda, e é ele que as áreas clicáveis usam.
|
|
98
|
+
* `fileName`: "keep" grava o título no mesmo arquivo (digitação em andamento); "sync" renomeia
|
|
99
|
+
* se o nome do arquivo não combina com o título (ao parar de digitar ou sair da página).
|
|
100
|
+
*/
|
|
101
|
+
export async function updatePage(dir, id, patch, opts = {}) {
|
|
102
|
+
const page = await readPage(dir, id);
|
|
103
|
+
if (patch.status !== undefined && !PAGE_STATUSES.includes(patch.status)) {
|
|
104
|
+
throw new Error(`Status inválido: ${patch.status}. Use ${PAGE_STATUSES.join(", ")}.`);
|
|
105
|
+
}
|
|
106
|
+
const next = {
|
|
107
|
+
...page,
|
|
108
|
+
...(patch.status === undefined ? {} : { status: patch.status }),
|
|
109
|
+
...(patch.title === undefined ? {} : { title: patch.title.trim() }),
|
|
110
|
+
...(patch.body === undefined ? {} : { body: patch.body }),
|
|
111
|
+
};
|
|
112
|
+
const sameName = opts.fileName === "keep" ||
|
|
113
|
+
(opts.fileName === "sync" ? fileMatchesTitle(page.file, next.title) : slugify(next.title) === slugify(page.title));
|
|
114
|
+
const { ifMatch } = opts;
|
|
115
|
+
if (sameName) {
|
|
116
|
+
if (ifMatch === undefined)
|
|
117
|
+
await writePageFile(dir, next);
|
|
118
|
+
else {
|
|
119
|
+
await inQueue(dir, async () => {
|
|
120
|
+
await assertVersion(dir, page.file, ifMatch);
|
|
121
|
+
await writePageFile(dir, next);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { ...next, version: await fileVersion(dir, next.file) };
|
|
125
|
+
}
|
|
126
|
+
// shortcut: com renomeio, a checagem e a gravação não são atômicas (janela de milissegundos); upgrade se aparecer na prática.
|
|
127
|
+
if (ifMatch !== undefined)
|
|
128
|
+
await inQueue(dir, () => assertVersion(dir, page.file, ifMatch));
|
|
129
|
+
next.file = await newPageFile(dir, next.title);
|
|
130
|
+
await writePageFile(dir, next);
|
|
131
|
+
const others = await updateManual(dir, (meta) => {
|
|
132
|
+
findNode(meta.pages, id).node.file = next.file;
|
|
133
|
+
return flatten(meta.pages).filter((n) => n.id !== id);
|
|
134
|
+
});
|
|
135
|
+
await rm(resolveInside(dir, page.file), { force: true });
|
|
136
|
+
await relink(dir, others.map((n) => n.file), page.file, next.file);
|
|
137
|
+
return { ...next, version: await fileVersion(dir, next.file) };
|
|
138
|
+
}
|
|
139
|
+
/** O arquivo já tem o nome do título: `titulo.md` ou `titulo-2.md` (sufixo que `uniquePath` põe). */
|
|
140
|
+
function fileMatchesTitle(file, title) {
|
|
141
|
+
const base = path.posix.basename(file, ".md");
|
|
142
|
+
const want = slugify(title);
|
|
143
|
+
return base === want || (base.startsWith(`${want}-`) && /^\d+$/.test(base.slice(want.length + 1)));
|
|
144
|
+
}
|
|
145
|
+
/** Links entre páginas são `./arquivo.md`. Quando um arquivo muda de nome, as outras páginas acompanham. */
|
|
146
|
+
async function relink(dir, files, from, to) {
|
|
147
|
+
const name = (f) => path.posix.basename(f);
|
|
148
|
+
const escaped = name(from).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
149
|
+
// ](arquivo.md ou ](./arquivo.md seguido de ) ou de #âncora
|
|
150
|
+
// Precisa ser new RegExp: o nome do arquivo entra no padrão (teste: "atualiza os links das outras páginas")
|
|
151
|
+
const pattern = new RegExp(`(\\]\\()(?:\\./)?${escaped}(?=[)#])`, "g");
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
const abs = resolveInside(dir, file);
|
|
154
|
+
const text = await readText(abs);
|
|
155
|
+
const updated = text.replace(pattern, `$1./${name(to)}`);
|
|
156
|
+
if (updated !== text)
|
|
157
|
+
await atomicWrite(abs, updated);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function movePage(dir, id, to) {
|
|
161
|
+
await updateManual(dir, (meta) => {
|
|
162
|
+
const { node, siblings } = findNode(meta.pages, id);
|
|
163
|
+
if (to.parentId && (to.parentId === id || findIn(node.children, to.parentId))) {
|
|
164
|
+
throw new Error("Não dá para mover uma página para dentro dela mesma.");
|
|
165
|
+
}
|
|
166
|
+
siblings.splice(siblings.indexOf(node), 1);
|
|
167
|
+
const target = childrenOf(meta.pages, to.parentId);
|
|
168
|
+
target.splice(clampIndex(to.index, target.length), 0, node);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function newPageFile(dir, title) {
|
|
172
|
+
return relativePosix(dir, await uniquePath(path.join(dir, PAGES_DIR), slugify(title), ".md"));
|
|
173
|
+
}
|
|
174
|
+
async function writePageFile(dir, page) {
|
|
175
|
+
// Rascunho é o padrão e não vai para o arquivo: página escrita à mão continua igual.
|
|
176
|
+
const status = page.status === "rascunho" ? {} : { status: page.status };
|
|
177
|
+
await atomicWrite(resolveInside(dir, page.file), joinFrontmatter({ id: page.id, title: page.title, ...status }, page.body));
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Troca o arquivo de `assets/` pela versão com as tarjas já queimadas (o navegador desenha e envia).
|
|
3
|
+
* Valida o formato e que a imagem abre, grava num temporário e renomeia por cima do caminho de sempre,
|
|
4
|
+
* então Markdown e marcações seguem valendo. O original não vai para a lixeira: some, junto com
|
|
5
|
+
* qualquer cópia idêntica dele na pasta de imagens sem uso.
|
|
6
|
+
*/
|
|
7
|
+
export declare function redactAsset(dir: string, assetPath: string, data: Uint8Array): Promise<void>;
|
|
8
|
+
/** A pasta do manual está dentro de um repositório git? Procura `.git` na pasta e nas de cima. */
|
|
9
|
+
export declare function insideGitRepo(dir: string): Promise<boolean>;
|