/** * Gera a página de entrada de cada formação e a lista geral de formações. * * npm run formacoes-viewer * * Cada pasta public/formacoes/N/ descreve o próprio material num * formacao.json. Daqui saem: * public/formacoes/N/index.html a página servida em /formacoes/N * public/formacoes/index.html a lista servida em /formacoes * * O que dá para medir não fica no JSON: tamanho de cada arquivo, número de * páginas dos PDFs e o roteiro da aula, lido das aberturas de seção do * slide.html. Assim a página não desatualiza quando o material muda. * * Os links são absolutos (/formacoes/N/...) porque o middleware serve a * página em /formacoes/N, sem barra final, e um link relativo apontaria para * a pasta de cima. */ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const pastaFormacoes = path.join(raiz, "public", "formacoes"); const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); function tamanho(bytes) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`; return `${(bytes / 1024 / 1024).toFixed(1).replace(".", ",")} MB`; } /* Conta os objetos /Type /Page de um PDF. Basta para os PDFs do Chrome, que não comprimem a árvore de páginas. */ function paginasPdf(arquivo) { const bruto = fs.readFileSync(arquivo, "latin1"); return (bruto.match(/\/Type\s*\/Page(?![s\w])/g) || []).length; } /* As aberturas de seção do slide.html viram o roteiro da aula, cada uma com link para o slide em que começa. */ function roteiro(pasta) { const html = fs.readFileSync(path.join(pasta, "slide.html"), "utf8"); const slides = [...html.matchAll(/
]*data-titulo="([^"]*)"/g)]; const partes = []; slides.forEach((m, i) => { if (!/\bsecao\b/.test(m[1])) return; partes.push({ indice: i + 1, titulo: m[2].replace(/^§\d+\s*·\s*/, "") }); }); partes.forEach((p, i) => { const fim = i + 1 < partes.length ? partes[i + 1].indice - 1 : slides.length; p.quantos = fim - p.indice + 1; }); return { partes, total: slides.length }; } const ICONES = { slides: '', apostila: '', pdf: '', codigo: '', zip: '', imagem: '', fritzing: '', seta: '', baixar: '', tocar: '', livro: '', }; const icone = (nome) => ``; const CLASSE_ICONE = { pdf: "pdf", codigo: "codigo", zip: "codigo", imagem: "imagem" }; const cabecalho = (titulo, descricao) => ` ${esc(titulo)} `; const topo = (nav) => `
LASER Liga Acadêmica de Sistemas Embarcados
Centro de Informática · UFPE
`; const rodape = `
LASER · Liga Acadêmica de Sistemas Embarcados · CIn · UFPE Material de Lucas Rayan Guerra · uso livre para fins educacionais · contato@lucasrguerra.dev.br
`; function paginaFormacao(pasta, dados) { const n = dados.modulo; const base = `/formacoes/${n}`; const url = (arq) => (/^https?:/.test(arq) ? arq : `${base}/${arq}`); const info = (arq) => { const caminho = path.join(pasta, arq); if (!fs.existsSync(caminho)) throw new Error(`formacao ${n}: não achei ${arq}`); const bytes = fs.statSync(caminho).size; const paginas = arq.endsWith(".pdf") ? paginasPdf(caminho) : 0; return { bytes, paginas }; }; const { partes, total } = roteiro(pasta); const pdfSlides = `LASER-ESP32-Modulo-${n}-Slides.pdf`; const pdfApostila = `LASER-ESP32-Modulo-${n}-Apostila.pdf`; const iSlides = info(pdfSlides); const iApostila = info(pdfApostila); const fichas = dados.fichas.map(([k, v]) => `
  • ${esc(k)}: ${esc(v)}
  • `).join(""); const acao = ([rotulo, arq, baixar], primario) => `${icone(baixar ? "baixar" : "seta")}${esc(rotulo)}`; const grupos = dados.grupos.map((g) => { const itens = g.arquivos.map((a) => { const { bytes, paginas } = info(a.arquivo); const nome = path.basename(a.arquivo); const meta = paginas ? `${paginas} págs · ${tamanho(bytes)}` : tamanho(bytes); const botoes = []; if (a.tipo === "codigo") { botoes.push(`Baixar`); } else { if (a.abrir) botoes.push(`Abrir`); if (a.baixar) botoes.push(`Baixar`); } let codigo = ""; if (a.tipo === "codigo") { const fonte = fs.readFileSync(path.join(pasta, a.arquivo), "utf8"); codigo = `
    Ver código
    ${esc(fonte.trimEnd())}
    `; } return `
  • ${icone(a.tipo)}
    ${esc(nome)}
    ${a.descricao}
    ${codigo}
    ${meta}
    ${botoes.join("")}
  • `; }).join(""); return `

    ${esc(g.titulo)}

      ${itens}
    `; }).join(""); const guias = [ `

    Apresentando os slides

    Abra slide.html no navegador. O palco é 16:9 e se ajusta a qualquer tela ou projetor.

    →←
    avançar e voltar (ou clique nas metades da tela)
    F
    tela cheia
    O
    panorama com todos os slides
    B
    tela preta, para pausar a projeção
    `, `

    Lendo e imprimindo a apostila

    A versão online se pagina em A4 sozinha ao abrir, o que leva alguns segundos.

    Para papel, prefira o PDF pronto: ele já sai com margens, cores e quebras de página certas, sem configurar a impressora.

    `, ...dados.guias.map((g) => `

    ${esc(g.titulo)}

    ${g.itens ? `
      ${g.itens.map((i) => `
    1. ${i}
    2. `).join("")}
    ` : ""} ${g.texto ? `

    ${g.texto}

    ` : ""}
    `), ].join("\n\t\t"); const listaRoteiro = partes.map((p, i) => `
  • ${i + 1}${esc(p.titulo)}slide ${p.indice} · ${p.quantos} slide${p.quantos > 1 ? "s" : ""}
  • `).join(""); const titulo = `LASER · Formação em ESP32 · Módulo ${n}: ${dados.titulo.join(" ")}`; return cabecalho(titulo, dados.descricao) + topo(`ArquivosComo usarFormações`) + `
    Formação em ESP32 · Módulo ${n}

    ${esc(dados.titulo[0])}${esc(dados.titulo[1])}

    ${esc(dados.subtitulo)}

      ${fichas}
    Capa da apostila do módulo ${n} Capa dos slides do módulo ${n}

    Comece por aqui

    Três formas de usar este material

    Para apresentar, para estudar e para pôr a mão no circuito. Os PDFs são a versão final, para quem só quer o material pronto.

    Apostila

    O conteúdo completo, com explicações, tabelas e o código comentado, paginado em A4.

    PDF · ${iApostila.paginas} págs · ${tamanho(iApostila.bytes)}

    ${esc(dados.pratica.titulo)}

    ${esc(dados.pratica.texto)}

    ${esc(dados.pratica.meta)}
    ${dados.pratica.acoes.map((a, i) => acao(a, i === 0)).join("")}

    Roteiro da aula

    ${partes.length} partes, ${total} slides

    Cada parte abre os slides direto no ponto em que ela começa.

      ${listaRoteiro}

    Todos os arquivos

    O que tem nesta formação

    Tudo o que foi usado na aula, com o que é cada arquivo e para que serve.

    ${grupos}

    Como usar

    Passo a passo

    ${guias}
    ` + rodape; } function paginaLista(formacoes) { const cartoes = formacoes.map(({ dados }) => { const base = `/formacoes/${dados.modulo}`; const duracao = (dados.fichas.find(([k]) => k === "Duração") || [])[1]; return `
    MÓDULO ${dados.modulo}${duracao ? ` · ${esc(duracao.toUpperCase())}` : ""}

    ${esc(dados.titulo.join(" "))}

    ${esc(dados.subtitulo)}

    `; }).join(""); return cabecalho("LASER · Formação em ESP32", "Slides, apostilas e arquivos de prática da formação em ESP32 da LASER, Liga Acadêmica de Sistemas Embarcados da UFPE.") + topo(`Módulos`) + `
    Formação em ESP32

    Material dasformações da LASER

    Slides, apostilas e arquivos de prática de cada módulo, para acompanhar a aula, revisar depois ou imprimir.


    Módulos

    ${formacoes.length} formações disponíveis

    ${cartoes}
    ` + rodape; } const formacoes = fs.readdirSync(pastaFormacoes) .filter((d) => /^\d+$/.test(d) && fs.existsSync(path.join(pastaFormacoes, d, "formacao.json"))) .sort((a, b) => a - b) .map((d) => { const pasta = path.join(pastaFormacoes, d); return { pasta, dados: JSON.parse(fs.readFileSync(path.join(pasta, "formacao.json"), "utf8")) }; }); for (const { pasta, dados } of formacoes) { fs.writeFileSync(path.join(pasta, "index.html"), paginaFormacao(pasta, dados)); console.log(`public/formacoes/${dados.modulo}/index.html`); } fs.writeFileSync(path.join(pastaFormacoes, "index.html"), paginaLista(formacoes)); console.log("public/formacoes/index.html");