sagadeck 1.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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// HTML -> PowerPoint EDITÁVEL.
|
|
2
|
+
// Lê a posição real de cada elemento no navegador e recria no .pptx:
|
|
3
|
+
// texto -> caixa de texto nativa (mesma fonte/tamanho/cor/espaçamento)
|
|
4
|
+
// caixas -> formas nativas (retângulo, arredondado, elipse)
|
|
5
|
+
// SVG/figuras/widgets -> imagem PNG 2x com fundo transparente
|
|
6
|
+
// gráficos (opcional --native-charts) -> gráfico nativo do PowerPoint
|
|
7
|
+
// cliques (data-step/data-exit) -> animações de entrada/saída no mesmo clique
|
|
8
|
+
// transição fade entre slides + notas do apresentador
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import PptxGenJS from "pptxgenjs";
|
|
11
|
+
import JSZip from "jszip";
|
|
12
|
+
import { openDeck } from "./browser.js";
|
|
13
|
+
import { pptxFontMap } from "../themes.js";
|
|
14
|
+
import { notesPlain } from "../markup.js";
|
|
15
|
+
|
|
16
|
+
const PX = 1 / 144; // 1920 px = 13,333 in
|
|
17
|
+
const PT = 0.5; // 1 px (na tela de 1920) = 0,5 pt
|
|
18
|
+
|
|
19
|
+
// ------------------------------------------------------------------ coleta no navegador
|
|
20
|
+
function collectInPage([idx, nativeCharts]) {
|
|
21
|
+
const slide = document.querySelectorAll("#stage > .slide")[idx];
|
|
22
|
+
document.querySelectorAll("[data-pid]").forEach((e) => e.removeAttribute("data-pid"));
|
|
23
|
+
const cv = document.createElement("canvas"); cv.width = cv.height = 1;
|
|
24
|
+
const cx = cv.getContext("2d", { willReadFrequently: true });
|
|
25
|
+
const rgba = (c) => {
|
|
26
|
+
if (!c || c === "transparent") return { hex: "000000", a: 0 };
|
|
27
|
+
cx.clearRect(0, 0, 1, 1); cx.fillStyle = "#000"; cx.fillStyle = c; cx.fillRect(0, 0, 1, 1);
|
|
28
|
+
const d = cx.getImageData(0, 0, 1, 1).data;
|
|
29
|
+
if (d[3] === 0) return { hex: "000000", a: 0 };
|
|
30
|
+
const un = (v) => Math.round(Math.min(255, (v * 255) / d[3]));
|
|
31
|
+
return { hex: [un(d[0]), un(d[1]), un(d[2])].map((v) => v.toString(16).padStart(2, "0")).join("").toUpperCase(), a: d[3] / 255 };
|
|
32
|
+
};
|
|
33
|
+
const sr = slide.getBoundingClientRect();
|
|
34
|
+
const box = (r) => ({ x: r.left - sr.left, y: r.top - sr.top, w: r.width, h: r.height });
|
|
35
|
+
const INLINE = new Set(["B", "I", "EM", "STRONG", "MARK", "SPAN", "A", "S", "CODE", "BR", "SUB", "SUP", "U", "SMALL"]);
|
|
36
|
+
let pid = 0;
|
|
37
|
+
const items = [];
|
|
38
|
+
|
|
39
|
+
const stepOf = (e) => {
|
|
40
|
+
let s = 0, ex = null;
|
|
41
|
+
for (let p = e; p && p !== slide; p = p.parentElement) {
|
|
42
|
+
if (p.dataset && p.dataset.step) s = Math.max(s, +p.dataset.step);
|
|
43
|
+
if (p.dataset && p.dataset.exit && ex == null) ex = +p.dataset.exit;
|
|
44
|
+
}
|
|
45
|
+
return { step: s, exit: ex };
|
|
46
|
+
};
|
|
47
|
+
const opac = (e) => { let o = 1; for (let p = e; p && p !== slide.parentElement; p = p.parentElement) o *= +getComputedStyle(p).opacity; return o; };
|
|
48
|
+
const isLeaf = (e) => {
|
|
49
|
+
const d = getComputedStyle(e).display;
|
|
50
|
+
if ((d.includes("flex") || d.includes("grid")) && e.children.length) return false;
|
|
51
|
+
let has = false;
|
|
52
|
+
for (const c of e.childNodes) {
|
|
53
|
+
if (c.nodeType === 3) { if (c.textContent.trim()) has = true; }
|
|
54
|
+
else if (c.nodeType === 1) {
|
|
55
|
+
if (!INLINE.has(c.tagName)) return false;
|
|
56
|
+
if (c.querySelector && c.querySelector("svg,img,div,canvas")) return false;
|
|
57
|
+
if (c.textContent.trim()) has = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return has;
|
|
61
|
+
};
|
|
62
|
+
const transformed = (cs) => (cs.transform && cs.transform !== "none" && !/^matrix\(1, 0, 0, 1,/.test(cs.transform)) || (cs.writingMode && cs.writingMode !== "horizontal-tb");
|
|
63
|
+
// área visual real de um SVG (desenhos podem "vazar" da caixa com overflow:visible)
|
|
64
|
+
const visualRect = (e) => {
|
|
65
|
+
let L = Infinity, T = Infinity, Rr = -Infinity, B = -Infinity;
|
|
66
|
+
const add = (r) => { if (r.width || r.height) { L = Math.min(L, r.left); T = Math.min(T, r.top); Rr = Math.max(Rr, r.right); B = Math.max(B, r.bottom); } };
|
|
67
|
+
add(e.getBoundingClientRect());
|
|
68
|
+
if (e.tagName.toLowerCase() === "svg") e.querySelectorAll("*").forEach((c) => { if (c.getBoundingClientRect && !["defs", "marker", "clippath", "lineargradient", "filter"].includes(c.tagName.toLowerCase()) && !c.closest("defs")) add(c.getBoundingClientRect()); });
|
|
69
|
+
L = Math.max(L, sr.left); T = Math.max(T, sr.top); Rr = Math.min(Rr, sr.right); B = Math.min(B, sr.bottom);
|
|
70
|
+
return { left: L, top: T, width: Rr - L, height: B - T, right: Rr, bottom: B };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Texto em "runs", com as MESMAS quebras de linha que o navegador fez (palavra a palavra).
|
|
74
|
+
function runsOf(leaf, lh) {
|
|
75
|
+
const paras = [[]];
|
|
76
|
+
const baseW = +getComputedStyle(leaf).fontWeight;
|
|
77
|
+
const baseItalic = getComputedStyle(leaf).fontStyle === "italic";
|
|
78
|
+
const rng = document.createRange();
|
|
79
|
+
let lastTop = null, pendingSpace = false;
|
|
80
|
+
const styleOf = (el) => {
|
|
81
|
+
const cs = getComputedStyle(el);
|
|
82
|
+
const faceEl = el.closest('[class*="f-"]');
|
|
83
|
+
const role = faceEl ? ([...faceEl.classList].find((k) => /^f-(display|heading|body|label|mono|quote)$/.test(k)) || "").slice(2) : "";
|
|
84
|
+
// link para outro slide (goto, [texto](#id), Voltar): vira "slide:N" e, no PowerPoint, salto para o slide
|
|
85
|
+
const go = el.closest("[data-goto]");
|
|
86
|
+
const link = go ? null : el.closest("a");
|
|
87
|
+
const goHref = go ? (() => { const all = [...document.querySelectorAll("#stage > .slide")], t = String(go.dataset.goto), j = all.findIndex((x) => x.dataset.id === t); const n = j >= 0 ? j : /^\d+$/.test(t) ? +t - 1 : -1; return n >= 0 ? `slide:${n}` : null; })() : null;
|
|
88
|
+
const col = rgba(cs.color);
|
|
89
|
+
return {
|
|
90
|
+
color: col.hex, alpha: col.a, size: parseFloat(cs.fontSize), weight: +cs.fontWeight, baseW,
|
|
91
|
+
italic: cs.fontStyle === "italic", baseItalic, family: cs.fontFamily.split(",")[0].replace(/["']/g, "").trim(), role,
|
|
92
|
+
spacing: parseFloat(cs.letterSpacing) || 0, upper: cs.textTransform === "uppercase", stretch: parseFloat(cs.fontStretch) || 100,
|
|
93
|
+
strike: cs.textDecorationLine.includes("line-through"), underline: !link && cs.textDecorationLine.includes("underline"),
|
|
94
|
+
href: goHref || (link ? link.href : null),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const same = (a, b) => ["color", "alpha", "size", "weight", "italic", "family", "role", "spacing", "strike", "underline", "href"].every((k) => a[k] === b[k]);
|
|
98
|
+
const push = (txt, st, newLine) => {
|
|
99
|
+
if (newLine && paras[paras.length - 1].length) paras.push([]);
|
|
100
|
+
const P = paras[paras.length - 1], prev = P[P.length - 1];
|
|
101
|
+
if (prev && same(prev, st)) prev.text += txt; else P.push({ ...st, text: txt });
|
|
102
|
+
};
|
|
103
|
+
const walk = (n) => {
|
|
104
|
+
for (const c of n.childNodes) {
|
|
105
|
+
if (c.nodeType === 3) {
|
|
106
|
+
const raw = c.textContent, st = styleOf(c.parentElement);
|
|
107
|
+
const re = /\S+/g; let m, lastEnd = 0;
|
|
108
|
+
while ((m = re.exec(raw))) {
|
|
109
|
+
if (m.index > lastEnd) pendingSpace = true;
|
|
110
|
+
rng.setStart(c, m.index); rng.setEnd(c, m.index + m[0].length);
|
|
111
|
+
const rects = rng.getClientRects();
|
|
112
|
+
const top = rects.length ? rects[0].top : lastTop;
|
|
113
|
+
const newLine = lastTop != null && top != null && top > lastTop + lh * 0.5;
|
|
114
|
+
let word = st.upper ? m[0].toUpperCase() : m[0];
|
|
115
|
+
const lineHasText = paras[paras.length - 1].length && !newLine;
|
|
116
|
+
push((pendingSpace && lineHasText ? " " : "") + word, st, newLine);
|
|
117
|
+
pendingSpace = false; lastTop = top; lastEnd = m.index + m[0].length;
|
|
118
|
+
}
|
|
119
|
+
if (raw.length > lastEnd && /\s/.test(raw.slice(lastEnd))) pendingSpace = true;
|
|
120
|
+
} else if (c.nodeType === 1) {
|
|
121
|
+
if (c.tagName === "BR") { paras.push([]); lastTop = null; pendingSpace = false; continue; }
|
|
122
|
+
const blk = getComputedStyle(c).display === "block";
|
|
123
|
+
if (blk && paras[paras.length - 1].length) { paras.push([]); lastTop = null; pendingSpace = false; }
|
|
124
|
+
walk(c);
|
|
125
|
+
if (blk) { paras.push([]); lastTop = null; pendingSpace = false; }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
walk(leaf);
|
|
130
|
+
return paras.filter((p) => p.length);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function visit(e) {
|
|
134
|
+
const cs = getComputedStyle(e);
|
|
135
|
+
if (cs.display === "none" || cs.visibility === "hidden" || +cs.opacity === 0) return;
|
|
136
|
+
const r = e.getBoundingClientRect();
|
|
137
|
+
if (r.width < 1 || r.height < 1) { if (e.children.length) [...e.children].forEach(visit); return; }
|
|
138
|
+
const tag = e.tagName;
|
|
139
|
+
const st = stepOf(e);
|
|
140
|
+
const tagIt = (o) => { e.setAttribute("data-pid", String(++pid)); items.push({ pid, ...box(r), ...st, opacity: opac(e), ...o }); };
|
|
141
|
+
|
|
142
|
+
// gráfico nativo
|
|
143
|
+
if (nativeCharts && tag.toLowerCase() === "svg" && e.classList.contains("chart")) {
|
|
144
|
+
const spec = JSON.parse(e.dataset.chart || "{}");
|
|
145
|
+
if (["bar", "column", "line", "donut"].includes(spec.type)) {
|
|
146
|
+
const sel = spec.type === "line" ? ".draw" : spec.type === "donut" ? ".arc" : spec.type === "bar" ? ".gx" : ".gy";
|
|
147
|
+
const colors = [...e.querySelectorAll(sel)].map((n) => rgba(getComputedStyle(n)[spec.type === "line" || spec.type === "donut" ? "stroke" : "fill"]).hex);
|
|
148
|
+
const fg = rgba(getComputedStyle(slide).color).hex;
|
|
149
|
+
tagIt({ kind: "chart", spec, colors, fg });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// tabela de verdade (src/table.js): tabela nativa do PowerPoint, editável, com as cores e larguras da tela
|
|
154
|
+
if (tag === "TABLE" && e.classList.contains("dtable")) {
|
|
155
|
+
const trs = [...e.rows];
|
|
156
|
+
const rows = trs.map((tr) => [...tr.cells].map((c) => {
|
|
157
|
+
const ccs = getComputedStyle(c);
|
|
158
|
+
let bg = rgba(ccs.backgroundColor);
|
|
159
|
+
if (bg.a < 0.01) bg = rgba(getComputedStyle(tr).backgroundColor);
|
|
160
|
+
const col = rgba(ccs.color), bb = rgba(ccs.borderBottomColor);
|
|
161
|
+
const run = { family: ccs.fontFamily.split(",")[0].replace(/["']/g, "").trim(), weight: +ccs.fontWeight, baseW: 400, italic: ccs.fontStyle === "italic", baseItalic: false, role: "body", stretch: 100 };
|
|
162
|
+
return { text: c.innerText.trim(), run, color: col.hex, size: parseFloat(ccs.fontSize), fill: bg.a > 0.01 ? bg.hex : null, fillA: bg.a, align: ccs.textAlign === "right" || ccs.textAlign === "end" ? "right" : ccs.textAlign === "center" ? "center" : "left",
|
|
163
|
+
pad: [ccs.paddingTop, ccs.paddingRight, ccs.paddingBottom, ccs.paddingLeft].map(parseFloat), line: parseFloat(ccs.borderBottomWidth) > 0 && bb.a > 0.05 ? { color: bb.hex, w: parseFloat(ccs.borderBottomWidth) } : null };
|
|
164
|
+
}));
|
|
165
|
+
const colW = trs.length ? [...trs[0].cells].map((c) => c.getBoundingClientRect().width) : [];
|
|
166
|
+
const rowH = trs.map((tr) => tr.getBoundingClientRect().height);
|
|
167
|
+
tagIt({ kind: "table", rows, colW, rowH });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// coisas que viram imagem
|
|
171
|
+
if (tag.toLowerCase() === "svg" || tag === "IMG" || tag === "CANVAS" || tag === "VIDEO" ||
|
|
172
|
+
e.classList.contains("katex") || (isLeaf(e) && e.querySelector(".katex")) || e.classList.contains("widget") || e.classList.contains("timer") || e.classList.contains("code") || e.classList.contains("raster") || transformed(cs)) {
|
|
173
|
+
const vr = visualRect(e);
|
|
174
|
+
e.setAttribute("data-pid", String(++pid));
|
|
175
|
+
items.push({ pid, kind: "image", ...box(vr), clip: { x: vr.left, y: vr.top, width: vr.width, height: vr.height }, ...st, opacity: opac(e) });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// caixa (fundo/borda)
|
|
179
|
+
const bg = rgba(cs.backgroundColor);
|
|
180
|
+
const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
181
|
+
const sameBorder = ["Right", "Bottom", "Left"].every((s) => cs[`border${s}Width`] === cs.borderTopWidth && cs[`border${s}Color`] === cs.borderTopColor);
|
|
182
|
+
const bc = rgba(cs.borderTopColor);
|
|
183
|
+
const hasBorder = bw > 0 && bc.a > 0.01 && cs.borderTopStyle !== "none" && sameBorder;
|
|
184
|
+
if (tag !== "MARK" && cs.backgroundImage !== "none" && !cs.backgroundImage.startsWith("linear-gradient(rgb") ) {
|
|
185
|
+
tagIt({ kind: "image", bgOnly: true });
|
|
186
|
+
} else if (bg.a > 0.01 || hasBorder) {
|
|
187
|
+
const rad = cs.borderTopLeftRadius;
|
|
188
|
+
const rpx = rad.endsWith("%") ? (parseFloat(rad) / 100) * Math.min(r.width, r.height) : parseFloat(rad) || 0;
|
|
189
|
+
const shape = rpx >= Math.min(r.width, r.height) / 2 - 0.5 ? (Math.abs(r.width - r.height) < 2 ? "ellipse" : "pill") : rpx > 0 ? "round" : "rect";
|
|
190
|
+
e.setAttribute("data-pid", String(++pid));
|
|
191
|
+
items.push({ pid, kind: "shape", ...box(r), ...st, opacity: opac(e), shape, radius: rpx, fill: bg.a > 0.01 ? bg.hex : null, fillA: bg.a, line: hasBorder ? bc.hex : null, lineA: bc.a, lineW: hasBorder ? bw : 0 });
|
|
192
|
+
}
|
|
193
|
+
// bordas diferentes por lado (divisor embaixo, barra à esquerda…): cada lado vira uma linha fina nativa
|
|
194
|
+
if (!sameBorder) {
|
|
195
|
+
for (const side of ["Top", "Right", "Bottom", "Left"]) {
|
|
196
|
+
const w = parseFloat(cs[`border${side}Width`]) || 0, c = rgba(cs[`border${side}Color`]);
|
|
197
|
+
if (w <= 0 || c.a <= 0.01 || cs[`border${side}Style`] === "none") continue;
|
|
198
|
+
const edge = side === "Top" ? { left: r.left, top: r.top, width: r.width, height: w }
|
|
199
|
+
: side === "Bottom" ? { left: r.left, top: r.bottom - w, width: r.width, height: w }
|
|
200
|
+
: side === "Left" ? { left: r.left, top: r.top, width: w, height: r.height }
|
|
201
|
+
: { left: r.right - w, top: r.top, width: w, height: r.height };
|
|
202
|
+
items.push({ pid: ++pid, kind: "shape", ...box(edge), ...st, opacity: opac(e), shape: "rect", radius: 0, fill: c.hex, fillA: c.a, line: null, lineA: 0, lineW: 0 });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// texto
|
|
206
|
+
if (isLeaf(e)) {
|
|
207
|
+
const pl = parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth || 0), pr = parseFloat(cs.paddingRight) + parseFloat(cs.borderRightWidth || 0);
|
|
208
|
+
const pt = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth || 0), pb = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth || 0);
|
|
209
|
+
const b = box(r);
|
|
210
|
+
const flexCenter = cs.display.includes("flex") && cs.alignItems === "center";
|
|
211
|
+
const ta = cs.textAlign === "start" || cs.textAlign === "left" ? "left" : cs.textAlign === "end" ? "right" : cs.textAlign === "justify" ? "left" : cs.textAlign;
|
|
212
|
+
const align = cs.display.includes("flex") && cs.justifyContent === "center" ? "center" : ta;
|
|
213
|
+
const lh = cs.lineHeight === "normal" ? parseFloat(cs.fontSize) * 1.2 : parseFloat(cs.lineHeight);
|
|
214
|
+
// marca-texto: um retângulo por linha, atrás do texto
|
|
215
|
+
e.querySelectorAll("mark").forEach((m) => {
|
|
216
|
+
const mcs = getComputedStyle(m);
|
|
217
|
+
if (!m.classList.contains("play") && !document.documentElement.classList.contains("export")) return;
|
|
218
|
+
const hi = rgba(mcs.getPropertyValue("--hi").trim() || mcs.backgroundColor);
|
|
219
|
+
for (const cr of m.getClientRects()) {
|
|
220
|
+
const bh = cr.height * 0.92, by = cr.top + cr.height * 0.044;
|
|
221
|
+
items.push({ pid: ++pid, kind: "shape", x: cr.left - sr.left, y: by - sr.top, w: cr.width, h: bh, ...st, opacity: opac(e), shape: "rect", radius: 0, fill: hi.hex, fillA: hi.a, line: null, lineA: 0, lineW: 0 });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
items.push({
|
|
225
|
+
pid: ++pid, kind: "text", x: b.x + pl, y: b.y + pt, w: b.w - pl - pr, h: b.h - pt - pb, ...st, opacity: opac(e),
|
|
226
|
+
align, valign: flexCenter ? "middle" : "top", lineHeight: lh, paras: runsOf(e, lh), nowrap: true,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
[...e.children].forEach(visit);
|
|
231
|
+
}
|
|
232
|
+
[...slide.children].forEach(visit);
|
|
233
|
+
const sbg = rgba(getComputedStyle(slide).backgroundColor).hex;
|
|
234
|
+
return { bg: sbg, tr: slide.dataset.tr || "fade", items };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ------------------------------------------------------------------ fontes
|
|
238
|
+
// Fontes variáveis do Windows: (peso, largura) -> nome exato da instância no PowerPoint
|
|
239
|
+
const VARIABLE = {
|
|
240
|
+
Bahnschrift: {
|
|
241
|
+
weights: [300, 350, 400, 600, 700], widths: [75, 87.5, 100],
|
|
242
|
+
name: (w, x) => {
|
|
243
|
+
const wn = { 300: " Light", 350: " SemiLight", 400: "", 600: " SemiBold", 700: "" }[w];
|
|
244
|
+
const xn = x === 75 ? " Condensed" : x === 87.5 ? (w === 600 ? " SemiConden" : w === 350 ? " SemiConde" : " SemiCondensed") : "";
|
|
245
|
+
return { face: `Bahnschrift${wn}${xn}`, bold: w === 700 };
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const near = (arr, v) => arr.reduce((a, b) => (Math.abs(b - v) < Math.abs(a - v) ? b : a));
|
|
250
|
+
function fontFor(run, fmap, aliases = {}) {
|
|
251
|
+
const fam = aliases[run.family] || run.family;
|
|
252
|
+
if (VARIABLE[fam]) {
|
|
253
|
+
const V = VARIABLE[fam];
|
|
254
|
+
const r = V.name(near(V.weights, run.weight), near(V.widths, run.stretch || 100));
|
|
255
|
+
return { face: r.face, bold: r.bold, italic: run.italic };
|
|
256
|
+
}
|
|
257
|
+
const m = fmap[run.role];
|
|
258
|
+
let face = fam, bold = run.weight >= 600, italic = run.italic;
|
|
259
|
+
if (m) {
|
|
260
|
+
const extraBold = run.weight >= Math.max(600, run.baseW + 150);
|
|
261
|
+
const f = extraBold ? m.bold : m.regular;
|
|
262
|
+
face = f.face; bold = !!f.bold; italic = !!f.italic || (run.italic && !run.baseItalic);
|
|
263
|
+
}
|
|
264
|
+
return { face, bold, italic };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ------------------------------------------------------------------ animações
|
|
268
|
+
function timingXML(anims, ids) {
|
|
269
|
+
// anims: [{spid, step, exit, isSp}]
|
|
270
|
+
let id = 2;
|
|
271
|
+
const clicks = new Map();
|
|
272
|
+
for (const a of anims) {
|
|
273
|
+
if (a.step > 0) { if (!clicks.has(a.step)) clicks.set(a.step, []); clicks.get(a.step).push({ ...a, type: "in" }); }
|
|
274
|
+
if (a.exit != null && a.exit > 0) { if (!clicks.has(a.exit)) clicks.set(a.exit, []); clicks.get(a.exit).push({ ...a, type: "out" }); }
|
|
275
|
+
}
|
|
276
|
+
if (!clicks.size) return "";
|
|
277
|
+
const keys = [...clicks.keys()].sort((a, b) => a - b);
|
|
278
|
+
let seq = "";
|
|
279
|
+
const bld = new Set();
|
|
280
|
+
for (const k of keys) {
|
|
281
|
+
const list = clicks.get(k);
|
|
282
|
+
let inner = "";
|
|
283
|
+
list.forEach((a, j) => {
|
|
284
|
+
const node = j === 0 ? "clickEffect" : "withEffect";
|
|
285
|
+
const grp = a.type === "in" ? 0 : 1;
|
|
286
|
+
if (a.isSp) bld.add(`<p:bldP spid="${a.spid}" grpId="${grp}" animBg="1"/>`);
|
|
287
|
+
if (a.type === "in") {
|
|
288
|
+
inner += `<p:par><p:cTn id="${++id}" presetID="10" presetClass="entr" presetSubtype="0" fill="hold" grpId="${grp}" nodeType="${node}"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>` +
|
|
289
|
+
`<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="visible"/></p:to></p:set>` +
|
|
290
|
+
`<p:animEffect transition="in" filter="fade"><p:cBhvr><p:cTn id="${++id}" dur="500"/><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl></p:cBhvr></p:animEffect>` +
|
|
291
|
+
`</p:childTnLst></p:cTn></p:par>`;
|
|
292
|
+
} else {
|
|
293
|
+
inner += `<p:par><p:cTn id="${++id}" presetID="10" presetClass="exit" presetSubtype="0" fill="hold" grpId="${grp}" nodeType="${node}"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>` +
|
|
294
|
+
`<p:animEffect transition="out" filter="fade"><p:cBhvr><p:cTn id="${++id}" dur="400"/><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl></p:cBhvr></p:animEffect>` +
|
|
295
|
+
`<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="399"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="hidden"/></p:to></p:set>` +
|
|
296
|
+
`</p:childTnLst></p:cTn></p:par>`;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
seq += `<p:par><p:cTn id="${++id}" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst><p:par><p:cTn id="${++id}" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>${inner}</p:childTnLst></p:cTn></p:par></p:childTnLst></p:cTn></p:par>`;
|
|
300
|
+
}
|
|
301
|
+
const initial = anims.filter(a => a.step > 0).map(a => `<p:set><p:cBhvr><p:cTn id="${++id}" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn><p:tgtEl><p:spTgt spid="${a.spid}"/></p:tgtEl><p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr><p:to><p:strVal val="hidden"/></p:to></p:set>`).join("");
|
|
302
|
+
return `<p:timing><p:tnLst><p:par><p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>${initial}<p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>${seq}</p:childTnLst></p:cTn><p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst><p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst></p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>${bld.size ? `<p:bldLst>${[...bld].join("")}</p:bldLst>` : ""}</p:timing>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ------------------------------------------------------------------ principal
|
|
306
|
+
// Linhas e decorações feitas com ::before/::after (eixo da linha do tempo, conectores, marcadores) não
|
|
307
|
+
// existem no DOM: viram <span> de verdade com o mesmo estilo calculado, e o coletor passa a enxergá-las.
|
|
308
|
+
function materializePseudos() {
|
|
309
|
+
for (const e of document.querySelectorAll("#stage > .slide *")) {
|
|
310
|
+
for (const which of ["::before", "::after"]) {
|
|
311
|
+
const cs = getComputedStyle(e, which);
|
|
312
|
+
if (!cs || cs.display === "none" || !/^["']/.test(cs.content || "")) continue; // só content: "…" (desenho ou texto)
|
|
313
|
+
const span = document.createElement("span");
|
|
314
|
+
// copia o estilo calculado, menos o que depende de qual slide está na tela agora (visibilidade) e animações
|
|
315
|
+
for (const prop of cs) if (!/^(visibility|transition|animation)/.test(prop)) span.style.setProperty(prop, cs.getPropertyValue(prop));
|
|
316
|
+
const text = cs.content.slice(1, -1);
|
|
317
|
+
if (text) span.textContent = text;
|
|
318
|
+
span.dataset.pseudo = which;
|
|
319
|
+
if (which === "::before") e.prepend(span); else e.append(span);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const st = document.createElement("style");
|
|
323
|
+
st.textContent = "#stage > .slide *::before, #stage > .slide *::after { content: none !important; }";
|
|
324
|
+
document.head.append(st);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// notes: false tira as notas do apresentador (mandar a apresentação para alguém sem a "cola")
|
|
328
|
+
export async function exportPptx(htmlFile, outFile, { theme, meta, nativeCharts = false, notes = true, log = () => {} } = {}) {
|
|
329
|
+
const notesOpt = notes !== false;
|
|
330
|
+
const { browser, page, errors } = await openDeck(htmlFile, { scale: 2 });
|
|
331
|
+
try {
|
|
332
|
+
await page.evaluate(materializePseudos);
|
|
333
|
+
await page.addStyleTag({ content: `
|
|
334
|
+
html.solo,html.solo body,html.solo #viewport,html.solo .slide{background:transparent!important}
|
|
335
|
+
html.solo .slide::after,html.solo .slide::before{display:none!important}
|
|
336
|
+
html.solo *{visibility:hidden!important}
|
|
337
|
+
html.solo .solo-t,html.solo .solo-t *{visibility:visible!important}
|
|
338
|
+
html.solo .solo-t.solo-bg *{visibility:hidden!important}` });
|
|
339
|
+
const fmap = pptxFontMap(theme);
|
|
340
|
+
// apelidos CSS (ex.: SagaDIN -> Bahnschrift) declarados em fontFaces do tema
|
|
341
|
+
const aliases = Object.fromEntries((theme.fontFaces || []).map((f) => [f.family, (f.src.match(/local\(['"]?([^'")]+)/) || [])[1]]).filter((x) => x[1]));
|
|
342
|
+
const pres = new PptxGenJS();
|
|
343
|
+
const size = await page.evaluate(() => window.sagadeck.size || { w: 1920, h: 1080 });
|
|
344
|
+
if (size.h === 1080) pres.layout = "LAYOUT_WIDE";
|
|
345
|
+
else { pres.defineLayout({ name: "SAGADECK", width: 13.333, height: +(13.333 * size.h / size.w).toFixed(3) }); pres.layout = "SAGADECK"; } // deck.aspect
|
|
346
|
+
pres.title = meta.title || "";
|
|
347
|
+
pres.author = meta.author || "";
|
|
348
|
+
const n = await page.evaluate(() => window.sagadeck.n);
|
|
349
|
+
const animBySlide = [];
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < n; i++) {
|
|
352
|
+
const S = await page.evaluate((j) => window.sagadeck.steps(j), i);
|
|
353
|
+
await page.evaluate(([j, k]) => window.sagadeck.goto(j, k, true), [i, S]);
|
|
354
|
+
await page.waitForTimeout(40);
|
|
355
|
+
const data = await page.evaluate(collectInPage, [i, nativeCharts]);
|
|
356
|
+
const slide = pres.addSlide();
|
|
357
|
+
slide.background = { color: data.bg };
|
|
358
|
+
const anims = [];
|
|
359
|
+
for (const it of data.items) {
|
|
360
|
+
const name = `sagadeck-${it.pid}`;
|
|
361
|
+
const geo = { x: it.x * PX, y: it.y * PX, w: Math.max(it.w, 1) * PX, h: Math.max(it.h, 1) * PX };
|
|
362
|
+
const alpha = it.opacity ?? 1;
|
|
363
|
+
if (it.kind === "shape") {
|
|
364
|
+
const shape = it.shape === "ellipse" ? pres.ShapeType.ellipse : it.shape === "rect" ? pres.ShapeType.rect : pres.ShapeType.roundRect;
|
|
365
|
+
const o = { ...geo, objectName: name };
|
|
366
|
+
o.fill = it.fill ? { color: it.fill, transparency: Math.round((1 - it.fillA * alpha) * 100) } : { type: "none" };
|
|
367
|
+
o.line = it.line ? { color: it.line, width: it.lineW * PT, transparency: Math.round((1 - it.lineA * alpha) * 100) } : { type: "none" };
|
|
368
|
+
if (shape === pres.ShapeType.roundRect) o.rectRadius = Math.min(it.radius, Math.min(it.w, it.h) / 2) * PX;
|
|
369
|
+
slide.addShape(shape, o);
|
|
370
|
+
anims.push({ name, step: it.step, exit: it.exit, isSp: true });
|
|
371
|
+
} else if (it.kind === "text") {
|
|
372
|
+
const runs = [];
|
|
373
|
+
it.paras.forEach((p, pi) => p.forEach((r, ri) => {
|
|
374
|
+
const f = fontFor(r, fmap, aliases);
|
|
375
|
+
const o = { fontFace: f.face, fontSize: +(r.size * PT).toFixed(2), bold: f.bold, italic: f.italic, color: r.color };
|
|
376
|
+
const tr = Math.round((1 - r.alpha * alpha) * 100); if (tr > 0) o.transparency = tr;
|
|
377
|
+
if (r.spacing) o.charSpacing = +(r.spacing * PT).toFixed(2);
|
|
378
|
+
if (r.hi) o.highlight = r.hi;
|
|
379
|
+
if (r.strike) o.strike = "sngStrike";
|
|
380
|
+
if (r.underline) o.underline = { style: "sng" };
|
|
381
|
+
if (r.href) o.hyperlink = r.href.startsWith("slide:") ? { slide: +r.href.slice(6) + 1 } : { url: r.href };
|
|
382
|
+
if (ri === p.length - 1 && pi < it.paras.length - 1) o.breakLine = true;
|
|
383
|
+
runs.push({ text: r.text, options: o });
|
|
384
|
+
}));
|
|
385
|
+
if (!runs.length) continue;
|
|
386
|
+
// folga para diferenças mínimas de métrica entre navegador e PowerPoint
|
|
387
|
+
const slack = it.nowrap ? 0.12 : 0.01;
|
|
388
|
+
const extra = it.w * slack;
|
|
389
|
+
let x = it.x - (it.align === "center" ? extra / 2 : it.align === "right" ? extra : 0);
|
|
390
|
+
slide.addText(runs, {
|
|
391
|
+
x: x * PX, y: it.y * PX, w: (it.w + extra) * PX, h: Math.max(it.h, it.lineHeight) * PX,
|
|
392
|
+
margin: 0, align: it.align, valign: it.valign, lineSpacing: +(it.lineHeight * PT).toFixed(2),
|
|
393
|
+
paraSpaceBefore: 0, paraSpaceAfter: 0, fit: "none", wrap: !it.nowrap, isTextBox: true, objectName: name,
|
|
394
|
+
});
|
|
395
|
+
anims.push({ name, step: it.step, exit: it.exit, isSp: true });
|
|
396
|
+
} else if (it.kind === "image") {
|
|
397
|
+
const loc = page.locator(`[data-pid="${it.pid}"]`);
|
|
398
|
+
await page.evaluate(([pid, bgOnly]) => {
|
|
399
|
+
document.documentElement.classList.add("solo");
|
|
400
|
+
const el = document.querySelector(`[data-pid="${pid}"]`);
|
|
401
|
+
el.classList.add("solo-t"); if (bgOnly) el.classList.add("solo-bg");
|
|
402
|
+
}, [it.pid, !!it.bgOnly]);
|
|
403
|
+
const buf = it.clip
|
|
404
|
+
? await page.screenshot({ omitBackground: true, animations: "disabled", clip: it.clip })
|
|
405
|
+
: await loc.screenshot({ omitBackground: true, animations: "disabled" });
|
|
406
|
+
await page.evaluate((pid) => {
|
|
407
|
+
document.documentElement.classList.remove("solo");
|
|
408
|
+
const el = document.querySelector(`[data-pid="${pid}"]`); el.classList.remove("solo-t", "solo-bg");
|
|
409
|
+
}, it.pid);
|
|
410
|
+
slide.addImage({ data: "image/png;base64," + buf.toString("base64"), ...geo, objectName: name });
|
|
411
|
+
anims.push({ name, step: it.step, exit: it.exit, isSp: false });
|
|
412
|
+
} else if (it.kind === "table") {
|
|
413
|
+
const rows = it.rows.map((r) => r.map((c) => {
|
|
414
|
+
const f = fontFor(c.run, fmap, aliases);
|
|
415
|
+
const o = { fontFace: f.face, bold: f.bold, italic: f.italic, fontSize: +(c.size * PT).toFixed(2), color: c.color, align: c.align, valign: "middle", margin: c.pad.map((v) => +(v * PT).toFixed(2)) };
|
|
416
|
+
o.fill = c.fill ? { color: c.fill, transparency: Math.round((1 - c.fillA * alpha) * 100) } : { type: "none" };
|
|
417
|
+
const none = { type: "none" };
|
|
418
|
+
o.border = [none, none, c.line ? { type: "solid", color: c.line.color, pt: +(c.line.w * PT).toFixed(2) } : none, none];
|
|
419
|
+
return { text: c.text, options: o };
|
|
420
|
+
}));
|
|
421
|
+
slide.addTable(rows, { ...geo, colW: it.colW.map((w) => w * PX), rowH: it.rowH.map((h) => h * PX), objectName: name, autoPage: false });
|
|
422
|
+
anims.push({ name, step: it.step, exit: it.exit, isSp: false });
|
|
423
|
+
} else if (it.kind === "chart") {
|
|
424
|
+
addNativeChart(pres, slide, it, geo, name, fmap);
|
|
425
|
+
anims.push({ name, step: it.step, exit: it.exit, isSp: false });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const notes = meta.slides[i]?.notesRaw ?? "", withNotes = notesOpt;
|
|
429
|
+
if (notes && withNotes) slide.addNotes(notesPlain(notes));
|
|
430
|
+
animBySlide.push({ anims, tr: data.tr });
|
|
431
|
+
log(` slide ${i + 1}/${n}: ${data.items.length} objetos`);
|
|
432
|
+
}
|
|
433
|
+
// pós-processamento: animações + transições
|
|
434
|
+
const buf = await pres.write({ outputType: "nodebuffer" });
|
|
435
|
+
const zip = await JSZip.loadAsync(buf);
|
|
436
|
+
for (let i = 0; i < animBySlide.length; i++) {
|
|
437
|
+
const f = `ppt/slides/slide${i + 1}.xml`;
|
|
438
|
+
let xml = await zip.file(f).async("string");
|
|
439
|
+
const ids = {};
|
|
440
|
+
for (const m of xml.matchAll(/<p:cNvPr id="(\d+)" name="(sagadeck-\d+)"/g)) ids[m[2]] = m[1];
|
|
441
|
+
const anims = animBySlide[i].anims.filter((a) => ids[a.name] && (a.step > 0 || a.exit != null)).map((a) => ({ ...a, spid: ids[a.name] }));
|
|
442
|
+
const tr = animBySlide[i].tr === "cut" ? "" : `<p:transition spd="med"><p:fade/></p:transition>`;
|
|
443
|
+
const timing = timingXML(anims);
|
|
444
|
+
xml = xml.replace(/<\/p:clrMapOvr>/, `</p:clrMapOvr>${tr}${timing}`);
|
|
445
|
+
if (!/<\/p:clrMapOvr>/.test(xml)) xml = xml.replace(/<\/p:sld>/, `${tr}${timing}</p:sld>`);
|
|
446
|
+
zip.file(f, xml);
|
|
447
|
+
}
|
|
448
|
+
const out = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
449
|
+
fs.writeFileSync(outFile, out);
|
|
450
|
+
return { errors };
|
|
451
|
+
} finally { await browser.close(); }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function addNativeChart(pres, slide, it, geo, name, fmap) {
|
|
455
|
+
const s = it.spec, font = fmap.body?.regular?.face || "Arial";
|
|
456
|
+
const base = { ...geo, objectName: name, fontFace: font, catAxisLabelColor: it.fg, valAxisLabelColor: it.fg, dataLabelColor: it.fg, showLegend: false,
|
|
457
|
+
valGridLine: { style: "none" }, catGridLine: { style: "none" }, catAxisLabelFontSize: 16, dataLabelFontSize: 20, dataLabelFontFace: font, catAxisLabelFontFace: font };
|
|
458
|
+
if (s.type === "bar" || s.type === "column") {
|
|
459
|
+
const data = s.series ? s.series.map((x) => ({ name: x.name, labels: s.labels, values: x.values.map((v) => (v == null ? "" : v)) })) : [{ name: "série", labels: s.labels, values: s.values }];
|
|
460
|
+
slide.addChart(pres.ChartType.bar, data, {
|
|
461
|
+
...base, barDir: s.type === "bar" ? "bar" : "col", chartColors: it.colors.length ? it.colors : ["888888"], barGapWidthPct: 50, barGrouping: "clustered", showLegend: !!s.series, legendPos: "t",
|
|
462
|
+
showValue: true, dataLabelPosition: "outEnd", dataLabelFormatCode: `"${s.prefix || ""}"0"${s.suffix || ""}"`, valAxisHidden: true,
|
|
463
|
+
valAxisMaxVal: s.max, catAxisOrientation: s.type === "bar" ? "maxMin" : "minMax", catAxisLineShow: false,
|
|
464
|
+
});
|
|
465
|
+
} else if (s.type === "line") {
|
|
466
|
+
slide.addChart(pres.ChartType.line, s.series.map((x) => ({ name: x.name, labels: s.labels, values: x.values.map((v) => (v == null ? "" : v)) })), {
|
|
467
|
+
...base, chartColors: it.colors.length ? it.colors : undefined, lineSize: 3, lineDataSymbolSize: 6, valAxisMinVal: s.min, valAxisMaxVal: s.max,
|
|
468
|
+
valGridLine: { color: "DDDDDD", size: 0.5 }, showLegend: s.series.length > 1, legendPos: "b",
|
|
469
|
+
});
|
|
470
|
+
} else if (s.type === "donut") {
|
|
471
|
+
slide.addChart(pres.ChartType.doughnut, [{ name: "série", labels: s.parts.map((p) => p.label), values: s.parts.map((p) => p.value) }], {
|
|
472
|
+
...base, chartColors: it.colors, holeSize: 62, showValue: false, showPercent: false,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Roteiro do apresentador em PDF: miniatura de cada slide + notas + relógio planejado.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { chromium } from "playwright-core";
|
|
4
|
+
import { findBrowser } from "./browser.js";
|
|
5
|
+
import { esc } from "../markup.js";
|
|
6
|
+
|
|
7
|
+
export async function roteiroPDF({ meta, slidesMeta, shotFiles, outFile, title, author, duration }) {
|
|
8
|
+
let acc = 0;
|
|
9
|
+
const fmt = (m) => `${String(Math.floor(m)).padStart(2, "0")}:${String(Math.round((m % 1) * 60)).padStart(2, "0")}`;
|
|
10
|
+
const rows = slidesMeta.map((s, i) => {
|
|
11
|
+
const start = acc; acc += s.time || 0;
|
|
12
|
+
const img = shotFiles[i] ? `<img src="data:image/${shotFiles[i].endsWith(".jpg") ? "jpeg" : "png"};base64,${fs.readFileSync(shotFiles[i]).toString("base64")}">` : "";
|
|
13
|
+
return `<section><div class="l"><div class="n">${String(i + 1).padStart(2, "0")}</div>${img}<div class="tm">${s.time ? `${fmt(start)} → ${fmt(acc)} · ${s.time} min` : ""}</div></div>
|
|
14
|
+
<div class="r"><h3>${esc(s.title)}</h3>${s.notes || "<p class='muted'>—</p>"}</div></section>`;
|
|
15
|
+
}).join("");
|
|
16
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><style>
|
|
17
|
+
@page{size:A4;margin:14mm 14mm 16mm}
|
|
18
|
+
body{font:11pt/1.5 'Segoe UI',Arial,sans-serif;color:#1a1a1a;margin:0}
|
|
19
|
+
header{border-bottom:2px solid #111;padding-bottom:8px;margin-bottom:14px}
|
|
20
|
+
header h1{font:700 22pt/1.1 'Segoe UI';margin:0}
|
|
21
|
+
header p{margin:4px 0 0;color:#666}
|
|
22
|
+
section{display:grid;grid-template-columns:62mm 1fr;gap:6mm;padding:5mm 0;border-bottom:1px solid #ddd;break-inside:avoid}
|
|
23
|
+
.l img{width:100%;border-radius:3px;border:1px solid #ccc;display:block}
|
|
24
|
+
.n{font:700 10pt 'Segoe UI';color:#999;margin-bottom:2px}
|
|
25
|
+
.tm{font:600 9pt 'Segoe UI';color:#b3261e;margin-top:4px}
|
|
26
|
+
h3{font:700 12.5pt/1.25 'Segoe UI';margin:0 0 6px}
|
|
27
|
+
p{margin:0 0 6px}
|
|
28
|
+
.say{border-left:3px solid #f2b705;padding-left:8px;font-size:11.5pt}
|
|
29
|
+
.tag{display:inline-block;font:700 7.5pt 'Segoe UI';letter-spacing:.1em;background:#111;color:#ffc20e;padding:1px 6px;border-radius:3px;margin-right:6px;vertical-align:1px}
|
|
30
|
+
ul{margin:0 0 6px;padding-left:16px}
|
|
31
|
+
h4{font:700 8.5pt 'Segoe UI';letter-spacing:.12em;text-transform:uppercase;color:#777;margin:8px 0 3px}
|
|
32
|
+
mark{background:#ffe07a}
|
|
33
|
+
.muted{color:#999}
|
|
34
|
+
</style></head><body><header><h1>Roteiro · ${esc(title || "")}</h1><p>${esc(author || "")}${duration ? ` · ${duration} min` : ""} · tempo planejado: ${Math.round(acc)} min · ${slidesMeta.length} slides</p></header>${rows}</body></html>`;
|
|
35
|
+
const browser = await chromium.launch({ executablePath: findBrowser() });
|
|
36
|
+
const page = await browser.newPage();
|
|
37
|
+
await page.setContent(html, { waitUntil: "load" });
|
|
38
|
+
await page.pdf({ path: outFile, format: "A4", printBackground: true, displayHeaderFooter: true, headerTemplate: "<span></span>", footerTemplate: `<div style="font:8pt Segoe UI;color:#999;width:100%;text-align:center"><span class="pageNumber"></span>/<span class="totalPages"></span></div>`, margin: { top: "14mm", bottom: "16mm", left: "14mm", right: "14mm" } });
|
|
39
|
+
await browser.close();
|
|
40
|
+
}
|