Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down
73 changes: 56 additions & 17 deletions desktop/latex.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
};
}

// ---------------------------------------------------------------------------
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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'),
};
}
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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(
Expand Down Expand Up @@ -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`);
}
5 changes: 5 additions & 0 deletions desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
6 changes: 5 additions & 1 deletion desktop/renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,11 @@ async function caricaInstallati() {
}
if (!stato.tex.tlmgr) {
$('avvisoPkg').hidden = false;
$('avvisoPkg').innerHTML = '<strong>Questa distribuzione non è gestibile da texforge.</strong> Stai usando il LaTeX di sistema: installa i pacchetti con il gestore della tua distribuzione, oppure lascia che texforge installi la propria copia da <em>Motore LaTeX</em>.';
// 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'
? '<strong>Questa distribuzione non è gestibile da texforge.</strong> La copia installata da texforge non contiene <code>tlmgr</code>: prova <em>Reinstalla la distribuzione</em> da <em>Motore LaTeX</em>.'
: '<strong>Questa distribuzione non è gestibile da texforge.</strong> Stai usando il LaTeX di sistema: installa i pacchetti con il gestore della tua distribuzione, oppure lascia che texforge installi la propria copia da <em>Motore LaTeX</em>.';
elenco.innerHTML = '<li class="vuoto">—</li>';
return;
}
Expand Down
64 changes: 60 additions & 4 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<ul class="bar-links" id="navLinks">
<li><a href="#app">L'applicazione</a></li>
<li><a href="#motore">Motore LaTeX</a></li>
<li><a href="#overleaf">Overleaf</a></li>
<li><a href="#comandi">Comandi</a></li>
<li><a href="#domande">Domande</a></li>
<li><a href="#esempi">Esempi</a></li>
Expand Down Expand Up @@ -283,8 +284,63 @@ <h3 class="mini-head">La pagina Pacchetti</h3>
</div>
</section>

<!-- ──────────────────────── OVERLEAF ──────────────────────── -->
<section class="sec" id="overleaf">
<div class="wrap">
<div class="sec-head">
<p class="rail">Senza installare niente</p>
<h2>Il PDF si compila anche su Overleaf</h2>
<p class="sub">
Il sorgente è LaTeX vero, quindi per il <strong>PDF</strong> texforge non è
obbligatorio: lo stesso <code>.tex</code> compila su
<a href="https://www.overleaf.com" target="_blank" rel="noopener">Overleaf</a>,
dal browser. Utile sul computer della scuola, su tablet, o per lavorare in due
sullo stesso documento.
</p>
</div>

<ol class="steps">
<li>
<h3>Crea il progetto</h3>
<p>
Su Overleaf, <em>New Project → Blank Project</em>. Se hai già uno ZIP di
sorgenti, <em>Upload Project</em>.
</p>
</li>
<li>
<h3>Carica due file</h3>
<p>
Il tuo <code>.tex</code> e <code>quizstruct.sty</code>, che trovi fra gli
allegati di ogni release. Il <code>.sty</code> va nella <strong>stessa
cartella</strong> del <code>.tex</code>: Overleaf lo trova da solo, non c'è
niente da installare. Usa solo pacchetti standard — <code>amsmath</code>,
<code>amssymb</code>, <code>xcolor</code>, <code>enumitem</code>,
<code>pgffor</code> — già presenti lì.
</p>
</li>
<li>
<h3>Compila con pdfLaTeX</h3>
<p>
<em>Menu → Compiler → pdfLaTeX</em>, poi <em>Recompile</em>. I file
<code>quiz.cfg</code>, <code>quiz.js</code> e <code>quiz.css</code> non
servono: riguardano solo l'HTML.
</p>
</li>
</ol>

<p class="small">
L'<strong>HTML interattivo</strong> — risposte cliccabili e punteggio che si
calcola da solo — è la parte in più, e si ottiene <strong>solo con
l'app</strong>: richiede <code>htlatex</code> e i file di configurazione, che
Overleaf non mette in conto. In pratica: su Overleaf ottieni il compito da
stampare, con texforge ottieni <em>anche</em> la versione che si corregge da
sola — stesso sorgente, nessuna modifica.
</p>
</div>
</section>

<!-- ──────────────────────── COMANDI ──────────────────────── -->
<section class="sec" id="comandi">
<section class="sec sec-alt" id="comandi">
<div class="wrap">
<div class="sec-head">
<p class="rail">quizstruct.sty</p>
Expand Down Expand Up @@ -429,7 +485,7 @@ <h2>I comandi del pacchetto</h2>
</section>

<!-- ──────────────────────── TIPI DI DOMANDA ──────────────────────── -->
<section class="sec sec-alt" id="domande">
<section class="sec" id="domande">
<div class="wrap">
<div class="sec-head">
<p class="rail">Riepilogo</p>
Expand Down Expand Up @@ -485,7 +541,7 @@ <h2>I tre modi di rispondere</h2>
</section>

<!-- ──────────────────────── ESEMPI ──────────────────────── -->
<section class="sec" id="esempi">
<section class="sec sec-alt" id="esempi">
<div class="wrap">
<div class="sec-head">
<p class="rail">Esempi</p>
Expand Down Expand Up @@ -587,7 +643,7 @@ <h2>Sorgenti da cui partire</h2>
</section>

<!-- ──────────────────────── SCARICA ──────────────────────── -->
<section class="sec sec-alt" id="scarica">
<section class="sec" id="scarica">
<div class="wrap">
<div class="sec-head">
<p class="rail">Installazione</p>
Expand Down
Loading