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
package/src/graph-lab.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { esc } from './markup.js';
|
|
2
|
+
import { iconSVG } from './figures/icons.js';
|
|
3
|
+
|
|
4
|
+
// O exemplo é dado, não uma regra por assunto: a IA escolhe nós, relações e aparência.
|
|
5
|
+
export const graphLabExample = { layout:'graphlab', title:'Qual rota sobrevive ao imprevisto?',
|
|
6
|
+
nodes:[{id:'fabrica',label:'Fábrica',icon:'factory',x:140,y:300},{id:'centro',label:'Centro de distribuição',icon:'warehouse',x:470,y:160},{id:'porto',label:'Porto',icon:'ship',x:470,y:460},{id:'loja',label:'Loja',icon:'store',x:850,y:300}],
|
|
7
|
+
edges:[{from:'fabrica',to:'centro',weight:4,label:'Rodovia'},{from:'fabrica',to:'porto',weight:2,label:'Ferrovia'},{from:'porto',to:'centro',weight:1},{from:'centro',to:'loja',weight:3},{from:'porto',to:'loja',weight:8}],
|
|
8
|
+
start:'fabrica',goal:'loja',explanation:'Selecione uma ligação e altere seu custo. Bloqueie um nó e compare a nova rota.' };
|
|
9
|
+
|
|
10
|
+
export function graphLabHTML(s,ctx,head) {
|
|
11
|
+
const nodes=(s.nodes||[]).map(n=>typeof n==='string'?{id:n,label:n}:{...n,id:String(n.id||'')});
|
|
12
|
+
const ids=new Set();
|
|
13
|
+
for(const n of nodes){if(!n.id||ids.has(String(n.id)))throw new Error('graphlab: cada nó precisa de id único');ids.add(String(n.id));}
|
|
14
|
+
const edges=(s.edges||[]).map(e=>({...e,from:String(e.from),to:String(e.to),weight:Number(e.weight??1)}));
|
|
15
|
+
for(const e of edges){if(!ids.has(String(e.from))||!ids.has(String(e.to))||!Number.isFinite(e.weight)||e.weight<0)throw new Error('graphlab: ligação com destino inexistente ou custo negativo');}
|
|
16
|
+
const model={nodes,edges,directed:!!s.directed,start:s.start||nodes[0]?.id,goal:s.goal||nodes.at(-1)?.id};
|
|
17
|
+
const icons=nodes.map(n=>`<template data-gl-icon="${esc(n.id)}">${iconSVG(n.icon||'map-pin',{size:28})}</template>`).join('');
|
|
18
|
+
return `<div class="L-graphlab" data-graphlab>${head(s)}
|
|
19
|
+
<div class="gl-toolbar"><button data-gl-action="route">Calcular rota</button><button data-gl-action="walk">Visitar vizinhos</button><button data-gl-action="step">Próxima visita</button><button data-gl-action="focus">Focar seleção</button><button data-gl-action="fit">Ver tudo</button><button data-gl-action="reset">Restaurar</button></div>
|
|
20
|
+
<div class="gl-body"><div class="gl-canvas"><svg viewBox="0 0 1000 620" role="img" aria-label="Grafo explorável"><g class="gl-world"><g class="gl-edges"></g><g class="gl-nodes"></g></g></svg><div class="gl-zoom"><button data-gl-action="in" aria-label="Ampliar grafo">+</button><button data-gl-action="out" aria-label="Reduzir grafo">−</button></div></div>
|
|
21
|
+
<aside class="gl-inspector"><h3>Explore a rede</h3><p data-gl-details>Clique num nó ou numa ligação. Arraste os nós para reorganizar.</p><label>Origem<select data-gl-start></select></label><label>Destino<select data-gl-goal></select></label><output data-gl-result aria-live="polite">Escolha origem e destino para comparar caminhos.</output>
|
|
22
|
+
<details class="gl-edit-panel"><summary>Editar seleção</summary><label>Nome do nó<input data-gl-label></label><label>Custo da ligação<input data-gl-weight type="number" min="0" step="0.5"></label><label>Cor do destaque<input data-gl-color type="color" value="#e75b40"></label><div class="gl-edit"><button data-gl-action="add">Adicionar nó</button><button data-gl-action="connect">Ligar à origem</button><button data-gl-action="remove">Remover seleção</button></div></details></aside></div>
|
|
23
|
+
${s.explanation?`<p class="gl-explanation">${esc(s.explanation)}</p>`:''}${icons}<script type="application/json" class="gl-model">${JSON.stringify(model).replace(/</g,'\\u003c')}</script></div>`;
|
|
24
|
+
}
|
package/src/hologram.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Geometrias projetadas em perspectiva; a tipografia e o conteúdo continuam nativos.
|
|
2
|
+
export function drawHologram(ctx,o,t,w,h){
|
|
3
|
+
const unit=Math.min(w,h),project=(v,l)=>{
|
|
4
|
+
const a=t*l.speed*.12+l.rotation,ca=Math.cos(a),sa=Math.sin(a),x=v[0]*ca+v[2]*sa,z=-v[0]*sa+v[2]*ca,y=v[1]*Math.cos(.38)-z*Math.sin(.38),depth=z*Math.cos(.38)+v[1]*Math.sin(.38),p=3.8/(3.8+depth);
|
|
5
|
+
return [w*l.x+x*unit*l.scale*p,h*l.y+y*unit*l.scale*p,depth];
|
|
6
|
+
};
|
|
7
|
+
const line=(vertices,l,color,closed=false)=>{ctx.beginPath();vertices.forEach((v,i)=>{const p=project(v,l);i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]);});if(closed)ctx.closePath();ctx.strokeStyle=color;ctx.lineWidth=Math.max(.6,unit/1000);ctx.stroke();};
|
|
8
|
+
const ring=(l,radius,tilt,color)=>{const v=[];for(let i=0;i<=96;i++){const a=i*Math.PI/48;v.push([radius*Math.cos(a),radius*Math.sin(a)*Math.sin(tilt),radius*Math.sin(a)*Math.cos(tilt)]);}line(v,l,color);};
|
|
9
|
+
ctx.save();ctx.globalAlpha=o.opacity;
|
|
10
|
+
for(const l of o.layers){
|
|
11
|
+
ctx.save();
|
|
12
|
+
if(l.shape==='sphere'){
|
|
13
|
+
for(let j=-3;j<=3;j++){const phi=j*Math.PI/9,v=[];for(let n=0;n<=64;n++){const a=n*Math.PI/32;v.push([Math.cos(phi)*Math.cos(a),Math.sin(phi),Math.cos(phi)*Math.sin(a)]);}line(v,l,o.foreground);}
|
|
14
|
+
for(let j=0;j<8;j++){const a=j*Math.PI/4,v=[];for(let n=0;n<=64;n++){const p=-Math.PI/2+n*Math.PI/64;v.push([Math.cos(p)*Math.cos(a),Math.sin(p),Math.cos(p)*Math.sin(a)]);}line(v,l,o.foreground);}
|
|
15
|
+
}else if(l.shape==='cube'){
|
|
16
|
+
const v=[[-1,-1,-1],[1,-1,-1],[1,1,-1],[-1,1,-1],[-1,-1,1],[1,-1,1],[1,1,1],[-1,1,1]];
|
|
17
|
+
for(const ids of [[0,1,2,3],[4,5,6,7]]){const face=ids.map(i=>project(v[i],l));ctx.beginPath();face.forEach((p,i)=>i?ctx.lineTo(...p.slice(0,2)):ctx.moveTo(...p.slice(0,2)));ctx.closePath();ctx.save();ctx.globalAlpha*=.08;ctx.fillStyle=o.secondary;ctx.fill();ctx.restore();line(ids.map(i=>v[i]),l,o.foreground,true);}
|
|
18
|
+
for(let i=0;i<4;i++)line([v[i],v[i+4]],l,o.foreground);
|
|
19
|
+
}else if(l.shape==='plane'){
|
|
20
|
+
for(let j=-4;j<=4;j++){const n=j/4;line([[-1,n,0],[1,n,0]],l,o.secondary);line([[n,-1,0],[n,1,0]],l,o.secondary);}
|
|
21
|
+
line([[-1,-1,0],[1,-1,0],[1,1,0],[-1,1,0]],l,o.foreground,true);
|
|
22
|
+
}
|
|
23
|
+
ring(l,1.3,.15,o.secondary);ring(l,1.45,1.3,o.accent);
|
|
24
|
+
for(let i=0;i<48;i++){const a=i*Math.PI/24;line([[1.48*Math.cos(a),0,1.48*Math.sin(a)],[(i%4?1.51:1.57)*Math.cos(a),0,(i%4?1.51:1.57)*Math.sin(a)]],l,i%4?o.secondary:o.accent);}
|
|
25
|
+
const orbit=t*l.speed*.24+l.rotation,p=project([1.3*Math.cos(orbit),.2,1.3*Math.sin(orbit)],l);
|
|
26
|
+
ctx.fillStyle=o.accent;ctx.beginPath();ctx.arc(p[0],p[1],Math.max(1,unit/500),0,Math.PI*2);ctx.fill();
|
|
27
|
+
ctx.restore();
|
|
28
|
+
}
|
|
29
|
+
ctx.restore();
|
|
30
|
+
}
|
package/src/identity.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Identidades: as fontes (e a paleta preferida) da empresa da pessoa. Ficam só na máquina dela, em
|
|
2
|
+
// ~/.sagadeck/identidades.yaml (ou SAGADECK_IDENTIDADES): o nome da fonte de uma empresa nunca entra no
|
|
3
|
+
// software nem no repositório. O deck só diz qual usar (identity: trabalho); sem o arquivo nesta máquina, o deck
|
|
4
|
+
// abre com as fontes do tema e um aviso.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
|
|
10
|
+
// (no Windows, C:\Users\<você>\.sagadeck\identidades.yaml, ao lado do ambientes.yaml)
|
|
11
|
+
export function identitiesFile(env = process.env, home = os.homedir()) {
|
|
12
|
+
return env.SAGADECK_IDENTIDADES || path.join(home, ".sagadeck", "identidades.yaml");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Modelo comentado (o Studio cria quando a pessoa pede para configurar)
|
|
16
|
+
export const IDENTITY_TEMPLATE = `# Identidades do sagadeck: as fontes e cores da sua empresa. Este arquivo fica só neste computador.
|
|
17
|
+
#
|
|
18
|
+
# Cada identidade tem um nome curto (usado no deck: "identity: trabalho") e:
|
|
19
|
+
# nome: como aparece no Studio
|
|
20
|
+
# fontes: por papel, em ordem de preferência; se nenhuma estiver instalada, entra a fonte do tema
|
|
21
|
+
# titulo: títulos e manchetes (nos temas com personalidade, como rabisco e pop, o título fica com a
|
|
22
|
+
# fonte do tema e o resto usa a da empresa)
|
|
23
|
+
# corpo: textos, listas e subtítulos
|
|
24
|
+
# compacta: rótulos, legendas e tabelas (opcional; sem ela, vale a do corpo)
|
|
25
|
+
# codigo: código (opcional)
|
|
26
|
+
# paleta: uma das paletas do sagadeck (rubi, ametista, tangerina, safira, esmeralda, tinta…) ou as suas cores:
|
|
27
|
+
# { paper: "FFFFFF", ink: "3B2B33", accent: "B83A6E", alert: "CC092F", family: ["F9DCE5", "D9668F"] }
|
|
28
|
+
# (vale quando o deck não escolheu outra paleta)
|
|
29
|
+
#
|
|
30
|
+
# O arquivo da fonte não é embutido na apresentação (a licença de fontes de empresa costuma proibir): quem abrir
|
|
31
|
+
# numa máquina sem a fonte vê a do tema.
|
|
32
|
+
|
|
33
|
+
trabalho:
|
|
34
|
+
nome: Trabalho
|
|
35
|
+
fontes:
|
|
36
|
+
titulo: ["Nome da Fonte Sans", "Nome da Fonte Sans Compact"]
|
|
37
|
+
corpo: ["Nome da Fonte Sans"]
|
|
38
|
+
compacta: ["Nome da Fonte Sans Compact"]
|
|
39
|
+
paleta: rubi
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
let cache = { file: null, mtime: 0, size: -1, data: null };
|
|
43
|
+
|
|
44
|
+
// { file, exists, identities: { id: { id, name, fonts: { titulo, corpo, compacta, codigo }, palette } }, error }
|
|
45
|
+
export function loadIdentities(env = process.env) {
|
|
46
|
+
const file = identitiesFile(env);
|
|
47
|
+
let st = null;
|
|
48
|
+
try { st = fs.statSync(file); } catch { return { file, exists: false, identities: {}, error: null }; }
|
|
49
|
+
if (cache.file === file && cache.mtime === st.mtimeMs && cache.size === st.size && cache.data) return cache.data;
|
|
50
|
+
let data;
|
|
51
|
+
try {
|
|
52
|
+
const raw = YAML.parse(fs.readFileSync(file, "utf8")) || {};
|
|
53
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error("o arquivo precisa ser uma lista de identidades (nome: { fontes, paleta })");
|
|
54
|
+
const list = (v) => [].concat(v || []).map((x) => String(x).trim().replace(/^["']|["']$/g, "")).filter(Boolean).slice(0, 6);
|
|
55
|
+
const identities = {};
|
|
56
|
+
for (const [id, v] of Object.entries(raw)) {
|
|
57
|
+
if (!v || typeof v !== "object") continue;
|
|
58
|
+
const f = v.fontes || v.fonts || {};
|
|
59
|
+
identities[id] = {
|
|
60
|
+
id, name: String(v.nome || v.name || id),
|
|
61
|
+
fonts: { titulo: list(f.titulo || f.title), corpo: list(f.corpo || f.body), compacta: list(f.compacta || f.compact), codigo: list(f.codigo || f.code) },
|
|
62
|
+
palette: v.paleta ?? v.palette ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
data = { file, exists: true, identities, error: null };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
data = { file, exists: true, identities: {}, error: `identidades.yaml com erro: ${e.message}` };
|
|
68
|
+
}
|
|
69
|
+
cache = { file, mtime: st.mtimeMs, size: st.size, data };
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function identityFor(id, env = process.env) {
|
|
74
|
+
if (!id) return null;
|
|
75
|
+
return loadIdentities(env).identities[id] || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// cria o modelo comentado (sem sobrescrever o que existe)
|
|
79
|
+
export function ensureIdentitiesFile(env = process.env) {
|
|
80
|
+
const file = identitiesFile(env);
|
|
81
|
+
if (!fs.existsSync(file)) {
|
|
82
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
83
|
+
fs.writeFileSync(file, IDENTITY_TEMPLATE, "utf8");
|
|
84
|
+
}
|
|
85
|
+
return file;
|
|
86
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Recortes de imagem (PNG) pelo Chromium: serve para tirar, da foto fiel do slide original, a região de um objeto
|
|
2
|
+
// que o sagadeck não desenha igual (equação antiga em WMF com fontes de símbolo, SmartArt…).
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
|
|
5
|
+
// fotos reduzidas (JPEG) para mandar ao modelo de visão: menos tokens, a mesma leitura
|
|
6
|
+
export async function imagesAsDataUrls(files, { width = 1024, quality = 0.82 } = {}) {
|
|
7
|
+
if (!files.length) return [];
|
|
8
|
+
const { findBrowser } = await import("../export/browser.js");
|
|
9
|
+
const { chromium } = await import("playwright-core");
|
|
10
|
+
const browser = await chromium.launch({ executablePath: findBrowser() });
|
|
11
|
+
try {
|
|
12
|
+
const page = await browser.newPage();
|
|
13
|
+
await page.setContent("<body></body>");
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const f of files) {
|
|
16
|
+
if (!f || !fs.existsSync(f)) { out.push(null); continue; }
|
|
17
|
+
const ext = f.toLowerCase().endsWith(".png") ? "png" : "jpeg";
|
|
18
|
+
const data = `data:image/${ext};base64,${fs.readFileSync(f).toString("base64")}`;
|
|
19
|
+
out.push(await page.evaluate(async ([data, width, quality]) => {
|
|
20
|
+
const img = new Image(); img.src = data; await img.decode();
|
|
21
|
+
const w = Math.min(width, img.width), h = Math.round((img.height * w) / img.width);
|
|
22
|
+
const c = document.createElement("canvas"); c.width = w; c.height = h;
|
|
23
|
+
const g = c.getContext("2d"); g.fillStyle = "#fff"; g.fillRect(0, 0, w, h); g.drawImage(img, 0, 0, w, h);
|
|
24
|
+
return c.toDataURL("image/jpeg", quality);
|
|
25
|
+
}, [data, width, quality]));
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
} finally { await browser.close(); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function cropRegions(jobs) {
|
|
32
|
+
if (!jobs.length) return [];
|
|
33
|
+
const { findBrowser } = await import("../export/browser.js");
|
|
34
|
+
const { chromium } = await import("playwright-core");
|
|
35
|
+
const browser = await chromium.launch({ executablePath: findBrowser() });
|
|
36
|
+
const done = [];
|
|
37
|
+
try {
|
|
38
|
+
const page = await browser.newPage();
|
|
39
|
+
await page.setContent("<body></body>");
|
|
40
|
+
const bySrc = new Map();
|
|
41
|
+
for (const j of jobs) { if (!bySrc.has(j.src)) bySrc.set(j.src, []); bySrc.get(j.src).push(j); }
|
|
42
|
+
for (const [src, list] of bySrc) {
|
|
43
|
+
if (!fs.existsSync(src)) continue;
|
|
44
|
+
const data = `data:image/png;base64,${fs.readFileSync(src).toString("base64")}`;
|
|
45
|
+
const outs = await page.evaluate(async ([data, list]) => {
|
|
46
|
+
const img = new Image(); img.src = data; await img.decode();
|
|
47
|
+
return list.map((j) => {
|
|
48
|
+
const x = Math.max(0, Math.floor(j.x)), y = Math.max(0, Math.floor(j.y));
|
|
49
|
+
const w = Math.max(1, Math.min(img.width - x, Math.ceil(j.w))), h = Math.max(1, Math.min(img.height - y, Math.ceil(j.h)));
|
|
50
|
+
const c = document.createElement("canvas"); c.width = w; c.height = h;
|
|
51
|
+
c.getContext("2d").drawImage(img, x, y, w, h, 0, 0, w, h);
|
|
52
|
+
return c.toDataURL("image/png");
|
|
53
|
+
});
|
|
54
|
+
}, [data, list.map(({ x, y, w, h }) => ({ x, y, w, h }))]);
|
|
55
|
+
outs.forEach((u, k) => { fs.writeFileSync(list[k].dst, Buffer.from(u.split(",")[1], "base64")); done.push(list[k].dst); });
|
|
56
|
+
}
|
|
57
|
+
} finally { await browser.close(); }
|
|
58
|
+
return done;
|
|
59
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Importar uma apresentação para uma pasta de deck: lê (src/import/pptx.js), grava as mídias em imagens/original/
|
|
2
|
+
// (WMF/EMF viram PNG), guarda uma cópia do original e a foto de cada slide em original/ (PowerPoint ou LibreOffice,
|
|
3
|
+
// se houver) e devolve o deck. Cada slide lembra de onde veio (original: { slide, image }).
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import JSZip from "jszip";
|
|
8
|
+
import { importPptx, plainOf } from "./pptx.js";
|
|
9
|
+
import { metafilesToPng, powerPointSnapshots, libreOfficeToPdf } from "./office.js";
|
|
10
|
+
|
|
11
|
+
const gcd = (a, b) => (b ? gcd(b, a % b) : a);
|
|
12
|
+
export function aspectOf(cx, cy) {
|
|
13
|
+
const r = cx / cy;
|
|
14
|
+
if (Math.abs(r - 16 / 9) < 0.005) return undefined;
|
|
15
|
+
if (Math.abs(r - 4 / 3) < 0.005) return "4:3";
|
|
16
|
+
if (Math.abs(r - 16 / 10) < 0.005) return "16:10";
|
|
17
|
+
const g = gcd(Math.round(cx / 1000), Math.round(cy / 1000)) || 1;
|
|
18
|
+
return `${Math.round(cx / 1000) / g}:${Math.round(cy / 1000) / g}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function importToDir(buf, dir, { fileName = "original.pptx", snapshots = true, log = () => {} } = {}) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
const res = await importPptx(buf);
|
|
24
|
+
const zip = await JSZip.loadAsync(buf);
|
|
25
|
+
// mídias
|
|
26
|
+
const rename = new Map(), convert = [];
|
|
27
|
+
for (const m of res.media) {
|
|
28
|
+
const f = zip.file(m.zipPath);
|
|
29
|
+
if (!f) continue;
|
|
30
|
+
const abs = path.join(dir, ...m.name.split("/"));
|
|
31
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
32
|
+
fs.writeFileSync(abs, await f.async("nodebuffer"));
|
|
33
|
+
if (["wmf", "emf", "tif", "tiff"].includes(m.ext)) convert.push({ src: abs, dst: abs.replace(/\.[a-z]+$/i, ".png"), from: m.name });
|
|
34
|
+
}
|
|
35
|
+
// mídia repetida (o mesmo logo gravado uma vez por slide): fica um arquivo só
|
|
36
|
+
const seen = new Map();
|
|
37
|
+
for (const m of res.media) {
|
|
38
|
+
const abs = path.join(dir, ...m.name.split("/"));
|
|
39
|
+
if (!fs.existsSync(abs)) continue;
|
|
40
|
+
const h = crypto.createHash("sha1").update(fs.readFileSync(abs)).digest("hex");
|
|
41
|
+
if (seen.has(h)) { rename.set(m.name, seen.get(h)); fs.rmSync(abs, { force: true }); const i = convert.findIndex((c) => c.from === m.name); if (i >= 0) convert.splice(i, 1); }
|
|
42
|
+
else seen.set(h, m.name);
|
|
43
|
+
}
|
|
44
|
+
if (convert.length) {
|
|
45
|
+
log(`convertendo ${convert.length} figura(s) WMF/EMF`);
|
|
46
|
+
const done = new Set(metafilesToPng(convert.map(({ src, dst }) => ({ src, dst }))));
|
|
47
|
+
for (const c of convert) if (done.has(c.dst)) { rename.set(c.from, c.from.replace(/\.[a-z]+$/i, ".png")); fs.rmSync(c.src, { force: true }); }
|
|
48
|
+
}
|
|
49
|
+
// repetida → a primeira; WMF/EMF → o PNG convertido (em cadeia)
|
|
50
|
+
const resolve = (n) => { let v = n; for (let k = 0; k < 4 && rename.has(v); k++) v = rename.get(v); return v; };
|
|
51
|
+
const fix = (e) => {
|
|
52
|
+
if (e?.image) e.image = resolve(e.image);
|
|
53
|
+
if (e?.drawing) e.drawing = String(e.drawing).replace(/href="media:([^"]+)"/g, (m, r) => `href="media:${resolve(r)}"`);
|
|
54
|
+
return e;
|
|
55
|
+
};
|
|
56
|
+
// original e fotos
|
|
57
|
+
const origDir = path.join(dir, "original");
|
|
58
|
+
fs.mkdirSync(origDir, { recursive: true });
|
|
59
|
+
const origFile = path.join(origDir, path.basename(fileName));
|
|
60
|
+
fs.writeFileSync(origFile, buf);
|
|
61
|
+
let snaps = [], snapBy = null;
|
|
62
|
+
if (snapshots) {
|
|
63
|
+
log("fotografando os slides do original");
|
|
64
|
+
snaps = powerPointSnapshots(origFile, origDir);
|
|
65
|
+
if (snaps.length) snapBy = "PowerPoint";
|
|
66
|
+
else {
|
|
67
|
+
const lo = libreOfficeToPdf(origFile);
|
|
68
|
+
if (lo) {
|
|
69
|
+
try { const { renderPdfPages } = await import("./pdf-render.js"); snaps = await renderPdfPages(fs.readFileSync(lo.pdf), origDir); snapBy = "LibreOffice"; }
|
|
70
|
+
catch (e) { log(`sem foto do original (${e.message})`); }
|
|
71
|
+
finally { lo.cleanup(); }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// o que não sai igual (equação OLE em WMF/EMF, SmartArt): recorte da foto fiel do slide, na mesma posição
|
|
76
|
+
const isMeta = (n) => /\.(wmf|emf)$/i.test(n || "");
|
|
77
|
+
const jobs = [];
|
|
78
|
+
for (const s of res.slides) {
|
|
79
|
+
const snap = path.join(origDir, `slide-${String(s.n).padStart(2, "0")}.png`);
|
|
80
|
+
const has = snaps.length && fs.existsSync(snap);
|
|
81
|
+
s.elements.forEach((e, k) => {
|
|
82
|
+
if (!(e.fromSnapshot || (e.ole && isMeta(e.image)))) return;
|
|
83
|
+
if (!has) return;
|
|
84
|
+
const rel = `imagens/original/recorte-${String(s.n).padStart(2, "0")}-${k + 1}.png`;
|
|
85
|
+
jobs.push({ src: snap, dst: path.join(dir, ...rel.split("/")), x: e.x, y: e.y, w: e.w, h: e.h, e, rel });
|
|
86
|
+
});
|
|
87
|
+
if (!has) s.elements = s.elements.filter((e) => !e.fromSnapshot);
|
|
88
|
+
}
|
|
89
|
+
if (jobs.length) {
|
|
90
|
+
log(`recortando ${jobs.length} objeto(s) da foto do original`);
|
|
91
|
+
try {
|
|
92
|
+
const { cropRegions } = await import("./crop.js");
|
|
93
|
+
const ok = new Set(await cropRegions(jobs));
|
|
94
|
+
for (const j of jobs) if (ok.has(j.dst)) { j.e.image = j.rel; j.e.fit = "fill"; delete j.e.crop; delete j.e.fromSnapshot; j.e.fromOriginal = true; }
|
|
95
|
+
} catch (e) { log(`sem recorte (${e.message})`); }
|
|
96
|
+
for (const s of res.slides) s.elements = s.elements.filter((e) => !e.fromSnapshot);
|
|
97
|
+
}
|
|
98
|
+
const snapOf = (n) => { const f = `original/slide-${String(n).padStart(2, "0")}.png`; return fs.existsSync(path.join(dir, f)) ? f : null; };
|
|
99
|
+
// deck
|
|
100
|
+
const slides = res.slides.map((s) => {
|
|
101
|
+
const bgColor = s.background?.color && s.background.color.toUpperCase() !== "FFFFFF" ? s.background.color : undefined;
|
|
102
|
+
const out = { layout: "canvas", title: s.title || `Slide ${s.n}` };
|
|
103
|
+
if (bgColor) out.bg = bgColor;
|
|
104
|
+
if (s.background?.image) out.background = { image: resolve(s.background.image), fit: "fill" };
|
|
105
|
+
out.elements = s.elements.map((e) => { const c = fix({ ...e }); delete c.kind; return c; });
|
|
106
|
+
if (s.notes) out.notes = s.notes;
|
|
107
|
+
out.original = { slide: s.n, ...(snapOf(s.n) ? { image: snapOf(s.n) } : {}), ...(s.layoutName ? { layout: s.layoutName } : {}) };
|
|
108
|
+
if (s.hidden) out.hidden = true;
|
|
109
|
+
return out;
|
|
110
|
+
});
|
|
111
|
+
const title = res.slides.map((s) => s.title).find((t) => t && t.length > 3) || path.basename(fileName).replace(/\.[a-z]+$/i, "");
|
|
112
|
+
const spec = {
|
|
113
|
+
title,
|
|
114
|
+
...(aspectOf(res.cx, res.cy) ? { aspect: aspectOf(res.cx, res.cy) } : {}),
|
|
115
|
+
theme: "manual",
|
|
116
|
+
footer: false,
|
|
117
|
+
import: { from: path.basename(fileName), slides: slides.length, fonts: res.fonts, colors: res.scheme, ...(snapBy ? { snapshots: snapBy } : {}), ...(res.warnings.length ? { warnings: res.warnings.slice(0, 40) } : {}) },
|
|
118
|
+
slides,
|
|
119
|
+
};
|
|
120
|
+
return { spec, media: res.media.length, converted: rename.size, snapshots: snaps.length, snapBy, warnings: res.warnings };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// texto de cada slide importado, compacto, para a IA ler o original (títulos, textos por posição, tabelas, notas)
|
|
124
|
+
export function outlineOf(spec) {
|
|
125
|
+
return (spec.slides || []).map((s, i) => {
|
|
126
|
+
const els = (s.elements || []).filter((e) => !e.deco);
|
|
127
|
+
const texts = els.filter((e) => e.textbox).map((e) => plainOf(e).trim()).filter((t) => t && !/^\d{1,3}$/.test(t));
|
|
128
|
+
const tables = els.filter((e) => e.tableData).map((e) => e.tableData.map((r) => r.join(" | ")).join("\n"));
|
|
129
|
+
const imgs = els.filter((e) => e.image).length;
|
|
130
|
+
return [`## ${i + 1}. ${s.title || ""}`, ...texts, ...tables.map((t) => `[tabela]\n${t}`), imgs ? `[${imgs} imagem(ns)]` : "", s.notes ? `[notas] ${s.notes}` : ""].filter(Boolean).join("\n");
|
|
131
|
+
}).join("\n\n");
|
|
132
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import {execFile} from 'node:child_process';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
|
|
8
|
+
const run=promisify(execFile), conversions=new Map();
|
|
9
|
+
// Word legado precisa de um conversor real: não decodifique o binário como texto.
|
|
10
|
+
export async function convertLegacyWord(bytes,format='docx') {
|
|
11
|
+
if(!['docx','pdf'].includes(format))throw Error('Formato de conversão inválido.');
|
|
12
|
+
const key=crypto.createHash('sha256').update(bytes).digest('hex')+format;
|
|
13
|
+
if(conversions.has(key))return conversions.get(key);
|
|
14
|
+
const pending=(async()=>{
|
|
15
|
+
const dir=await fs.mkdtemp(path.join(os.tmpdir(),'sagadeck-word-'));
|
|
16
|
+
const input=path.join(dir,'source.doc'),output=path.join(dir,'source.'+format);
|
|
17
|
+
try {
|
|
18
|
+
await fs.writeFile(input,bytes);
|
|
19
|
+
if(process.platform==='win32') {
|
|
20
|
+
const script=path.join(dir,'convert.ps1');
|
|
21
|
+
await fs.writeFile(script,`param([string]$inputFile,[string]$outputFile)
|
|
22
|
+
$word=$null; $document=$null
|
|
23
|
+
try {
|
|
24
|
+
$word=New-Object -ComObject Word.Application
|
|
25
|
+
$word.Visible=$false; $word.DisplayAlerts=0; $word.AutomationSecurity=3
|
|
26
|
+
$document=$word.Documents.Open($inputFile,$false,$true)
|
|
27
|
+
$document.SaveAs2($outputFile,${format==='pdf'?17:16})
|
|
28
|
+
} finally {
|
|
29
|
+
if($document){$document.Close(0); [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($document)}
|
|
30
|
+
if($word){$word.Quit(); [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($word)}
|
|
31
|
+
}`);
|
|
32
|
+
const command=(await fs.readFile(script,'utf8')).replace('param([string]$inputFile,[string]$outputFile)',`$ErrorActionPreference='Stop'; $inputFile='${input.replace(/'/g,"''")}'; $outputFile='${output.replace(/'/g,"''")}'`);
|
|
33
|
+
await run('powershell.exe',['-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(command,'utf16le').toString('base64')],{timeout:120000,windowsHide:true});
|
|
34
|
+
} else await run(process.env.SAGADECK_OFFICE_BIN||'libreoffice',['-env:UserInstallation=file://'+path.join(dir,'profile'),'--headless','--convert-to',format,'--outdir',dir,input],{timeout:120000});
|
|
35
|
+
return await fs.readFile(output);
|
|
36
|
+
} catch(cause) {throw new Error('Não foi possÃvel converter o Word antigo (.doc). É necessário Microsoft Word no Windows ou LibreOffice no servidor; você também pode enviar o arquivo como .docx.',{cause});}
|
|
37
|
+
finally {await fs.rm(dir,{recursive:true,force:true});}
|
|
38
|
+
})();
|
|
39
|
+
conversions.set(key,pending);
|
|
40
|
+
try {return await pending;} catch(e){conversions.delete(key);throw e;}
|
|
41
|
+
finally {if(conversions.size>4)conversions.delete(conversions.keys().next().value);}
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Slides progressivos → um slide que se monta por cliques. Muita gente "anima" no PowerPoint copiando o slide e
|
|
2
|
+
// acrescentando partes (o ciclo da água em 7 slides). Aqui cada elemento aparece no clique em que surgiu (step) e
|
|
3
|
+
// some no clique em que saiu (exit). Sem IA: é o mesmo desenho, só que num slide.
|
|
4
|
+
const keyOf = (e) => { const { step, exit, deco, ...rest } = e || {}; return JSON.stringify(rest); };
|
|
5
|
+
|
|
6
|
+
export function mergeProgressive(slides) {
|
|
7
|
+
const list = (slides || []).filter((s) => s && Array.isArray(s.elements));
|
|
8
|
+
if (!list.length) throw new Error("nenhum slide com elementos para juntar");
|
|
9
|
+
if (list.length === 1) return structuredClone(list[0]);
|
|
10
|
+
const out = [];
|
|
11
|
+
let alive = new Map(); // chave -> elemento no resultado (vivo no passo anterior)
|
|
12
|
+
list.forEach((s, k) => {
|
|
13
|
+
const now = new Map();
|
|
14
|
+
for (const e of s.elements) {
|
|
15
|
+
const key = keyOf(e);
|
|
16
|
+
if (now.has(key)) { // repetido no mesmo slide: outra instância
|
|
17
|
+
const c = structuredClone(e); if (k > 0) c.step = k; out.push(c); continue;
|
|
18
|
+
}
|
|
19
|
+
if (alive.has(key)) now.set(key, alive.get(key));
|
|
20
|
+
else { const c = structuredClone(e); if (k > 0) c.step = k; out.push(c); now.set(key, c); }
|
|
21
|
+
}
|
|
22
|
+
for (const [key, el] of alive) if (!now.has(key)) el.exit = k;
|
|
23
|
+
alive = now;
|
|
24
|
+
});
|
|
25
|
+
const first = list[0];
|
|
26
|
+
const notes = list.map((s) => s.notes).filter(Boolean).join("\n\n");
|
|
27
|
+
return {
|
|
28
|
+
...structuredClone({ ...first, elements: undefined, notes: undefined }),
|
|
29
|
+
elements: out,
|
|
30
|
+
...(notes ? { notes } : {}),
|
|
31
|
+
original: { ...(first.original || {}), merged: list.map((s) => s.original?.slide).filter(Boolean) },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// é uma sequência progressiva? (cada slide mantém boa parte do anterior: o mesmo desenho crescendo)
|
|
36
|
+
export function isProgressive(slides) {
|
|
37
|
+
if (!slides || slides.length < 2) return false;
|
|
38
|
+
for (let k = 1; k < slides.length; k++) {
|
|
39
|
+
const prev = new Set((slides[k - 1].elements || []).map(keyOf));
|
|
40
|
+
const cur = (slides[k].elements || []).map(keyOf);
|
|
41
|
+
const kept = cur.filter((x) => prev.has(x)).length;
|
|
42
|
+
if (!prev.size || kept / prev.size < 0.5) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// O que o sistema oferece para importar melhor (tudo opcional; sem nada disso a importação funciona igual):
|
|
2
|
+
// - Windows: WMF/EMF (figuras antigas do Office) viram PNG pelo próprio Windows (System.Drawing), sem Office.
|
|
3
|
+
// - PowerPoint instalado: a foto fiel de cada slide (para comparar e para a IA ver). Abre uma CÓPIA do arquivo,
|
|
4
|
+
// só para leitura e sem janela; fecha só o que abriu e nunca fecha o PowerPoint de quem está usando.
|
|
5
|
+
// - LibreOffice (comum no Linux): converte o .pptx em PDF, e as páginas viram PNG pelo pdf.js (src/import/pdf-render.js);
|
|
6
|
+
// também converte WMF/EMF.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const isWin = process.platform === "win32";
|
|
13
|
+
const tmpDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "sagadeck-import-"));
|
|
14
|
+
|
|
15
|
+
function powershell(script, args, timeout = 600000) {
|
|
16
|
+
const dir = tmpDir();
|
|
17
|
+
try {
|
|
18
|
+
const ps1 = path.join(dir, "run.ps1"), json = path.join(dir, "args.json");
|
|
19
|
+
fs.writeFileSync(ps1, "\uFEFF" + script, "utf8");
|
|
20
|
+
fs.writeFileSync(json, JSON.stringify(args), "utf8");
|
|
21
|
+
const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ps1, json], { encoding: "utf8", timeout, windowsHide: true });
|
|
22
|
+
return { ok: r.status === 0, out: (r.stdout || "").trim(), err: (r.stderr || "").trim() };
|
|
23
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let officeCache = null;
|
|
27
|
+
export function powerPointAvailable() {
|
|
28
|
+
if (!isWin || process.env.SAGADECK_NO_OFFICE) return false;
|
|
29
|
+
if (officeCache != null) return officeCache;
|
|
30
|
+
const r = powershell("$t = [type]::GetTypeFromProgID('PowerPoint.Application'); if ($t) { 'sim' } else { 'nao' }", {}, 30000);
|
|
31
|
+
officeCache = r.ok && r.out === "sim";
|
|
32
|
+
return officeCache;
|
|
33
|
+
}
|
|
34
|
+
export function libreOfficeBin() {
|
|
35
|
+
if (process.env.SAGADECK_NO_OFFICE) return null;
|
|
36
|
+
const names = isWin ? ["soffice.exe"] : ["soffice", "libreoffice"];
|
|
37
|
+
const dirs = [...String(process.env.PATH || "").split(path.delimiter), ...(isWin ? ["C:\\Program Files\\LibreOffice\\program", "C:\\Program Files (x86)\\LibreOffice\\program"] : ["/usr/bin", "/usr/local/bin", "/opt/libreoffice/program", "/Applications/LibreOffice.app/Contents/MacOS"])];
|
|
38
|
+
for (const d of dirs) for (const n of names) { const f = path.join(d, n); if (d && fs.existsSync(f)) return f; }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// WMF/EMF → PNG. pairs: [{ src, dst }] (caminhos absolutos). Devolve os dst que ficaram prontos.
|
|
43
|
+
export function metafilesToPng(pairs, { maxSide = 2400 } = {}) {
|
|
44
|
+
if (!pairs.length) return [];
|
|
45
|
+
if (isWin && !process.env.SAGADECK_NO_OFFICE) {
|
|
46
|
+
const script = `
|
|
47
|
+
$ErrorActionPreference = 'Continue'
|
|
48
|
+
Add-Type -AssemblyName System.Drawing
|
|
49
|
+
$a = Get-Content -Raw -Encoding UTF8 $args[0] | ConvertFrom-Json
|
|
50
|
+
foreach ($p in $a.pairs) {
|
|
51
|
+
try {
|
|
52
|
+
$m = New-Object System.Drawing.Imaging.Metafile($p.src)
|
|
53
|
+
$w = [double]$m.Width; $h = [double]$m.Height
|
|
54
|
+
if ($w -le 0 -or $h -le 0) { $m.Dispose(); continue }
|
|
55
|
+
$k = [Math]::Min(4.0, [Math]::Max(1.0, $a.maxSide / [Math]::Max($w, $h)))
|
|
56
|
+
if ([Math]::Max($w, $h) * $k -gt $a.maxSide) { $k = $a.maxSide / [Math]::Max($w, $h) }
|
|
57
|
+
$bw = [int][Math]::Max(1, $w * $k); $bh = [int][Math]::Max(1, $h * $k)
|
|
58
|
+
$b = New-Object System.Drawing.Bitmap($bw, $bh)
|
|
59
|
+
$g = [System.Drawing.Graphics]::FromImage($b)
|
|
60
|
+
$g.SmoothingMode = 'AntiAlias'; $g.InterpolationMode = 'HighQualityBicubic'; $g.TextRenderingHint = 'AntiAliasGridFit'
|
|
61
|
+
$g.Clear([System.Drawing.Color]::Transparent)
|
|
62
|
+
$g.DrawImage($m, 0, 0, $bw, $bh)
|
|
63
|
+
$b.Save($p.dst, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
64
|
+
$g.Dispose(); $b.Dispose(); $m.Dispose()
|
|
65
|
+
Write-Output ("ok " + $p.dst)
|
|
66
|
+
} catch { Write-Output ("falhou " + $p.src) }
|
|
67
|
+
}`;
|
|
68
|
+
powershell(script, { pairs, maxSide });
|
|
69
|
+
return pairs.filter((p) => fs.existsSync(p.dst)).map((p) => p.dst);
|
|
70
|
+
}
|
|
71
|
+
const lo = libreOfficeBin();
|
|
72
|
+
if (lo) {
|
|
73
|
+
for (const p of pairs) {
|
|
74
|
+
const out = tmpDir();
|
|
75
|
+
spawnSync(lo, ["--headless", "--convert-to", "png", "--outdir", out, p.src], { timeout: 120000 });
|
|
76
|
+
const f = fs.readdirSync(out).find((x) => x.endsWith(".png"));
|
|
77
|
+
if (f) fs.copyFileSync(path.join(out, f), p.dst);
|
|
78
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
return pairs.filter((p) => fs.existsSync(p.dst)).map((p) => p.dst);
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Foto de cada slide pelo PowerPoint: outDir/slide-01.png… Devolve a lista de arquivos (vazia se não deu).
|
|
86
|
+
export function powerPointSnapshots(pptxFile, outDir, { width = 1920 } = {}) {
|
|
87
|
+
if (!powerPointAvailable()) return [];
|
|
88
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
89
|
+
const work = tmpDir();
|
|
90
|
+
const copy = path.join(work, "copia" + path.extname(pptxFile));
|
|
91
|
+
fs.copyFileSync(pptxFile, copy); // trabalha numa cópia: o original pode estar aberto (e travado) no PowerPoint
|
|
92
|
+
const script = `
|
|
93
|
+
$ErrorActionPreference = 'Stop'
|
|
94
|
+
$a = Get-Content -Raw -Encoding UTF8 $args[0] | ConvertFrom-Json
|
|
95
|
+
$pp = New-Object -ComObject PowerPoint.Application
|
|
96
|
+
$had = $pp.Presentations.Count
|
|
97
|
+
$pres = $pp.Presentations.Open($a.file, -1, 0, 0)
|
|
98
|
+
try {
|
|
99
|
+
$h = [int]([double]$a.width * $pres.PageSetup.SlideHeight / $pres.PageSetup.SlideWidth)
|
|
100
|
+
for ($i = 1; $i -le $pres.Slides.Count; $i++) {
|
|
101
|
+
$pres.Slides.Item($i).Export((Join-Path $a.out ('slide-{0:D2}.png' -f $i)), 'PNG', [int]$a.width, $h)
|
|
102
|
+
}
|
|
103
|
+
Write-Output $pres.Slides.Count
|
|
104
|
+
} finally {
|
|
105
|
+
$pres.Close()
|
|
106
|
+
if ($had -eq 0 -and $pp.Presentations.Count -eq 0) { $pp.Quit() }
|
|
107
|
+
}`;
|
|
108
|
+
try {
|
|
109
|
+
const r = powershell(script, { file: copy, out: outDir, width });
|
|
110
|
+
if (!r.ok) return [];
|
|
111
|
+
} finally { fs.rmSync(work, { recursive: true, force: true }); }
|
|
112
|
+
return fs.readdirSync(outDir).filter((f) => /^slide-\d+\.png$/.test(f)).sort().map((f) => path.join(outDir, f));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// .pptx → .pdf pelo LibreOffice (para fotografar as páginas quando não há PowerPoint)
|
|
116
|
+
export function libreOfficeToPdf(file) {
|
|
117
|
+
const lo = libreOfficeBin();
|
|
118
|
+
if (!lo) return null;
|
|
119
|
+
const out = tmpDir();
|
|
120
|
+
spawnSync(lo, ["--headless", "--convert-to", "pdf", "--outdir", out, file], { timeout: 300000 });
|
|
121
|
+
const f = fs.readdirSync(out).find((x) => x.endsWith(".pdf"));
|
|
122
|
+
return f ? { pdf: path.join(out, f), cleanup: () => fs.rmSync(out, { recursive: true, force: true }) } : null;
|
|
123
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Equação do Office (OMML, <m:oMath>) → LaTeX para o KaTeX: frações, índices, raízes, somatórios/integrais,
|
|
2
|
+
// parênteses, barras e acentos, funções, matrizes. O que não conhece vira o texto que tem dentro.
|
|
3
|
+
import { kids, kid, textOf } from "./xml.js";
|
|
4
|
+
|
|
5
|
+
const NARY = { "∑": "\\sum", "∏": "\\prod", "∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint", "⋃": "\\bigcup", "⋂": "\\bigcap" };
|
|
6
|
+
const SYM = { "α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta", "Δ": "\\Delta", "ε": "\\varepsilon", "θ": "\\theta", "λ": "\\lambda", "μ": "\\mu", "ν": "\\nu", "π": "\\pi", "ρ": "\\rho", "σ": "\\sigma", "Σ": "\\Sigma", "τ": "\\tau", "φ": "\\varphi", "ω": "\\omega", "Ω": "\\Omega", "≤": "\\le ", "≥": "\\ge ", "≠": "\\ne ", "≈": "\\approx ", "×": "\\times ", "·": "\\cdot ", "∞": "\\infty ", "→": "\\to ", "±": "\\pm ", "∂": "\\partial ", "∇": "\\nabla " };
|
|
7
|
+
const ACC = { "̅": "\\overline", "̂": "\\hat", "̃": "\\tilde", "̇": "\\dot", "⃗": "\\vec", "̄": "\\bar" };
|
|
8
|
+
const FUNCS = new Set(["sin", "cos", "tan", "log", "ln", "exp", "lim", "max", "min", "sec", "csc", "cot", "arcsin", "arccos", "arctan"]);
|
|
9
|
+
|
|
10
|
+
// letras matemáticas do Unicode (𝑥, 𝐀, 𝛼…) que o Word usa: viram a letra comum, que o KaTeX já põe em itálico
|
|
11
|
+
function plainLetter(c) {
|
|
12
|
+
const cp = c.codePointAt(0);
|
|
13
|
+
if (cp === 0x210e) return "h";
|
|
14
|
+
if (cp >= 0x1d400 && cp <= 0x1d6a3) { const k = (cp - 0x1d400) % 52; return String.fromCharCode(k < 26 ? 65 + k : 97 + k - 26); }
|
|
15
|
+
if (cp >= 0x1d6a8 && cp <= 0x1d7c9) { const greek = "ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡϴΣΤΥΦΧΨΩ∇αβγδεζηθικλμνξοπρςστυφχψω∂ϵϑϰϕϱϖ"; return greek[(cp - 0x1d6a8) % 58] || c; }
|
|
16
|
+
if (cp >= 0x1d7ce && cp <= 0x1d7ff) return String((cp - 0x1d7ce) % 10);
|
|
17
|
+
return c;
|
|
18
|
+
}
|
|
19
|
+
const esc = (s) => [...s].map(plainLetter).map((c) => (SYM[c] ? SYM[c].replace(/([a-zA-Z])$/, "$1 ") : /[{}#$%&_]/.test(c) ? `\\${c}` : c)).join("");
|
|
20
|
+
const val = (n, name) => kid(kid(n, name), "val")?.attrs["m:val"] ?? kid(n, name)?.attrs?.["m:val"];
|
|
21
|
+
|
|
22
|
+
export function ommlToLatex(node) {
|
|
23
|
+
const conv = (n) => (n ? kids(n).map(one).join("") : "");
|
|
24
|
+
const arg = (n, name) => conv(kid(n, name));
|
|
25
|
+
const g = (s) => (s.length === 1 ? s : `{${s}}`);
|
|
26
|
+
function one(n) {
|
|
27
|
+
const k = n.name.split(":").pop();
|
|
28
|
+
switch (k) {
|
|
29
|
+
case "r": {
|
|
30
|
+
const t = kids(n, "t").map(textOf).join("");
|
|
31
|
+
const sty = kid(kid(n, "rPr"), "sty")?.attrs["m:val"];
|
|
32
|
+
if (FUNCS.has(t.trim())) return `\\${t.trim()} `;
|
|
33
|
+
return sty === "p" && /[a-z]{2,}/i.test(t) ? `\\mathrm{${esc(t)}}` : esc(t);
|
|
34
|
+
}
|
|
35
|
+
case "f": return `\\frac{${arg(n, "num")}}{${arg(n, "den")}}`;
|
|
36
|
+
case "sSup": return `${g(arg(n, "e"))}^{${arg(n, "sup")}}`;
|
|
37
|
+
case "sSub": return `${g(arg(n, "e"))}_{${arg(n, "sub")}}`;
|
|
38
|
+
case "sSubSup": return `${g(arg(n, "e"))}_{${arg(n, "sub")}}^{${arg(n, "sup")}}`;
|
|
39
|
+
case "sPre": return `{}_{${arg(n, "sub")}}^{${arg(n, "sup")}}${g(arg(n, "e"))}`;
|
|
40
|
+
case "rad": { const deg = arg(n, "deg"); return deg ? `\\sqrt[${deg}]{${arg(n, "e")}}` : `\\sqrt{${arg(n, "e")}}`; }
|
|
41
|
+
case "nary": {
|
|
42
|
+
const ch = kid(kid(n, "naryPr"), "chr")?.attrs["m:val"] || "∫";
|
|
43
|
+
const sub = arg(n, "sub"), sup = arg(n, "sup");
|
|
44
|
+
return `${NARY[ch] || esc(ch)}${sub ? `_{${sub}}` : ""}${sup ? `^{${sup}}` : ""} ${arg(n, "e")}`;
|
|
45
|
+
}
|
|
46
|
+
case "d": {
|
|
47
|
+
const pr = kid(n, "dPr");
|
|
48
|
+
const beg = kid(pr, "begChr")?.attrs["m:val"] ?? "(", end = kid(pr, "endChr")?.attrs["m:val"] ?? ")";
|
|
49
|
+
const sep = kid(pr, "sepChr")?.attrs["m:val"] ?? ",";
|
|
50
|
+
const L = (c) => (c === "" ? "." : c === "{" ? "\\{" : c === "}" ? "\\}" : c === "|" ? "|" : c === "‖" ? "\\|" : c === "⌈" ? "\\lceil" : c === "⌉" ? "\\rceil" : c === "⌊" ? "\\lfloor" : c === "⌋" ? "\\rfloor" : c === "〈" || c === "⟨" ? "\\langle" : c === "〉" || c === "⟩" ? "\\rangle" : c);
|
|
51
|
+
return `\\left${L(beg)}${kids(n, "e").map(conv).join(sep)}\\right${L(end)}`;
|
|
52
|
+
}
|
|
53
|
+
case "bar": return `${kid(kid(n, "barPr"), "pos")?.attrs["m:val"] === "bot" ? "\\underline" : "\\overline"}{${arg(n, "e")}}`;
|
|
54
|
+
case "acc": { const ch = kid(kid(n, "accPr"), "chr")?.attrs["m:val"] || "̂"; return `${ACC[ch] || "\\hat"}{${arg(n, "e")}}`; }
|
|
55
|
+
case "func": return `${arg(n, "fName").trim()} ${arg(n, "e")}`;
|
|
56
|
+
case "limLow": return `${arg(n, "e")}_{${arg(n, "lim")}}`;
|
|
57
|
+
case "limUpp": return `${arg(n, "e")}^{${arg(n, "lim")}}`;
|
|
58
|
+
case "groupChr": return `\\underbrace{${arg(n, "e")}}`;
|
|
59
|
+
case "box": case "borderBox": return arg(n, "e");
|
|
60
|
+
case "eqArr": return `\\begin{aligned}${kids(n, "e").map(conv).join("\\\\")}\\end{aligned}`;
|
|
61
|
+
case "m": return `\\begin{matrix}${kids(n, "mr").map((r) => kids(r, "e").map(conv).join("&")).join("\\\\")}\\end{matrix}`;
|
|
62
|
+
case "e": case "num": case "den": case "sub": case "sup": case "deg": case "fName": case "lim": case "oMath": case "oMathPara": return conv(n);
|
|
63
|
+
case "rPr": case "fPr": case "sSupPr": case "sSubPr": case "sSubSupPr": case "radPr": case "naryPr": case "dPr": case "barPr": case "accPr": case "funcPr": case "ctrlPr": case "limLowPr": case "limUppPr": case "eqArrPr": case "mPr": case "groupChrPr": case "boxPr": case "borderBoxPr": case "sPrePr": case "oMathParaPr": return "";
|
|
64
|
+
default: return conv(n) || textOf(n);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return one(node).replace(/\s+/g, " ").trim();
|
|
68
|
+
}
|
|
69
|
+
void val;
|