Files
TieMeasureFlow/src/frontend/flask_app/static/js/rich-text.js
T
Adriano Dal Pastro 5aa3d595ad feat(recipes): la ricetta decide cosa pretendere e come si misura
Tre cose che prima non erano di nessuno diventano regole della ricetta, decise da
chi la scrive e fatte valere dal server.

Punto 8 — tracciabilita' obbligatoria. Lotto e seriale si dichiarano obbligatori
sulla ricetta. L'operatore li inserisce alla selezione, dove il pulsante non si
attiva finche' mancano, e la stessa regola vale sulla lista task, raggiungibile
anche per link diretto, e sul barcode: uno sbarramento che dura solo finche'
qualcuno prende in mano il lettore non e' uno sbarramento. La produzione non si
apre e la misura non si salva senza cio' che la ricetta pretende, perche' un
valore senza il suo lotto non e' riconducibile a niente, e accorgersene dopo
significa accorgersene tardi.

Punto 9 — inserimento manuale. Una ricetta puo' vietare i valori digitati, ed e'
il valore predefinito: il calibro e' lo strumento, digitare e' cio' che va
concesso. Dove e' vietato il tastierino non viene disegnato (non nascosto con i
CSS: il markup nascosto e' markup che si puo' rimostrare) e restano correzione e
conferma, perche' una lettura sbagliata va cancellata. Il controllo vero e' sul
server: una regola che vive solo nel frontend e' un consiglio.

Migliorato al passaggio il riconoscimento del calibro. Contava solo la raffica di
cifre, cosi' una lettura corta come "9.5" — tre battute — finiva registrata come
digitata a mano; ora conta anche l'Invio che il wedge manda dentro la stessa
raffica. Senza questa correzione il divieto avrebbe respinto misure legittime.

Punto 11 — formattazione delle descrizioni. A capo e grassetto sopravvivono: chi
scrive le ricette incolla dal PDF della scheda tecnica e il testo arrivava
appiattito, da risistemare a mano ogni volta. Nessun HTML viene accettato o
salvato — il testo viene escapato e gli unici tag nel risultato sono quelli
prodotti dal renderer. La sanificazione e' questa: non c'e' niente da sanificare
perche' non si accetta niente. Le stesse due regole in Jinja e in JS, cosi' una
descrizione si legge uguale ovunque. E la descrizione ora si vede anche in
esecuzione: era scritta per l'operatore e la vedeva solo chi la scriveva.

Migrazione 009: tre colonne sulla ricetta. Le due di tracciabilita' partono
false, che e' il comportamento di oggi; l'inserimento manuale parte *vero* sulle
ricette gia' esistenti — il default della colonna e' falso, quindi le ricette
nuove sono solo-calibro, ma spegnerlo d'ufficio su quelle in uso fermerebbe una
linea alla misura successiva. Chi possiede la ricetta lo decide dall'editor.

Test: +26 (291). Coprono il rifiuto sul server per lotto, seriale e valore
digitato, il calibro sempre ammesso, le regole che sopravvivono alla nuova
versione, il tastierino assente in pagina, l'Avvia sbarrato, e il renderer delle
descrizioni compreso il caso in cui si prova a farci passare un tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:27:19 +00:00

55 lines
2.0 KiB
JavaScript

/**
* Task descriptions: line breaks and bold, and nothing else.
*
* Whoever writes a recipe pastes from the PDF of the technical sheet, and the text
* used to arrive flattened - every line run into the next, every emphasis lost, to
* be put back by hand each time. Two conventions carry it now: a newline is a line
* break, **like this** is bold.
*
* No HTML is stored or trusted. The text is escaped first and the only tags in the
* result are the ones produced here, so a description that contains <script> shows
* those characters and does nothing. The server-side filter (rich_text in app.py)
* follows exactly these rules, so a description reads the same whether the page
* was rendered by Flask or by Alpine.
*/
(function (global) {
'use strict';
function escapeHtml(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/* Escaped text, then the two tags we add ourselves. Order matters: escaping
after the substitution would turn our own <strong> into visible text. */
function richText(value) {
if (!value) return '';
return escapeHtml(value)
.replace(/\*\*([\s\S]+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
}
/* Wrap whatever is selected in a textarea in ** **, the way a B button does.
Dispatching 'input' is what keeps x-model in step: assigning to value alone
changes the DOM and leaves Alpine with the old string. */
function wrapSelectionBold(el) {
if (!el) return;
var start = el.selectionStart;
var end = el.selectionEnd;
var text = el.value || '';
el.value = text.slice(0, start) + '**' + text.slice(start, end) + '**' + text.slice(end);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.focus();
// Leave the cursor around the same words, now inside the markers.
el.selectionStart = start + 2;
el.selectionEnd = end + 2;
}
global.richText = richText;
global.wrapSelectionBold = wrapSelectionBold;
})(window);