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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seta com ponta fechada. O tamanho da ponta é proporcional à espessura e encolhe em setas curtas
|
|
3
|
+
* (no máximo 70% do comprimento), sem perder a forma. `thickness` na mesma unidade de `from` e `to`.
|
|
4
|
+
*/
|
|
5
|
+
export function arrowShape(from, to, thickness) {
|
|
6
|
+
const dx = to.x - from.x;
|
|
7
|
+
const dy = to.y - from.y;
|
|
8
|
+
const len = Math.hypot(dx, dy);
|
|
9
|
+
const ux = len === 0 ? 1 : dx / len;
|
|
10
|
+
const uy = len === 0 ? 0 : dy / len;
|
|
11
|
+
const headLen = Math.min(thickness * 4.5, len * 0.7);
|
|
12
|
+
const half = headLen * 0.45;
|
|
13
|
+
const bx = to.x - ux * headLen;
|
|
14
|
+
const by = to.y - uy * headLen;
|
|
15
|
+
const inside = headLen * 0.2;
|
|
16
|
+
return {
|
|
17
|
+
shaft: { x1: from.x, y1: from.y, x2: bx + ux * inside, y2: by + uy * inside },
|
|
18
|
+
head: [to, { x: bx - uy * half, y: by + ux * half }, { x: bx + uy * half, y: by - ux * half }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Linhas da legenda por número; marcadores sem descrição em sequência dividem uma linha só. */
|
|
22
|
+
export function legendRows(markers) {
|
|
23
|
+
const rows = [];
|
|
24
|
+
for (const m of [...markers].sort((a, b) => a.n - b.n)) {
|
|
25
|
+
const label = (m.label ?? "").trim();
|
|
26
|
+
const last = rows.at(-1);
|
|
27
|
+
if (!label && last && !last.label)
|
|
28
|
+
last.numbers.push(m.n);
|
|
29
|
+
else
|
|
30
|
+
rows.push({ numbers: [m.n], badges: [], label });
|
|
31
|
+
}
|
|
32
|
+
for (const r of rows) {
|
|
33
|
+
const [first, last] = [r.numbers[0], r.numbers.at(-1)];
|
|
34
|
+
r.badges = !r.label && r.numbers.length >= 3 ? [`${first}–${last}`] : r.numbers.map(String);
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=annotationShapes.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Token } from "marked";
|
|
2
|
+
/** Cor de cada tipo de alerta no manual exportado (fundo branco). O de `note` segue a cor de destaque do manual. */
|
|
3
|
+
export declare const CALLOUT_COLORS: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Alerta GFM (`> [!TIP]` na primeira linha da citação): devolve o tipo e o corpo sem o marcador.
|
|
6
|
+
* Qualquer outra citação, inclusive `[!IMPORTANT]`, volta null e sai como citação comum.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readCallout(tokens: Token[]): {
|
|
9
|
+
type: string;
|
|
10
|
+
body: Token[];
|
|
11
|
+
} | null;
|
|
@@ -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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ManualMeta } from "../types.js";
|
|
2
|
+
export interface ExportPage {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
/** Posição na árvore, como no sumário: "2", "2.1". */
|
|
6
|
+
number: string;
|
|
7
|
+
depth: number;
|
|
8
|
+
/** Nome do arquivo (`cadastrar-cliente.md`), para achar a página pelos links `./arquivo.md`. */
|
|
9
|
+
fileName: string;
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Collected {
|
|
13
|
+
meta: ManualMeta;
|
|
14
|
+
pages: ExportPage[];
|
|
15
|
+
/** Páginas que ficaram de fora, com o motivo (arquivo apagado, versão sumida). */
|
|
16
|
+
warnings: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface CollectOptions {
|
|
19
|
+
/** Só páginas aprovadas ou publicadas. Subpágina aprovada entra mesmo com a mãe em rascunho. */
|
|
20
|
+
onlyApproved?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
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
|
+
* A numeração conta só as páginas que entram, então o sumário não fica com buracos.
|
|
26
|
+
*/
|
|
27
|
+
export declare function collectPages(dir: string, opts?: CollectOptions): Promise<Collected>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readManual } from "../manuals.js";
|
|
2
|
+
import { readPage } from "../pages.js";
|
|
3
|
+
import { readVersion } from "../versions.js";
|
|
4
|
+
import { labelsFor } from "./labels.js";
|
|
5
|
+
const APPROVED = ["aprovado", "publicado"];
|
|
6
|
+
/**
|
|
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
|
+
* A numeração conta só as páginas que entram, então o sumário não fica com buracos.
|
|
10
|
+
*/
|
|
11
|
+
export async function collectPages(dir, opts = {}) {
|
|
12
|
+
const meta = await readManual(dir);
|
|
13
|
+
const pages = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
// `count` é compartilhado com os filhos de uma página que ficou de fora: eles sobem para o lugar dela.
|
|
16
|
+
const walk = async (nodes, prefix, depth, count) => {
|
|
17
|
+
for (const node of nodes) {
|
|
18
|
+
let included = false;
|
|
19
|
+
let number = prefix;
|
|
20
|
+
try {
|
|
21
|
+
const current = await readPage(dir, node.id);
|
|
22
|
+
if (!opts.onlyApproved || APPROVED.includes(current.status)) {
|
|
23
|
+
const source = node.published ? await readVersion(dir, node.id, node.published) : current;
|
|
24
|
+
count.n += 1;
|
|
25
|
+
number = prefix ? `${prefix}.${count.n}` : String(count.n);
|
|
26
|
+
pages.push({
|
|
27
|
+
id: node.id,
|
|
28
|
+
title: source.title || labelsFor(meta.language).untitled,
|
|
29
|
+
number,
|
|
30
|
+
depth,
|
|
31
|
+
fileName: node.file.split("/").pop() ?? node.file,
|
|
32
|
+
body: source.body,
|
|
33
|
+
});
|
|
34
|
+
included = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
warnings.push(`${node.file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
if (included)
|
|
41
|
+
await walk(node.children, number, depth + 1, { n: 0 });
|
|
42
|
+
else
|
|
43
|
+
await walk(node.children, prefix, depth, count);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
await walk(meta.pages, "", 0, { n: 0 });
|
|
47
|
+
return { meta, pages, warnings };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Collected } from "./collect.js";
|
|
2
|
+
import { type ImageInfo } from "./refs.js";
|
|
3
|
+
/** Um arquivo HTML com o manual inteiro: sumário ao lado, uma seção por página, marcações sobre os prints. */
|
|
4
|
+
export declare function renderHtml(collected: Collected, images: Map<string, ImageInfo>, logo?: string | null): string;
|
|
@@ -0,0 +1,242 @@
|
|
|
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";
|
|
6
|
+
import { labelsFor } from "./labels.js";
|
|
7
|
+
import { assetFromSrc, headingAnchor, pageAnchor, pageLink, safeHref } from "./refs.js";
|
|
8
|
+
import { colorSpanOpen, TEXT_COLORS } from "./textColors.js";
|
|
9
|
+
import { fontFacesCss, themeCss } from "./theme.js";
|
|
10
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11
|
+
const pct = (n) => `${(n * 100).toFixed(3)}%`;
|
|
12
|
+
/** Um arquivo HTML com o manual inteiro: sumário ao lado, uma seção por página, marcações sobre os prints. */
|
|
13
|
+
export function renderHtml(collected, images, logo = null) {
|
|
14
|
+
const { meta, pages } = collected;
|
|
15
|
+
const L = labelsFor(meta.language);
|
|
16
|
+
const toc = pages
|
|
17
|
+
.map((p) => `<li style="--d:${p.depth}"><a href="#${pageAnchor(p.id)}"><span class="num">${p.number}</span> ${esc(p.title)}</a></li>`)
|
|
18
|
+
.join("\n");
|
|
19
|
+
const logoImg = logo
|
|
20
|
+
? `<img class="logo" src="${esc(logo)}" alt="">
|
|
21
|
+
`
|
|
22
|
+
: "";
|
|
23
|
+
const sections = pages.map((p) => renderPage(p, pages, images, L)).join("\n");
|
|
24
|
+
return `<!doctype html>
|
|
25
|
+
<html lang="${esc(meta.language)}">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
+
<meta name="generator" content="Explicaí Studio">
|
|
30
|
+
<title>${esc(meta.title)}</title>
|
|
31
|
+
<style>${fontFacesCss(meta.theme)}
|
|
32
|
+
${CSS}${themeCss(meta.theme)}</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div class="layout">
|
|
36
|
+
<nav class="toc" aria-label="${L.toc}">
|
|
37
|
+
${logoImg}<p class="toc-title">${esc(meta.title)}</p>
|
|
38
|
+
<ol>
|
|
39
|
+
${toc}
|
|
40
|
+
</ol>
|
|
41
|
+
</nav>
|
|
42
|
+
<main>
|
|
43
|
+
<header class="cover">
|
|
44
|
+
${logoImg}<h1>${esc(meta.title)}</h1>
|
|
45
|
+
<p>${L.pages(pages.length)}</p>
|
|
46
|
+
</header>
|
|
47
|
+
${sections}
|
|
48
|
+
<footer class="colophon">${L.colophon}</footer>
|
|
49
|
+
</main>
|
|
50
|
+
</div>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
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;
|
|
66
|
+
const marked = new Marked({
|
|
67
|
+
gfm: true,
|
|
68
|
+
renderer: {
|
|
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`;
|
|
73
|
+
},
|
|
74
|
+
link({ href, tokens }) {
|
|
75
|
+
const target = pageLink(href, pages) ?? safeHref(href);
|
|
76
|
+
const text = this.parser.parseInline(tokens);
|
|
77
|
+
if (!target)
|
|
78
|
+
return text;
|
|
79
|
+
const external = /^https?:/i.test(target) ? ' rel="noopener noreferrer"' : "";
|
|
80
|
+
return `<a href="${esc(target)}"${external}>${text}</a>`;
|
|
81
|
+
},
|
|
82
|
+
image({ href, text }) {
|
|
83
|
+
const asset = assetFromSrc(href);
|
|
84
|
+
const info = asset ? images.get(asset) : undefined;
|
|
85
|
+
if (!info)
|
|
86
|
+
return `<span class="missing">[${L.missingImage}: ${esc(text || href)}]</span>`;
|
|
87
|
+
return renderShot(info, text, pages, L);
|
|
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
|
+
},
|
|
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).
|
|
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
|
+
}
|
|
112
|
+
return esc(text);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const body = marked.parse(page.body, { async: false });
|
|
117
|
+
return `<section class="page" id="${pageAnchor(page.id)}">
|
|
118
|
+
<h${titleLevel} class="page-title"><span class="num">${page.number}</span> ${esc(page.title)}</h${titleLevel}>
|
|
119
|
+
<div class="page-body">${body}</div>
|
|
120
|
+
</section>`;
|
|
121
|
+
}
|
|
122
|
+
function hotspotHref(target, pages) {
|
|
123
|
+
if (target.type === "url") {
|
|
124
|
+
const href = safeHref(target.href);
|
|
125
|
+
return href ? { href, label: target.href } : null;
|
|
126
|
+
}
|
|
127
|
+
const page = pages.find((p) => p.id === target.pageId);
|
|
128
|
+
if (!page)
|
|
129
|
+
return null;
|
|
130
|
+
const href = target.type === "anchor" ? `#${headingAnchor(page.id, target.anchor)}` : `#${pageAnchor(page.id)}`;
|
|
131
|
+
return { href, label: page.title };
|
|
132
|
+
}
|
|
133
|
+
/** O print com as marcações por cima (posições em %, acompanham o tamanho) e a legenda embaixo. */
|
|
134
|
+
function renderShot(info, alt, pages, L) {
|
|
135
|
+
const a = info.annotations;
|
|
136
|
+
const w = info.size?.w ?? 1000;
|
|
137
|
+
const h = info.size?.h ?? 1000;
|
|
138
|
+
const box = (r) => `left:${pct(r.x)};top:${pct(r.y)};width:${pct(r.w)};height:${pct(r.h)}`;
|
|
139
|
+
const highlights = a.highlights.map((r) => `<span class="hl" style="${box(r)}"></span>`).join("");
|
|
140
|
+
const hotspots = a.hotspots
|
|
141
|
+
.map((r) => {
|
|
142
|
+
const link = hotspotHref(r.target, pages);
|
|
143
|
+
return link
|
|
144
|
+
? `<a class="hot" href="${esc(link.href)}" style="${box(r)}"><span class="sr">${L.goTo} ${esc(link.label)}</span></a>`
|
|
145
|
+
: "";
|
|
146
|
+
})
|
|
147
|
+
.join("");
|
|
148
|
+
const unit = Math.max(w, h);
|
|
149
|
+
const px = (p) => ({ x: p.x * w, y: p.y * h });
|
|
150
|
+
const f1 = (n) => n.toFixed(1);
|
|
151
|
+
const arrows = a.arrows.length
|
|
152
|
+
? `<svg class="arrows" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" aria-hidden="true">${a.arrows
|
|
153
|
+
.map(({ from, to }) => {
|
|
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}"/>`;
|
|
159
|
+
})
|
|
160
|
+
.join("")}</svg>`
|
|
161
|
+
: "";
|
|
162
|
+
const markers = a.markers
|
|
163
|
+
.map((m) => `<span class="mk" style="left:${pct(m.x)};top:${pct(m.y)}" data-n="${m.n}" aria-hidden="true"></span>`)
|
|
164
|
+
.join("");
|
|
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
|
+
})
|
|
171
|
+
.join("");
|
|
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>`;
|
|
173
|
+
}
|
|
174
|
+
const CSS = `
|
|
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;
|
|
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}
|
|
177
|
+
*{box-sizing:border-box}
|
|
178
|
+
body{margin:0;background:#fff;color:var(--tinta);font:16px/1.65 var(--texto);-webkit-font-smoothing:antialiased}
|
|
179
|
+
.layout{display:grid;grid-template-columns:17rem minmax(0,1fr)}
|
|
180
|
+
.toc{position:sticky;top:0;height:100vh;overflow:auto;padding:24px 16px;background:var(--papel);border-right:1px solid var(--linha)}
|
|
181
|
+
.toc-title{margin:0 0 12px;font:800 18px/1.2 var(--display)}
|
|
182
|
+
.toc ol{list-style:none;margin:0;padding:0}
|
|
183
|
+
.toc li{padding-left:calc(var(--d) * 14px)}
|
|
184
|
+
.toc a{display:block;padding:4px 6px;border-radius:6px;color:var(--tinta);text-decoration:none;font-size:14px}
|
|
185
|
+
.toc a:hover{background:#e9ecf5}
|
|
186
|
+
.num{font-family:var(--mono);font-size:.8em;color:var(--accent);margin-right:4px}
|
|
187
|
+
main{max-width:52rem;padding:32px 40px 96px;margin:0 auto;width:100%}
|
|
188
|
+
.cover{padding:48px 0 32px;border-bottom:3px solid var(--marca);margin-bottom:24px}
|
|
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}
|
|
193
|
+
.cover p{margin:8px 0 0;color:var(--grafite)}
|
|
194
|
+
.page{padding-top:32px;scroll-margin-top:16px}
|
|
195
|
+
.page-title{margin:0 0 12px;color:var(--t-page);font:800 30px/1.2 var(--display);letter-spacing:-.01em}
|
|
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)}
|
|
198
|
+
.page-body h3{font-size:22px}.page-body h4{font-size:19px}.page-body h5,.page-body h6{font-size:17px}
|
|
199
|
+
.page-body p{margin:.6em 0}
|
|
200
|
+
.page-body a{color:var(--accent);text-underline-offset:.2em}
|
|
201
|
+
.page-body code{font:.88em var(--mono);background:var(--papel);border:1px solid var(--linha);border-radius:4px;padding:.05em .35em}
|
|
202
|
+
.page-body pre{font:14px/1.5 var(--mono);background:var(--papel);border:1px solid var(--linha);border-radius:8px;padding:12px 14px;overflow-x:auto}
|
|
203
|
+
.page-body pre code{background:none;border:0;padding:0}
|
|
204
|
+
.page-body blockquote{margin:.6em 0;padding:2px 0 2px 16px;border-left:2px solid var(--linha);color:var(--grafite)}
|
|
205
|
+
.page-body table{border-collapse:collapse;width:100%;margin:.8em 0;font-size:15px}
|
|
206
|
+
.page-body th,.page-body td{border:1px solid var(--linha);padding:6px 10px;text-align:left;vertical-align:top}
|
|
207
|
+
.page-body th{background:var(--papel)}
|
|
208
|
+
.page-body ul{padding-left:1.4em}
|
|
209
|
+
.page-body>ol,.page-body li>ol{list-style:none;counter-reset:passo;padding-left:2.25em}
|
|
210
|
+
.page-body>ol>li,.page-body li>ol>li{counter-increment:passo;position:relative}
|
|
211
|
+
.page-body>ol>li+li,.page-body li>ol>li+li{margin-top:.6em}
|
|
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)}
|
|
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("")}
|
|
222
|
+
.shot{display:block;margin:.6em 0}
|
|
223
|
+
${ANNOTATION_CSS}
|
|
224
|
+
.missing{color:#c8411f;font-style:italic}
|
|
225
|
+
${Object.entries(TEXT_COLORS)
|
|
226
|
+
.map(([name, hex]) => `[data-color="${name}"]{color:${hex}}`)
|
|
227
|
+
.join("")}
|
|
228
|
+
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
229
|
+
.colophon{margin-top:64px;padding-top:16px;border-top:1px solid var(--linha);color:var(--grafite);font-size:13px}
|
|
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}}
|
|
231
|
+
@media print{
|
|
232
|
+
@page{margin:18mm 16mm}
|
|
233
|
+
body{font-size:11.5pt}
|
|
234
|
+
.layout{display:block}.toc{display:none}main{max-width:none;padding:0}
|
|
235
|
+
.cover{padding:30vh 0 0;border:0;break-after:page}
|
|
236
|
+
.page{break-before:page;padding-top:0}
|
|
237
|
+
.shot,.callout,.page-body table,.page-body pre,.page-body li{break-inside:avoid}
|
|
238
|
+
.page-body a{color:var(--tinta)}
|
|
239
|
+
.hot{border-style:dashed}
|
|
240
|
+
}
|
|
241
|
+
`;
|
|
242
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Size {
|
|
2
|
+
w: number;
|
|
3
|
+
h: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Largura e altura de um print lendo só o cabeçalho do arquivo (PNG, JPEG, GIF, WebP e SVG).
|
|
7
|
+
* A exportação precisa da proporção para desenhar setas sem entortar. Formato desconhecido: null.
|
|
8
|
+
*/
|
|
9
|
+
export declare function imageSize(file: string): Promise<Size | null>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { open, readFile } from "node:fs/promises";
|
|
2
|
+
const HEAD = 64 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Largura e altura de um print lendo só o cabeçalho do arquivo (PNG, JPEG, GIF, WebP e SVG).
|
|
5
|
+
* A exportação precisa da proporção para desenhar setas sem entortar. Formato desconhecido: null.
|
|
6
|
+
*/
|
|
7
|
+
export async function imageSize(file) {
|
|
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) {
|
|
25
|
+
if (b.length > 24 && b.toString("ascii", 1, 4) === "PNG")
|
|
26
|
+
return { w: b.readUInt32BE(16), h: b.readUInt32BE(20) };
|
|
27
|
+
if (b.length >= 10 && b.toString("ascii", 0, 3) === "GIF")
|
|
28
|
+
return { w: b.readUInt16LE(6), h: b.readUInt16LE(8) };
|
|
29
|
+
if (b.length > 30 && b.toString("ascii", 0, 4) === "RIFF" && b.toString("ascii", 8, 12) === "WEBP")
|
|
30
|
+
return webp(b);
|
|
31
|
+
if (b[0] === 0xff && b[1] === 0xd8)
|
|
32
|
+
return jpeg(b);
|
|
33
|
+
if (b.toString("utf8", 0, 512).includes("<svg"))
|
|
34
|
+
return svg(b.toString("utf8"));
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function webp(b) {
|
|
38
|
+
const chunk = b.toString("ascii", 12, 16);
|
|
39
|
+
if (chunk === "VP8 ")
|
|
40
|
+
return { w: b.readUInt16LE(26) & 0x3fff, h: b.readUInt16LE(28) & 0x3fff };
|
|
41
|
+
if (chunk === "VP8L") {
|
|
42
|
+
const bits = b.readUInt32LE(21);
|
|
43
|
+
return { w: (bits & 0x3fff) + 1, h: ((bits >> 14) & 0x3fff) + 1 };
|
|
44
|
+
}
|
|
45
|
+
if (chunk === "VP8X")
|
|
46
|
+
return { w: b.readUIntLE(24, 3) + 1, h: b.readUIntLE(27, 3) + 1 };
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function jpeg(b) {
|
|
50
|
+
let i = 2;
|
|
51
|
+
while (i + 9 < b.length) {
|
|
52
|
+
if (b[i] !== 0xff)
|
|
53
|
+
return null;
|
|
54
|
+
const marker = b[i + 1] ?? 0;
|
|
55
|
+
// SOF0 a SOF15, menos DHT (C4), JPG (C8) e DAC (CC): é onde ficam as dimensões.
|
|
56
|
+
if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
|
|
57
|
+
return { h: b.readUInt16BE(i + 5), w: b.readUInt16BE(i + 7) };
|
|
58
|
+
}
|
|
59
|
+
i += 2 + b.readUInt16BE(i + 2);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function svg(text) {
|
|
64
|
+
const tag = /<svg[^>]*>/i.exec(text)?.[0] ?? "";
|
|
65
|
+
const num = (name) => Number.parseFloat(new RegExp(`\\s${name}="([\\d.]+)`, "i").exec(tag)?.[1] ?? "");
|
|
66
|
+
const [w, h] = [num("width"), num("height")];
|
|
67
|
+
if (w > 0 && h > 0)
|
|
68
|
+
return { w, h };
|
|
69
|
+
const box = /viewBox="[\d.\s-]*?([\d.]+)\s+([\d.]+)"/i.exec(tag);
|
|
70
|
+
return box ? { w: Number(box[1]), h: Number(box[2]) } : null;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=imageSize.js.map
|