diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d45aebe..a769e97 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,7 +52,8 @@ jobs:
const fs = require('fs');
const html = fs.readFileSync('docs/index.html', 'utf-8');
const host = [...html.matchAll(/https?:\/\/([a-zA-Z0-9.-]+)/g)].map(m => m[1]);
- const ammessi = new Set(['github.com', 'www.w3.org']);
+ // overleaf.com: la sezione che spiega come compilare il PDF lΓ .
+ const ammessi = new Set(['github.com', 'www.w3.org', 'www.overleaf.com']);
const estranei = [...new Set(host)].filter(h => !ammessi.has(h));
if (estranei.length) { console.error('domini inattesi:', estranei); process.exit(1); }
console.log('Nessun dominio esterno inatteso');
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 ac6cc5f..aa7ace0 100644
--- a/desktop/latex.js
+++ b/desktop/latex.js
@@ -99,8 +99,43 @@ 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\...).
+//
+// 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 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}"`],
+ opzioni: { ...opzioni, windowsVerbatimArguments: true },
+ };
}
// ---------------------------------------------------------------------------
@@ -122,7 +157,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 +170,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 +180,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 +436,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;
}
@@ -432,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(
@@ -491,19 +529,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 = '