explicai 0.1.0 → 0.2.1
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 +85 -0
- package/LICENSE +21 -21
- package/README.md +87 -27
- package/dist/cli.js +30 -11
- package/dist/core/annotations.js +2 -0
- package/dist/core/assets.d.ts +2 -0
- package/dist/core/assets.js +51 -16
- 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 +2 -2
- package/dist/core/export/collect.js +2 -2
- 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 +1 -3
- package/dist/core/export/html.js +86 -39
- package/dist/core/export/imageSize.js +20 -3
- package/dist/core/export/index.d.ts +12 -6
- package/dist/core/export/index.js +106 -26
- package/dist/core/export/labels.d.ts +1 -0
- package/dist/core/export/labels.js +2 -0
- package/dist/core/export/logo.d.ts +10 -0
- package/dist/core/export/logo.js +29 -0
- package/dist/core/export/markdown.js +20 -11
- package/dist/core/export/pdf.js +2 -1
- package/dist/core/export/refs.d.ts +2 -2
- package/dist/core/export/refs.js +22 -9
- 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/fs.d.ts +4 -0
- package/dist/core/fs.js +13 -1
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.js +5 -2
- package/dist/core/manuals.d.ts +7 -1
- package/dist/core/manuals.js +29 -9
- package/dist/core/pages.d.ts +5 -0
- package/dist/core/pages.js +56 -15
- package/dist/core/redact.d.ts +9 -0
- package/dist/core/redact.js +107 -0
- package/dist/core/text.d.ts +2 -0
- package/dist/core/text.js +9 -0
- package/dist/core/trash.js +13 -8
- package/dist/core/types.d.ts +26 -1
- package/dist/core/types.js +2 -0
- package/dist/core/versions.js +3 -3
- package/dist/server/errorPage.d.ts +9 -0
- package/dist/server/errorPage.js +122 -0
- package/dist/server/http.d.ts +11 -3
- package/dist/server/http.js +65 -11
- package/dist/server/index.js +35 -5
- package/dist/server/routes/assets.js +21 -2
- package/dist/server/routes/export.js +18 -8
- package/dist/server/routes/fonts.d.ts +3 -0
- package/dist/server/routes/fonts.js +24 -0
- package/dist/server/routes/pages.js +3 -1
- package/dist/server/routes/web.js +7 -1
- package/dist/server/security.d.ts +1 -1
- package/dist/server/security.js +7 -6
- package/dist/web/assets/index-BlWnNe8d.js +262 -0
- package/dist/web/assets/index-DFBrhkII.css +1 -0
- package/dist/web/index.html +2 -3
- 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 +41 -7
- package/dist/cli.js.map +0 -1
- package/dist/core/annotations.js.map +0 -1
- package/dist/core/assets.js.map +0 -1
- package/dist/core/export/collect.js.map +0 -1
- package/dist/core/export/html.js.map +0 -1
- package/dist/core/export/imageSize.js.map +0 -1
- package/dist/core/export/index.js.map +0 -1
- package/dist/core/export/labels.js.map +0 -1
- package/dist/core/export/markdown.js.map +0 -1
- package/dist/core/export/pdf.js.map +0 -1
- package/dist/core/export/refs.js.map +0 -1
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/fs.js.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/core/manuals.js.map +0 -1
- package/dist/core/pages.js.map +0 -1
- package/dist/core/text.js.map +0 -1
- package/dist/core/trash.js.map +0 -1
- package/dist/core/tree.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/versions.js.map +0 -1
- package/dist/server/http.js.map +0 -1
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.js.map +0 -1
- package/dist/server/routes/assets.js.map +0 -1
- package/dist/server/routes/export.js.map +0 -1
- package/dist/server/routes/manuals.js.map +0 -1
- package/dist/server/routes/pages.js.map +0 -1
- package/dist/server/routes/versions.js.map +0 -1
- package/dist/server/routes/web.js.map +0 -1
- package/dist/server/security.js.map +0 -1
- package/dist/server/workspace.js.map +0 -1
- package/dist/web/assets/index-BXkBk-2Z.css +0 -1
- package/dist/web/assets/index-C02XKPHS.js +0 -260
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Cor de cada tipo de alerta no manual exportado (fundo branco). O de `note` segue a cor de destaque do manual. */
|
|
2
|
+
export const CALLOUT_COLORS = {
|
|
3
|
+
tip: "#17793a",
|
|
4
|
+
warning: "#b04a00",
|
|
5
|
+
caution: "#c3261b",
|
|
6
|
+
note: "#2b4fe0",
|
|
7
|
+
};
|
|
8
|
+
const MARKER = /^\[!(TIP|WARNING|CAUTION|NOTE)\][ \t]*\n?/i;
|
|
9
|
+
/**
|
|
10
|
+
* Alerta GFM (`> [!TIP]` na primeira linha da citação): devolve o tipo e o corpo sem o marcador.
|
|
11
|
+
* Qualquer outra citação, inclusive `[!IMPORTANT]`, volta null e sai como citação comum.
|
|
12
|
+
*/
|
|
13
|
+
export function readCallout(tokens) {
|
|
14
|
+
const [first, ...rest] = tokens;
|
|
15
|
+
if (first?.type !== "paragraph")
|
|
16
|
+
return null;
|
|
17
|
+
const [lead, ...more] = first.tokens;
|
|
18
|
+
const m = lead?.type === "text" ? MARKER.exec(lead.raw) : null;
|
|
19
|
+
if (!lead || !m?.[1])
|
|
20
|
+
return null;
|
|
21
|
+
const left = lead.raw.slice(m[0].length);
|
|
22
|
+
const inline = left ? [{ ...lead, raw: left, text: left }, ...more] : more;
|
|
23
|
+
const body = inline.length ? [{ ...first, tokens: inline }, ...rest] : rest;
|
|
24
|
+
return { type: m[1].toLowerCase(), body };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=callout.js.map
|
|
@@ -20,8 +20,8 @@ export interface CollectOptions {
|
|
|
20
20
|
onlyApproved?: boolean;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* As páginas na ordem da árvore, com o texto que vai para o manual final: a
|
|
24
|
-
*
|
|
23
|
+
* As páginas na ordem da árvore, com o texto que vai para o manual final: a Versão do manual
|
|
24
|
+
* quando existe, senão o texto atual. Página com problema fica de fora e vira aviso.
|
|
25
25
|
* A numeração conta só as páginas que entram, então o sumário não fica com buracos.
|
|
26
26
|
*/
|
|
27
27
|
export declare function collectPages(dir: string, opts?: CollectOptions): Promise<Collected>;
|
|
@@ -4,8 +4,8 @@ import { readVersion } from "../versions.js";
|
|
|
4
4
|
import { labelsFor } from "./labels.js";
|
|
5
5
|
const APPROVED = ["aprovado", "publicado"];
|
|
6
6
|
/**
|
|
7
|
-
* As páginas na ordem da árvore, com o texto que vai para o manual final: a
|
|
8
|
-
*
|
|
7
|
+
* As páginas na ordem da árvore, com o texto que vai para o manual final: a Versão do manual
|
|
8
|
+
* quando existe, senão o texto atual. Página com problema fica de fora e vira aviso.
|
|
9
9
|
* A numeração conta só as páginas que entram, então o sumário não fica com buracos.
|
|
10
10
|
*/
|
|
11
11
|
export async function collectPages(dir, opts = {}) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Contas de cor do tema do manual: hexadecimal de 6 dígitos, contraste WCAG com branco e com outra cor. */
|
|
2
|
+
export declare const HEX: RegExp;
|
|
3
|
+
/** Contraste entre duas cores, de 1 a 21. */
|
|
4
|
+
export declare const contrast: (a: string, b: string) => number;
|
|
5
|
+
export declare const contrastWithWhite: (hex: string) => number;
|
|
6
|
+
/** `#rrggbb` com transparência, como `rgba()`. */
|
|
7
|
+
export declare const withAlpha: (hex: string, alpha: number) => string;
|
|
8
|
+
/** A cor sobre o papel branco, com a transparência dada: o que a pessoa vê de um marca-texto. */
|
|
9
|
+
export declare const overWhite: (hex: string, alpha: number) => string;
|
|
10
|
+
/** Versão mais clara da cor, para texto secundário: a mais clara que ainda passa de 4,5:1 com o branco. */
|
|
11
|
+
export declare function muted(hex: string, min?: number): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Contas de cor do tema do manual: hexadecimal de 6 dígitos, contraste WCAG com branco e com outra cor. */
|
|
2
|
+
export const HEX = /^#[0-9a-f]{6}$/i;
|
|
3
|
+
const rgb = (hex) => [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16));
|
|
4
|
+
const toHex = (c) => `#${c.map((n) => Math.round(n).toString(16).padStart(2, "0")).join("")}`;
|
|
5
|
+
function luminance(c) {
|
|
6
|
+
const [r, g, b] = c.map((n) => {
|
|
7
|
+
const s = n / 255;
|
|
8
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
9
|
+
});
|
|
10
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
11
|
+
}
|
|
12
|
+
const ratio = (a, b) => {
|
|
13
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
14
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
15
|
+
};
|
|
16
|
+
const WHITE = [255, 255, 255];
|
|
17
|
+
/** Contraste entre duas cores, de 1 a 21. */
|
|
18
|
+
export const contrast = (a, b) => ratio(rgb(a), rgb(b));
|
|
19
|
+
export const contrastWithWhite = (hex) => ratio(rgb(hex), WHITE);
|
|
20
|
+
/** `#rrggbb` com transparência, como `rgba()`. */
|
|
21
|
+
export const withAlpha = (hex, alpha) => `rgba(${rgb(hex).join(",")},${alpha})`;
|
|
22
|
+
/** A cor sobre o papel branco, com a transparência dada: o que a pessoa vê de um marca-texto. */
|
|
23
|
+
export const overWhite = (hex, alpha) => toHex(rgb(hex).map((n) => n * alpha + 255 * (1 - alpha)));
|
|
24
|
+
/** Versão mais clara da cor, para texto secundário: a mais clara que ainda passa de 4,5:1 com o branco. */
|
|
25
|
+
export function muted(hex, min = 4.5) {
|
|
26
|
+
for (const keep of [0.8, 0.85, 0.9, 0.95]) {
|
|
27
|
+
const next = toHex(rgb(hex).map((n) => n * keep + 255 * (1 - keep)));
|
|
28
|
+
if (contrastWithWhite(next) >= min)
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
return hex;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=color.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fontes que o manual exportado pode usar. Todas OFL, empacotadas na pasta `fonts/` da raiz do pacote
|
|
3
|
+
* (copiadas do @fontsource por `npm run fonts:sync`). O export copia só as escolhidas, então o HTML abre offline.
|
|
4
|
+
*/
|
|
5
|
+
export interface FontSpec {
|
|
6
|
+
/** Nome do pacote do @fontsource, sem escopo: `inter`. É o valor gravado em `theme.fonts`. */
|
|
7
|
+
id: string;
|
|
8
|
+
family: string;
|
|
9
|
+
kind: "sans" | "serif";
|
|
10
|
+
/** Uma fonte variável (um arquivo para todos os pesos); senão, os arquivos de peso 400 e 700. */
|
|
11
|
+
variable: boolean;
|
|
12
|
+
/** Tem itálico empacotado: só essas servem para o texto do manual. */
|
|
13
|
+
italic: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Pasta das fontes (OFL), na raiz do pacote: o mesmo caminho vale de `src/` e de `dist/`. */
|
|
16
|
+
export declare const FONTS_DIR: URL;
|
|
17
|
+
/** Atkinson (texto) e Archivo (títulos) são as da marca, usadas quando o manual não escolhe outra. */
|
|
18
|
+
export declare const DEFAULT_TEXT_FONT = "atkinson-hyperlegible-next";
|
|
19
|
+
export declare const DEFAULT_HEADINGS_FONT = "archivo";
|
|
20
|
+
export declare const FONTS: FontSpec[];
|
|
21
|
+
/** Código do manual: sempre sai com ela, não é escolha. */
|
|
22
|
+
export declare const MONO: FontSpec;
|
|
23
|
+
export declare const fontById: (id: string) => FontSpec | undefined;
|
|
24
|
+
interface FontFile {
|
|
25
|
+
file: string;
|
|
26
|
+
weight: string;
|
|
27
|
+
style: "normal" | "italic";
|
|
28
|
+
}
|
|
29
|
+
/** Os arquivos woff2 (latin) da fonte, com o nome que o @fontsource dá. */
|
|
30
|
+
export declare function fontFiles(f: FontSpec): FontFile[];
|
|
31
|
+
/** O `@font-face` de cada arquivo da fonte, apontando para `fonts/`. */
|
|
32
|
+
export declare const fontFaceCss: (f: FontSpec) => string;
|
|
33
|
+
/** Valor de `font-family`: a fonte e, se não carregar, uma parecida do sistema. */
|
|
34
|
+
export declare const fontStack: (f: FontSpec) => string;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Pasta das fontes (OFL), na raiz do pacote: o mesmo caminho vale de `src/` e de `dist/`. */
|
|
2
|
+
export const FONTS_DIR = new URL("../../../fonts/", import.meta.url);
|
|
3
|
+
/** Atkinson (texto) e Archivo (títulos) são as da marca, usadas quando o manual não escolhe outra. */
|
|
4
|
+
export const DEFAULT_TEXT_FONT = "atkinson-hyperlegible-next";
|
|
5
|
+
export const DEFAULT_HEADINGS_FONT = "archivo";
|
|
6
|
+
export const FONTS = [
|
|
7
|
+
{ id: "atkinson-hyperlegible-next", family: "Atkinson Hyperlegible Next", kind: "sans", variable: false, italic: true },
|
|
8
|
+
{ id: "inter", family: "Inter", kind: "sans", variable: true, italic: true },
|
|
9
|
+
{ id: "source-sans-3", family: "Source Sans 3", kind: "sans", variable: true, italic: true },
|
|
10
|
+
{ id: "source-serif-4", family: "Source Serif 4", kind: "serif", variable: true, italic: true },
|
|
11
|
+
{ id: "lora", family: "Lora", kind: "serif", variable: true, italic: true },
|
|
12
|
+
{ id: "archivo", family: "Archivo", kind: "sans", variable: true, italic: false },
|
|
13
|
+
{ id: "montserrat", family: "Montserrat", kind: "sans", variable: true, italic: false },
|
|
14
|
+
{ id: "playfair-display", family: "Playfair Display", kind: "serif", variable: true, italic: false },
|
|
15
|
+
];
|
|
16
|
+
/** Código do manual: sempre sai com ela, não é escolha. */
|
|
17
|
+
export const MONO = { id: "jetbrains-mono", family: "JetBrains Mono", kind: "sans", variable: true, italic: false };
|
|
18
|
+
export const fontById = (id) => FONTS.find((f) => f.id === id);
|
|
19
|
+
/** Os arquivos woff2 (latin) da fonte, com o nome que o @fontsource dá. */
|
|
20
|
+
export function fontFiles(f) {
|
|
21
|
+
const name = (axis, style) => `${f.id}-latin-${axis}-${style}.woff2`;
|
|
22
|
+
if (f.variable) {
|
|
23
|
+
const files = [{ file: name("wght", "normal"), weight: "100 900", style: "normal" }];
|
|
24
|
+
if (f.italic)
|
|
25
|
+
files.push({ file: name("wght", "italic"), weight: "100 900", style: "italic" });
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
28
|
+
const files = [{ file: name("400", "normal"), weight: "400", style: "normal" }];
|
|
29
|
+
if (f.italic)
|
|
30
|
+
files.push({ file: name("400", "italic"), weight: "400", style: "italic" });
|
|
31
|
+
files.push({ file: name("700", "normal"), weight: "700", style: "normal" });
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
/** O `@font-face` de cada arquivo da fonte, apontando para `fonts/`. */
|
|
35
|
+
export const fontFaceCss = (f) => fontFiles(f)
|
|
36
|
+
.map((x) => `@font-face{font-family:"${f.family}";src:url(fonts/${x.file}) format("woff2");font-weight:${x.weight};font-style:${x.style};font-display:block}`)
|
|
37
|
+
.join("\n");
|
|
38
|
+
/** Valor de `font-family`: a fonte e, se não carregar, uma parecida do sistema. */
|
|
39
|
+
export const fontStack = (f) => `"${f.family}",${f.kind === "serif" ? 'Georgia,"Times New Roman",serif' : '"Segoe UI",system-ui,sans-serif'}`;
|
|
40
|
+
//# sourceMappingURL=fonts.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Níveis dos títulos do corpo de uma página no manual exportado, sem pular nível.
|
|
3
|
+
* O título mais alto da página vira o nível logo abaixo do título da página (`titleLevel + 1`),
|
|
4
|
+
* os outros mantêm a diferença entre si, e nenhum desce mais de um nível por vez.
|
|
5
|
+
* `depths` são os níveis do Markdown (1 a 6), na ordem em que aparecem.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exportLevels(depths: number[], titleLevel: number): number[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Níveis dos títulos do corpo de uma página no manual exportado, sem pular nível.
|
|
3
|
+
* O título mais alto da página vira o nível logo abaixo do título da página (`titleLevel + 1`),
|
|
4
|
+
* os outros mantêm a diferença entre si, e nenhum desce mais de um nível por vez.
|
|
5
|
+
* `depths` são os níveis do Markdown (1 a 6), na ordem em que aparecem.
|
|
6
|
+
*/
|
|
7
|
+
export function exportLevels(depths, titleLevel) {
|
|
8
|
+
if (!depths.length)
|
|
9
|
+
return [];
|
|
10
|
+
const top = Math.min(...depths);
|
|
11
|
+
let prev = titleLevel;
|
|
12
|
+
return depths.map((d) => {
|
|
13
|
+
prev = Math.min(titleLevel + 1 + d - top, prev + 1, 6);
|
|
14
|
+
return prev;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=headings.js.map
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { Collected } from "./collect.js";
|
|
2
2
|
import { type ImageInfo } from "./refs.js";
|
|
3
|
-
/** Cor da marca, quando o manual não escolheu outra. */
|
|
4
|
-
export declare const DEFAULT_ACCENT = "#2b4fe0";
|
|
5
3
|
/** Um arquivo HTML com o manual inteiro: sumário ao lado, uma seção por página, marcações sobre os prints. */
|
|
6
|
-
export declare function renderHtml(collected: Collected, images: Map<string, ImageInfo
|
|
4
|
+
export declare function renderHtml(collected: Collected, images: Map<string, ImageInfo>, logo?: string | null): string;
|
package/dist/core/export/html.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { Marked } from "marked";
|
|
2
|
+
import { ANNOTATION_CSS } from "./annotationCss.js";
|
|
3
|
+
import { arrowShape, legendRows } from "./annotationShapes.js";
|
|
4
|
+
import { CALLOUT_COLORS, readCallout } from "./callout.js";
|
|
5
|
+
import { exportLevels } from "./headings.js";
|
|
2
6
|
import { labelsFor } from "./labels.js";
|
|
3
7
|
import { assetFromSrc, headingAnchor, pageAnchor, pageLink, safeHref } from "./refs.js";
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
import { colorSpanOpen, TEXT_COLORS } from "./textColors.js";
|
|
9
|
+
import { fontFacesCss, themeCss } from "./theme.js";
|
|
6
10
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7
11
|
const pct = (n) => `${(n * 100).toFixed(3)}%`;
|
|
8
12
|
/** Um arquivo HTML com o manual inteiro: sumário ao lado, uma seção por página, marcações sobre os prints. */
|
|
9
|
-
export function renderHtml(collected, images) {
|
|
13
|
+
export function renderHtml(collected, images, logo = null) {
|
|
10
14
|
const { meta, pages } = collected;
|
|
11
15
|
const L = labelsFor(meta.language);
|
|
12
|
-
const accent = meta.theme?.accent ?? DEFAULT_ACCENT;
|
|
13
16
|
const toc = pages
|
|
14
17
|
.map((p) => `<li style="--d:${p.depth}"><a href="#${pageAnchor(p.id)}"><span class="num">${p.number}</span> ${esc(p.title)}</a></li>`)
|
|
15
18
|
.join("\n");
|
|
19
|
+
const logoImg = logo
|
|
20
|
+
? `<img class="logo" src="${esc(logo)}" alt="">
|
|
21
|
+
`
|
|
22
|
+
: "";
|
|
16
23
|
const sections = pages.map((p) => renderPage(p, pages, images, L)).join("\n");
|
|
17
24
|
return `<!doctype html>
|
|
18
25
|
<html lang="${esc(meta.language)}">
|
|
@@ -21,19 +28,20 @@ export function renderHtml(collected, images) {
|
|
|
21
28
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
22
29
|
<meta name="generator" content="Explicaí Studio">
|
|
23
30
|
<title>${esc(meta.title)}</title>
|
|
24
|
-
<style>${
|
|
31
|
+
<style>${fontFacesCss(meta.theme)}
|
|
32
|
+
${CSS}${themeCss(meta.theme)}</style>
|
|
25
33
|
</head>
|
|
26
34
|
<body>
|
|
27
35
|
<div class="layout">
|
|
28
36
|
<nav class="toc" aria-label="${L.toc}">
|
|
29
|
-
<p class="toc-title">${esc(meta.title)}</p>
|
|
37
|
+
${logoImg}<p class="toc-title">${esc(meta.title)}</p>
|
|
30
38
|
<ol>
|
|
31
39
|
${toc}
|
|
32
40
|
</ol>
|
|
33
41
|
</nav>
|
|
34
42
|
<main>
|
|
35
43
|
<header class="cover">
|
|
36
|
-
<h1>${esc(meta.title)}</h1>
|
|
44
|
+
${logoImg}<h1>${esc(meta.title)}</h1>
|
|
37
45
|
<p>${L.pages(pages.length)}</p>
|
|
38
46
|
</header>
|
|
39
47
|
${sections}
|
|
@@ -45,13 +53,23 @@ ${sections}
|
|
|
45
53
|
`;
|
|
46
54
|
}
|
|
47
55
|
function renderPage(page, pages, images, L) {
|
|
56
|
+
const titleLevel = Math.min(page.depth + 2, 3);
|
|
57
|
+
const depths = [];
|
|
58
|
+
const lexer = new Marked({ gfm: true });
|
|
59
|
+
lexer.walkTokens(lexer.lexer(page.body), (t) => {
|
|
60
|
+
if (t.type === "heading")
|
|
61
|
+
depths.push(t.depth);
|
|
62
|
+
});
|
|
63
|
+
const levels = exportLevels(depths, titleLevel);
|
|
64
|
+
let next = 0;
|
|
65
|
+
let openSpans = 0;
|
|
48
66
|
const marked = new Marked({
|
|
49
67
|
gfm: true,
|
|
50
68
|
renderer: {
|
|
51
|
-
// Os títulos
|
|
52
|
-
heading({ tokens,
|
|
53
|
-
const level =
|
|
54
|
-
return `<h${level} id="${headingAnchor(page.id, text)}">${this.parser.parseInline(tokens)}</h${level}>\n`;
|
|
69
|
+
// Os títulos ficam abaixo do título da página, sem pular nível (ver exportLevels).
|
|
70
|
+
heading({ tokens, text, depth }) {
|
|
71
|
+
const level = levels[next++] ?? 6;
|
|
72
|
+
return `<h${level} class="t${Math.min(depth, 3)}" id="${headingAnchor(page.id, text)}">${this.parser.parseInline(tokens)}</h${level}>\n`;
|
|
55
73
|
},
|
|
56
74
|
link({ href, tokens }) {
|
|
57
75
|
const target = pageLink(href, pages) ?? safeHref(href);
|
|
@@ -68,16 +86,36 @@ function renderPage(page, pages, images, L) {
|
|
|
68
86
|
return `<span class="missing">[${L.missingImage}: ${esc(text || href)}]</span>`;
|
|
69
87
|
return renderShot(info, text, pages, L);
|
|
70
88
|
},
|
|
89
|
+
// Alerta GFM vira painel com rótulo em texto; outra citação segue como estava.
|
|
90
|
+
blockquote({ tokens }) {
|
|
91
|
+
const callout = readCallout(tokens);
|
|
92
|
+
if (!callout)
|
|
93
|
+
return `<blockquote>
|
|
94
|
+
${this.parser.parse(tokens)}</blockquote>
|
|
95
|
+
`;
|
|
96
|
+
return `<div class="callout" data-callout="${callout.type}" role="note"><p class="callout-label">${L.callouts[callout.type]}</p>
|
|
97
|
+
${this.parser.parse(callout.body)}</div>
|
|
98
|
+
`;
|
|
99
|
+
},
|
|
71
100
|
// HTML colado no texto sai como texto: o manual exportado não roda código de ninguém.
|
|
101
|
+
// Só a cor do texto passa, e só com um nome da paleta (ver textColors.ts).
|
|
72
102
|
html({ text }) {
|
|
103
|
+
const color = colorSpanOpen(text);
|
|
104
|
+
if (color) {
|
|
105
|
+
openSpans++;
|
|
106
|
+
return `<span data-color="${color}">`;
|
|
107
|
+
}
|
|
108
|
+
if (text === "</span>" && openSpans > 0) {
|
|
109
|
+
openSpans--;
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
73
112
|
return esc(text);
|
|
74
113
|
},
|
|
75
114
|
},
|
|
76
115
|
});
|
|
77
116
|
const body = marked.parse(page.body, { async: false });
|
|
78
|
-
const level = Math.min(page.depth + 2, 3);
|
|
79
117
|
return `<section class="page" id="${pageAnchor(page.id)}">
|
|
80
|
-
<h${
|
|
118
|
+
<h${titleLevel} class="page-title"><span class="num">${page.number}</span> ${esc(page.title)}</h${titleLevel}>
|
|
81
119
|
<div class="page-body">${body}</div>
|
|
82
120
|
</section>`;
|
|
83
121
|
}
|
|
@@ -109,29 +147,32 @@ function renderShot(info, alt, pages, L) {
|
|
|
109
147
|
.join("");
|
|
110
148
|
const unit = Math.max(w, h);
|
|
111
149
|
const px = (p) => ({ x: p.x * w, y: p.y * h });
|
|
150
|
+
const f1 = (n) => n.toFixed(1);
|
|
112
151
|
const arrows = a.arrows.length
|
|
113
152
|
? `<svg class="arrows" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true">${a.arrows
|
|
114
153
|
.map(({ from, to }) => {
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
return `<line x1="${f.x.toFixed(1)}" y1="${f.y.toFixed(1)}" x2="${t.x.toFixed(1)}" y2="${t.y.toFixed(1)}" stroke-width="${(unit * 0.005).toFixed(1)}"/><polygon points="${t.x.toFixed(1)},${t.y.toFixed(1)} ${wing(0.45)} ${wing(-0.45)}"/>`;
|
|
154
|
+
const t = unit * 0.005;
|
|
155
|
+
const { shaft: s, head } = arrowShape(px(from), px(to), t);
|
|
156
|
+
const line = `x1="${f1(s.x1)}" y1="${f1(s.y1)}" x2="${f1(s.x2)}" y2="${f1(s.y2)}"`;
|
|
157
|
+
const pts = head.map((p) => `${f1(p.x)},${f1(p.y)}`).join(" ");
|
|
158
|
+
return `<line class="halo" ${line} stroke-width="${f1(t * 2.2)}"/><polygon class="halo" points="${pts}" stroke-width="${f1(t * 1.2)}"/><line ${line} stroke-width="${f1(t)}"/><polygon points="${pts}"/>`;
|
|
121
159
|
})
|
|
122
160
|
.join("")}</svg>`
|
|
123
161
|
: "";
|
|
124
162
|
const markers = a.markers
|
|
125
|
-
.map((m) => `<span class="mk" style="left:${pct(m.x)};top:${pct(m.y)}"
|
|
163
|
+
.map((m) => `<span class="mk" style="left:${pct(m.x)};top:${pct(m.y)}" data-n="${m.n}" aria-hidden="true"></span>`)
|
|
126
164
|
.join("");
|
|
127
|
-
const legend =
|
|
128
|
-
.
|
|
129
|
-
.map((
|
|
165
|
+
const legend = legendRows(a.markers)
|
|
166
|
+
.map((r) => {
|
|
167
|
+
const nums = r.badges.map((n) => `<span class="n">${n}</span>`).join("");
|
|
168
|
+
const text = r.label ? esc(r.label) : `<span class="none">${L.noDescription}</span>`;
|
|
169
|
+
return `<span class="row" role="listitem">${nums}<span>${text}</span></span>`;
|
|
170
|
+
})
|
|
130
171
|
.join("");
|
|
131
|
-
return `<span class="shot"><span class="frame" style="aspect-ratio:${w} / ${h}"><img src="${esc(info.asset)}" alt="${esc(alt)}" loading="lazy">${highlights}${arrows}${hotspots}${markers}</span>${legend ? `<span class="legend" role="list" aria-label="${L.legend}">${legend}</span>` : ""}</span>`;
|
|
172
|
+
return `<span class="shot${a.display ? ` shot-${a.display}` : ""}"><span class="frame" style="aspect-ratio:${w} / ${h}"><img src="${esc(info.asset)}" alt="${esc(alt)}" loading="lazy">${highlights}${arrows}${hotspots}${markers}</span>${legend ? `<span class="legend" role="list" aria-label="${L.legend}">${legend}</span>` : ""}</span>`;
|
|
132
173
|
}
|
|
133
174
|
const CSS = `
|
|
134
|
-
:root{--accent
|
|
175
|
+
:root{--accent:#2b4fe0;--anotacao:#2b4fe0;--anotacao-fundo:rgba(43,79,224,.08);--sobre-anotacao:#fff;--marca:#ffd43b;--marca-faixa:rgba(255,212,59,.55);--t-cover:#000;--t-page:#000;--t-h1:#000;--t-h2:#000;--t-h3:#000;--marca-faixa:rgba(255,212,59,.55);--tinta:#151a33;--grafite:#575e78;--linha:#dce0eb;--papel:#f5f6fa;
|
|
135
176
|
--texto:"Atkinson Hyperlegible Next","Segoe UI",system-ui,sans-serif;--display:"Archivo","Arial Narrow",system-ui,sans-serif;--mono:"JetBrains Mono",ui-monospace,Consolas,monospace}
|
|
136
177
|
*{box-sizing:border-box}
|
|
137
178
|
body{margin:0;background:#fff;color:var(--tinta);font:16px/1.65 var(--texto);-webkit-font-smoothing:antialiased}
|
|
@@ -145,11 +186,15 @@ body{margin:0;background:#fff;color:var(--tinta);font:16px/1.65 var(--texto);-we
|
|
|
145
186
|
.num{font-family:var(--mono);font-size:.8em;color:var(--accent);margin-right:4px}
|
|
146
187
|
main{max-width:52rem;padding:32px 40px 96px;margin:0 auto;width:100%}
|
|
147
188
|
.cover{padding:48px 0 32px;border-bottom:3px solid var(--marca);margin-bottom:24px}
|
|
148
|
-
.cover h1{margin:0;font:800 clamp(32px,5vw,48px)/1.1 var(--display);letter-spacing:-.02em}
|
|
189
|
+
.cover h1{margin:0;color:var(--t-cover);font:800 clamp(32px,5vw,48px)/1.1 var(--display);letter-spacing:-.02em}
|
|
190
|
+
.logo{display:block;width:auto;height:auto;object-fit:contain}
|
|
191
|
+
.cover .logo{max-height:96px;max-width:min(280px,100%);margin:0 0 24px}
|
|
192
|
+
.toc .logo{max-height:48px;max-width:100%;margin:0 0 16px}
|
|
149
193
|
.cover p{margin:8px 0 0;color:var(--grafite)}
|
|
150
194
|
.page{padding-top:32px;scroll-margin-top:16px}
|
|
151
|
-
.page-title{margin:0 0 12px;font:800 30px/1.2 var(--display);letter-spacing:-.01em}
|
|
195
|
+
.page-title{margin:0 0 12px;color:var(--t-page);font:800 30px/1.2 var(--display);letter-spacing:-.01em}
|
|
152
196
|
.page-body h3,.page-body h4,.page-body h5,.page-body h6{font-family:var(--display);line-height:1.25;margin:1.4em 0 .4em;scroll-margin-top:16px}
|
|
197
|
+
.page-body .t1{color:var(--t-h1)}.page-body .t2{color:var(--t-h2)}.page-body .t3{color:var(--t-h3)}
|
|
153
198
|
.page-body h3{font-size:22px}.page-body h4{font-size:19px}.page-body h5,.page-body h6{font-size:17px}
|
|
154
199
|
.page-body p{margin:.6em 0}
|
|
155
200
|
.page-body a{color:var(--accent);text-underline-offset:.2em}
|
|
@@ -165,19 +210,21 @@ main{max-width:52rem;padding:32px 40px 96px;margin:0 auto;width:100%}
|
|
|
165
210
|
.page-body>ol>li,.page-body li>ol>li{counter-increment:passo;position:relative}
|
|
166
211
|
.page-body>ol>li+li,.page-body li>ol>li+li{margin-top:.6em}
|
|
167
212
|
.page-body>ol>li::before,.page-body li>ol>li::before{content:counter(passo);position:absolute;left:-2.25em;top:.12em;display:grid;place-items:center;width:1.55em;height:1.55em;border-radius:50%;background:var(--accent);color:#fff;font:700 .82em var(--mono)}
|
|
168
|
-
.page-body li>blockquote{
|
|
213
|
+
.page-body li>blockquote{margin:.6em 0 0;padding:0;border:0;color:var(--tinta)}
|
|
214
|
+
.page-body li>blockquote>p{display:inline;background:linear-gradient(transparent 50%,var(--marca-faixa) 50%)}
|
|
215
|
+
.page-body li>blockquote>p>strong:first-child{font:700 .78em var(--mono);letter-spacing:.04em;text-transform:uppercase}
|
|
216
|
+
.page-body .callout{margin:.8em 0;padding:10px 14px;border:1px solid color-mix(in srgb,var(--c) 35%,#fff);border-left:4px solid var(--c);border-radius:3px;background:color-mix(in srgb,var(--c) 8%,#fff);color:var(--tinta)}
|
|
217
|
+
.page-body .callout>:last-child{margin-bottom:0}
|
|
218
|
+
.page-body .callout-label{margin:0 0 .3em;color:color-mix(in srgb,var(--c) 85%,#000);font:700 .78em var(--mono);letter-spacing:.06em;text-transform:uppercase}
|
|
219
|
+
${Object.entries(CALLOUT_COLORS)
|
|
220
|
+
.map(([type, hex]) => `.callout[data-callout="${type}"]{--c:${type === "note" ? "var(--accent)" : hex}}`)
|
|
221
|
+
.join("")}
|
|
169
222
|
.shot{display:block;margin:.6em 0}
|
|
170
|
-
|
|
171
|
-
.frame img{display:block;max-width:100%;height:auto;border:1px solid var(--linha);border-radius:8px}
|
|
172
|
-
.hl{position:absolute;border:2px solid var(--marca);background:rgba(255,212,59,.3);border-radius:3px}
|
|
173
|
-
.hot{position:absolute;border:2px dashed var(--anotacao);background:rgba(43,79,224,.08);border-radius:3px}
|
|
174
|
-
.arrows{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
|
|
175
|
-
.arrows line{stroke:var(--anotacao);stroke-linecap:round}
|
|
176
|
-
.arrows polygon{fill:var(--anotacao)}
|
|
177
|
-
.mk{position:absolute;transform:translate(-50%,-50%);display:grid;place-items:center;width:28px;height:28px;border-radius:50%;background:var(--anotacao);color:#fff;font:600 13px var(--mono);box-shadow:0 0 0 2px #fff,0 2px 6px rgba(21,26,51,.35)}
|
|
178
|
-
.legend{display:grid;gap:4px;margin-top:8px;font-size:15px}
|
|
179
|
-
.legend .n{display:inline-grid;place-items:center;width:20px;height:20px;margin-right:6px;border-radius:50%;background:var(--anotacao);color:#fff;font:600 11px var(--mono);vertical-align:1px}
|
|
223
|
+
${ANNOTATION_CSS}
|
|
180
224
|
.missing{color:#c8411f;font-style:italic}
|
|
225
|
+
${Object.entries(TEXT_COLORS)
|
|
226
|
+
.map(([name, hex]) => `[data-color="${name}"]{color:${hex}}`)
|
|
227
|
+
.join("")}
|
|
181
228
|
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
182
229
|
.colophon{margin-top:64px;padding-top:16px;border-top:1px solid var(--linha);color:var(--grafite);font-size:13px}
|
|
183
230
|
@media (max-width:800px){.layout{display:block}.toc{position:static;height:auto;border-right:0;border-bottom:1px solid var(--linha)}main{padding:24px 20px 64px}}
|
|
@@ -187,7 +234,7 @@ body{font-size:11.5pt}
|
|
|
187
234
|
.layout{display:block}.toc{display:none}main{max-width:none;padding:0}
|
|
188
235
|
.cover{padding:30vh 0 0;border:0;break-after:page}
|
|
189
236
|
.page{break-before:page;padding-top:0}
|
|
190
|
-
.shot,.page-body table,.page-body pre,.page-body li{break-inside:avoid}
|
|
237
|
+
.shot,.callout,.page-body table,.page-body pre,.page-body li{break-inside:avoid}
|
|
191
238
|
.page-body a{color:var(--tinta)}
|
|
192
239
|
.hot{border-style:dashed}
|
|
193
240
|
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { open, readFile } from "node:fs/promises";
|
|
2
|
+
const HEAD = 64 * 1024;
|
|
2
3
|
/**
|
|
3
4
|
* Largura e altura de um print lendo só o cabeçalho do arquivo (PNG, JPEG, GIF, WebP e SVG).
|
|
4
5
|
* A exportação precisa da proporção para desenhar setas sem entortar. Formato desconhecido: null.
|
|
5
6
|
*/
|
|
6
7
|
export async function imageSize(file) {
|
|
7
|
-
const b = await
|
|
8
|
+
const b = await head(file);
|
|
9
|
+
const size = parse(b);
|
|
10
|
+
// JPEG com EXIF grande: o marcador SOF pode estar além do começo. Lê o arquivo todo.
|
|
11
|
+
return size ?? (b.length === HEAD && b[0] === 0xff && b[1] === 0xd8 ? parse(await readFile(file)) : null);
|
|
12
|
+
}
|
|
13
|
+
async function head(file) {
|
|
14
|
+
const fh = await open(file, "r");
|
|
15
|
+
try {
|
|
16
|
+
const buf = Buffer.alloc(HEAD);
|
|
17
|
+
const { bytesRead } = await fh.read(buf, 0, HEAD, 0);
|
|
18
|
+
return buf.subarray(0, bytesRead);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await fh.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function parse(b) {
|
|
8
25
|
if (b.length > 24 && b.toString("ascii", 1, 4) === "PNG")
|
|
9
26
|
return { w: b.readUInt32BE(16), h: b.readUInt32BE(20) };
|
|
10
27
|
if (b.length >= 10 && b.toString("ascii", 0, 3) === "GIF")
|
|
@@ -45,7 +62,7 @@ function jpeg(b) {
|
|
|
45
62
|
}
|
|
46
63
|
function svg(text) {
|
|
47
64
|
const tag = /<svg[^>]*>/i.exec(text)?.[0] ?? "";
|
|
48
|
-
const num = (name) => Number.parseFloat(new RegExp(
|
|
65
|
+
const num = (name) => Number.parseFloat(new RegExp(`\\s${name}="([\\d.]+)`, "i").exec(tag)?.[1] ?? "");
|
|
49
66
|
const [w, h] = [num("width"), num("height")];
|
|
50
67
|
if (w > 0 && h > 0)
|
|
51
68
|
return { w, h };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ManualTheme } from "../types.js";
|
|
2
1
|
import { type CollectOptions } from "./collect.js";
|
|
2
|
+
import { type ThemePatch } from "./theme.js";
|
|
3
|
+
export { logoSizeError } from "./logo.js";
|
|
3
4
|
export { findBrowser } from "./pdf.js";
|
|
4
5
|
/** Pasta da exportação, dentro do manual. Cada exportação substitui a anterior do mesmo formato. */
|
|
5
6
|
export declare const EXPORT_DIR = "exportacao";
|
|
@@ -10,14 +11,19 @@ export interface ExportResult {
|
|
|
10
11
|
/** Arquivo principal, relativo à pasta do manual (`exportacao/html/index.html`). */
|
|
11
12
|
file: string;
|
|
12
13
|
pages: number;
|
|
13
|
-
/** Páginas que ficaram de fora e
|
|
14
|
+
/** Páginas que ficaram de fora e imagens que não existem, com o motivo. */
|
|
14
15
|
warnings: string[];
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Exporta o manual: HTML (um arquivo com sumário, para publicar ou abrir no navegador), PDF (impresso
|
|
18
|
-
* a partir desse HTML) ou Markdown (um arquivo só, para quem lê com IA). Usa a
|
|
19
|
-
*
|
|
19
|
+
* a partir desse HTML) ou Markdown (um arquivo só, para quem lê com IA). Usa a Versão do manual
|
|
20
|
+
* de cada página.
|
|
20
21
|
*/
|
|
21
22
|
export declare function exportManual(dir: string, format: ExportFormat, opts?: CollectOptions): Promise<ExportResult>;
|
|
22
|
-
/**
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Muda a aparência do manual exportado. Só os campos de `patch` mudam (`null` apaga o campo);
|
|
25
|
+
* cores sem contraste suficiente são recusadas.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setManualTheme(dir: string, patch: ThemePatch): Promise<void>;
|
|
28
|
+
/** Grava o logo em `assets/` (PNG, JPG ou SVG, até 1 MB) e o põe no tema. Devolve o caminho. */
|
|
29
|
+
export declare function setManualLogo(dir: string, fileName: string, data: Uint8Array): Promise<string>;
|