From 03cb7b06da2ab52d8ec3c07a347f9a23b6292840 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 08:37:07 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Trova=20ed=20esegui=20tlmgr=20su=20Windows,?= =?UTF-8?q?=20dove=20=C3=A8=20un=20.bat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Su Windows TeX Live installa tlmgr come tlmgr.bat, ma il rilevamento cercava solo tlmgr.exe. Non trovandolo, rilevaTeX restituiva tlmgr: null e l'installazione dei pacchetti di base in main.js — che sta dentro un if (tex.tlmgr) — veniva saltata senza dire niente: il registro passava da "Distribuzione installata" a "Pronto" con la distribuzione a metà, la pagina Pacchetti restava inattiva e htlatex non c'era. Ora i comandi si cercano provando tutti i suffissi eseguibili (.exe, .bat, .cmd, nessuno) invece di assumere .exe. Non basta però trovarli: spawn ed execFile non sanno eseguire uno script .bat, quindi i comandi che lo sono passano per cmd.exe con la riga citata a mano, perché i percorsi possono contenere spazi (C:\Users\Nome Cognome\...). Le due conseguenze visibili sono sistemate a loro volta: se dopo l'installazione tlmgr non c'è, il registro lo dice invece di chiudere con "Pronto", e la pagina Pacchetti non annuncia più "stai usando il LaTeX di sistema" a chi ha installato la copia gestita da texforge. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017W5az9Prp2cseUs6W4zi2N --- desktop/latex.js | 60 ++++++++++++++++++++++++++++++----------- desktop/main.js | 5 ++++ desktop/renderer/app.js | 6 ++++- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/desktop/latex.js b/desktop/latex.js index ac6cc5f..a462376 100644 --- a/desktop/latex.js +++ b/desktop/latex.js @@ -99,8 +99,34 @@ function cartelleBin(radice) { .filter((d) => fs.statSync(d).isDirectory()); } -function nomeEseguibile(nome) { - return PIATTAFORMA === 'win32' ? `${nome}.exe` : nome; +// Su Windows non tutti i comandi di TeX Live sono .exe: tlmgr, per dirne uno, +// è tlmgr.bat. Cercare solo .exe lo rende invisibile e la distribuzione +// sembra non gestibile. Proviamo quindi tutti i suffissi eseguibili. +const SUFFISSI_ESEGUIBILE = PIATTAFORMA === 'win32' ? ['.exe', '.bat', '.cmd', ''] : ['']; + +function trovaEseguibile(dir, nome) { + for (const suffisso of SUFFISSI_ESEGUIBILE) { + const candidato = path.join(dir, nome + suffisso); + if (fs.existsSync(candidato)) return candidato; + } + return null; +} + +// spawn/execFile non sanno eseguire uno script .bat o .cmd: vanno passati a +// cmd.exe. La riga viene composta a mano e citata perché i percorsi possono +// contenere spazi (C:\Users\Nome Cognome\...). +function comandoPiattaforma(eseguibile, argomenti = [], opzioni = {}) { + if (PIATTAFORMA !== 'win32' || !/\.(bat|cmd)$/i.test(eseguibile)) { + return { comando: eseguibile, argomenti, opzioni }; + } + const riga = [eseguibile, ...argomenti] + .map((a) => `"${String(a).replace(/"/g, '""')}"`) + .join(' '); + return { + comando: process.env.COMSPEC || 'cmd.exe', + argomenti: ['/d', '/s', '/c', `"${riga}"`], + opzioni: { ...opzioni, windowsVerbatimArguments: true }, + }; } // --------------------------------------------------------------------------- @@ -122,7 +148,8 @@ async function esisteNelPath(nome) { async function versioneDi(eseguibile) { try { - const { stdout } = await execFileAsync(eseguibile, ['--version'], { timeout: 15000 }); + const c = comandoPiattaforma(eseguibile, ['--version'], { timeout: 15000 }); + const { stdout } = await execFileAsync(c.comando, c.argomenti, c.opzioni); return String(stdout).split('\n')[0].trim(); } catch { return null; @@ -134,8 +161,8 @@ async function versioneDi(eseguibile) { export async function rilevaTeX(userDataDir) { const gestita = radiceGestita(userDataDir); for (const dir of cartelleBin(gestita)) { - const pdflatex = path.join(dir, nomeEseguibile('pdflatex')); - if (fs.existsSync(pdflatex)) { + const pdflatex = trovaEseguibile(dir, 'pdflatex'); + if (pdflatex) { const versione = await versioneDi(pdflatex); if (versione) { return { @@ -144,10 +171,8 @@ export async function rilevaTeX(userDataDir) { radice: gestita, versione, pdflatex, - htlatex: fs.existsSync(path.join(dir, nomeEseguibile('htlatex'))) - ? path.join(dir, nomeEseguibile('htlatex')) : null, - tlmgr: fs.existsSync(path.join(dir, nomeEseguibile('tlmgr'))) - ? path.join(dir, nomeEseguibile('tlmgr')) : null, + htlatex: trovaEseguibile(dir, 'htlatex'), + tlmgr: trovaEseguibile(dir, 'tlmgr'), }; } } @@ -402,17 +427,19 @@ function ambiente(tex) { async function tlmgr(tex, argomenti, { onLog, timeout = 300000 } = {}) { if (!tex?.tlmgr) throw new Error('tlmgr non disponibile in questa distribuzione'); - return eseguiConLog(tex.tlmgr, argomenti, { env: ambiente(tex), timeout }, onLog); + const c = comandoPiattaforma(tex.tlmgr, argomenti, { env: ambiente(tex), timeout }); + return eseguiConLog(c.comando, c.argomenti, c.opzioni, onLog); } async function tlmgrOutput(tex, argomenti, timeout = 120000) { if (!tex?.tlmgr) throw new Error('tlmgr non disponibile in questa distribuzione'); - const { stdout } = await execFileAsync(tex.tlmgr, argomenti, { + const c = comandoPiattaforma(tex.tlmgr, argomenti, { env: ambiente(tex), timeout, maxBuffer: 32 * 1024 * 1024, windowsHide: true, }); + const { stdout } = await execFileAsync(c.comando, c.argomenti, c.opzioni); return stdout; } @@ -491,19 +518,20 @@ export async function compilaPdf(tex, cartella, stem, onLog) { env: { ...ambiente(tex), openin_any: 'p', openout_any: 'p' }, timeout: 120000, }; + const c = comandoPiattaforma(tex.pdflatex, argomenti, opzioni); // Due passate: la seconda risolve riferimenti e indice. - await eseguiConLog(tex.pdflatex, argomenti, opzioni, onLog); - await eseguiConLog(tex.pdflatex, argomenti, opzioni, onLog); + await eseguiConLog(c.comando, c.argomenti, c.opzioni, onLog); + await eseguiConLog(c.comando, c.argomenti, c.opzioni, onLog); return path.join(cartella, `${stem}.pdf`); } export async function compilaHtml(tex, cartella, stem, cfgStem, onLog) { if (!tex.htlatex) throw new Error('htlatex non disponibile'); - const argomenti = cfgStem ? [stem, cfgStem] : [stem]; - await eseguiConLog(tex.htlatex, argomenti, { + const c = comandoPiattaforma(tex.htlatex, cfgStem ? [stem, cfgStem] : [stem], { cwd: cartella, env: { ...ambiente(tex), openin_any: 'p', openout_any: 'p' }, timeout: 120000, - }, onLog); + }); + await eseguiConLog(c.comando, c.argomenti, c.opzioni, onLog); return path.join(cartella, `${stem}.html`); } diff --git a/desktop/main.js b/desktop/main.js index 4fb5968..1e61ecf 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -155,6 +155,11 @@ ipcMain.handle('installa-tex', async () => { log(`\nAvviso: alcuni pacchetti di base non sono stati installati: ${e.message}\n`); }); tex = await latex.rilevaTeX(userData); + } else { + // Senza tlmgr non si installa nulla: meglio dirlo che chiudere con un + // "Pronto" che nasconde una distribuzione a metà. + log('\nAvviso: tlmgr non trovato nella distribuzione appena installata: ' + + 'i pacchetti di base non sono stati aggiunti e la pagina Pacchetti resterà inattiva.\n'); } log('\nPronto.\n'); diff --git a/desktop/renderer/app.js b/desktop/renderer/app.js index 896bc3d..a73ae24 100644 --- a/desktop/renderer/app.js +++ b/desktop/renderer/app.js @@ -246,7 +246,11 @@ async function caricaInstallati() { } if (!stato.tex.tlmgr) { $('avvisoPkg').hidden = false; - $('avvisoPkg').innerHTML = 'Questa distribuzione non è gestibile da texforge. Stai usando il LaTeX di sistema: installa i pacchetti con il gestore della tua distribuzione, oppure lascia che texforge installi la propria copia da Motore LaTeX.'; + // Il motivo cambia con l'origine: dire "stai usando il LaTeX di sistema" + // a chi ha installato la copia di texforge manda fuori strada. + $('avvisoPkg').innerHTML = stato.tex.tipo === 'gestita' + ? 'Questa distribuzione non è gestibile da texforge. La copia installata da texforge non contiene tlmgr: prova Reinstalla la distribuzione da Motore LaTeX.' + : 'Questa distribuzione non è gestibile da texforge. Stai usando il LaTeX di sistema: installa i pacchetti con il gestore della tua distribuzione, oppure lascia che texforge installi la propria copia da Motore LaTeX.'; elenco.innerHTML = '
  • '; return; } From 9ef9d2a35de9bce96cd6b209296673d4417350ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 08:47:21 +0000 Subject: [PATCH 2/3] Chiudi l'iniezione via cmd e documenta la via Overleaf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due cose separate. Sicurezza. Il passaggio dei comandi .bat per cmd.exe, introdotto per far funzionare tlmgr su Windows, componeva una riga di comando testuale raddoppiando le virgolette. Ma cmd non le tratta come virgolette letterali: le doppie chiudono e riaprono la citazione, e da lì in poi &, | e > tornano metacaratteri attivi. cercaPacchetto è l'unica funzione che passa a tlmgr una stringa dell'utente senza validarla (installa e rimuovi hanno l'allowlist), quindi un termine di ricerca come x"&calc&"x sarebbe finito eseguito. Ora un argomento che contiene virgolette viene rifiutato invece di essere citato male, e il termine di ricerca è ripulito a monte tenendo solo i caratteri che compaiono nei nomi dei pacchetti. Documentazione. Il PDF si può compilare anche su Overleaf, senza installare niente: bastano il .tex e quizstruct.sty nella stessa cartella del progetto, compilatore pdfLaTeX. Il pacchetto usa solo dipendenze standard, già presenti lì, e quiz.cfg/quiz.js/quiz.css non servono perché riguardano il solo HTML. Detto esplicitamente anche il confine fra i due output: l'HTML interattivo richiede htlatex e si ottiene solo con l'app. README e sito, con voce di menu e alternanza degli sfondi sistemata. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017W5az9Prp2cseUs6W4zi2N --- README.md | 28 +++++++++++++++++++++ desktop/latex.js | 19 +++++++++++--- docs/index.html | 64 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c14dbdc..e22bb82 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,34 @@ Il sorgente è un `.tex` **vero** — stesso documento, due output diversi. | comando | `\numerica{valore}{tolleranza}` | Risposta numerica libera | | comando | `\risultato` | Placeholder punteggio | +## 🌍 Il PDF si compila anche su Overleaf + +Il sorgente è LaTeX vero: per il **PDF** non serve texforge. Lo stesso `.tex` compila +su [Overleaf](https://www.overleaf.com) — comodo se sei su un computer della scuola, +su tablet, o se devi lavorare in due sullo stesso documento. + +1. Su Overleaf: **New Project → Blank Project** (oppure *Upload Project* se hai già uno ZIP). +2. Carica (**Upload**) nel progetto due file: il tuo `.tex` e **`quizstruct.sty`** + ([scaricalo qui](latex-source/quizstruct.sty), o prendilo dagli allegati di una + [release](https://github.com/Francy2009/TeXForge/releases/latest)). + Il `.sty` va nella **stessa cartella** del `.tex`: Overleaf lo trova da solo, non + serve installare niente. +3. Compilatore **pdfLaTeX** (`Menu → Compiler`), poi **Recompile**. + +`quizstruct.sty` usa solo pacchetti standard (`amsmath`, `amssymb`, `xcolor`, +`enumitem`, `pgffor`), già presenti nella TeX Live di Overleaf. + +**Non servono** `quiz.cfg`, `quiz.js` e `quiz.css`: quelli riguardano solo l'HTML. + +### E l'HTML? + +L'**HTML interattivo** — quello con le risposte cliccabili e il punteggio che si +calcola da solo — è la parte in più, e **si ottiene solo con l'app**: richiede +`htlatex` più i file di configurazione, che Overleaf non mette in conto. + +In pratica: su Overleaf ottieni il compito da stampare, con texforge ottieni **anche** +la versione che si corregge da sola. Stesso sorgente, nessuna modifica. + ## 📁 Struttura ``` diff --git a/desktop/latex.js b/desktop/latex.js index a462376..aa7ace0 100644 --- a/desktop/latex.js +++ b/desktop/latex.js @@ -115,13 +115,22 @@ function trovaEseguibile(dir, nome) { // spawn/execFile non sanno eseguire uno script .bat o .cmd: vanno passati a // cmd.exe. La riga viene composta a mano e citata perché i percorsi possono // contenere spazi (C:\Users\Nome Cognome\...). +// +// Attenzione: cmd non ha modo di rappresentare una virgoletta dentro una +// stringa citata. Raddoppiarla non la rende letterale, chiude e riapre le +// virgolette, e da lì in poi &, | e > tornano metacaratteri attivi: sarebbe +// esecuzione di comandi arbitrari. Un argomento che le contiene non è quindi +// esprimibile e viene rifiutato, invece di essere citato male. function comandoPiattaforma(eseguibile, argomenti = [], opzioni = {}) { if (PIATTAFORMA !== 'win32' || !/\.(bat|cmd)$/i.test(eseguibile)) { return { comando: eseguibile, argomenti, opzioni }; } - const riga = [eseguibile, ...argomenti] - .map((a) => `"${String(a).replace(/"/g, '""')}"`) - .join(' '); + const pezzi = [eseguibile, ...argomenti].map(String); + const insidioso = pezzi.find((a) => /["\r\n]/.test(a)); + if (insidioso !== undefined) { + throw new Error(`argomento non ammesso per un comando Windows: ${insidioso}`); + } + const riga = pezzi.map((a) => `"${a}"`).join(' '); return { comando: process.env.COMSPEC || 'cmd.exe', argomenti: ['/d', '/s', '/c', `"${riga}"`], @@ -459,7 +468,9 @@ export async function pacchettiInstallati(tex) { // Cerca un pacchetto fra quelli disponibili. export async function cercaPacchetto(tex, termine) { - const pulito = String(termine || '').trim(); + // Il termine arriva da un campo di testo e finisce in una riga di comando: + // teniamo solo i caratteri che compaiono davvero nei nomi dei pacchetti. + const pulito = String(termine || '').trim().replace(/[^A-Za-z0-9._+\- ]/g, ''); if (!pulito) return []; // --global cerca nel database remoto, non solo fra gli installati. const out = await tlmgrOutput(tex, ['search', '--global', '--file', `${pulito}`]).catch( diff --git a/docs/index.html b/docs/index.html index d450414..261308e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,6 +29,7 @@