From e94264e3cf3904c515d14487f343b98da9173c6d Mon Sep 17 00:00:00 2001 From: gabriele-cusato Date: Mon, 23 Mar 2026 22:21:57 +0100 Subject: [PATCH] migliorato ocr, aggiungendo anche il supporto alle --- CLAUDE.md | 14 +- HandTranscriptMd/src/md-parser.ts | 460 +++++++++++++++++++++++++--- HandTranscriptMd/src/parser.test.ts | 220 +++++++++++++ HandTranscriptMd/src/settings.ts | 66 ++++ HandTranscriptMd/styles.css | 37 +++ NOTES.md | 10 + 6 files changed, 755 insertions(+), 52 deletions(-) create mode 100644 HandTranscriptMd/src/parser.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index cdec43a..ccae81e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -215,12 +215,16 @@ cd C:\Projects\pluginObsidian\handWrittenMarkdownConverter\HandTranscriptMd; nod > **Storico sessioni, bug risolti e funzionalità completate** → vedi [`NOTES.md`](./NOTES.md) > I task completati vanno spostati in `NOTES.md`; in questa sezione restano solo i task ancora da fare. +## Regole di sviluppo + +> **Keyword parser**: ogni volta che si aggiunge o rimuove una keyword accettata in `src/md-parser.ts`, va aggiornata **anche** la tabella `KEYWORDS` in `src/settings.ts` (sezione "Keyword riconosciute dal parser OCR"). Le due liste devono essere sempre sincronizzate. + ## Prossimi passi ### Task aperti -- **Migliorare il riconoscimento OCR da Gemini** — `src/recognizer.ts` + `src/md-parser.ts`: - - Migliorare la qualità complessiva del riconoscimento, non solo i simboli markdown - - Affinare il prompt con few-shot examples per simboli (`#`, `-`, `>`, `**`, `==`, ecc.) - - Valutare se `md-parser.ts` può coprire i casi che il prompt non gestisce - - Valutare modelli Gemini alternativi o parametri diversi (temperatura, ecc.) \ No newline at end of file +- **Keyword personalizzate nelle impostazioni** — `src/settings.ts` + `src/md-parser.ts`: + - Permettere all'utente di aggiungere keyword custom nella pagina impostazioni + - Ogni keyword custom ha: nome (es. `FIRMA`), output markdown (es. `— Mario Rossi`) + - Le keyword custom vengono caricate in `expandKeywords` insieme a quelle predefinite + diff --git a/HandTranscriptMd/src/md-parser.ts b/HandTranscriptMd/src/md-parser.ts index 0d5c1b4..f62b465 100644 --- a/HandTranscriptMd/src/md-parser.ts +++ b/HandTranscriptMd/src/md-parser.ts @@ -1,55 +1,421 @@ /* ============================================= - md-parser — Parser prefissi Markdown - Converte testo grezzo (output OCR) in markdown - strutturato, analizzando riga per riga. + md-parser — Parsing OCR → Markdown Obsidian + + Pipeline in due step: + 1. normalizeMarkdownSymbols — corregge simboli approssimativi + 2. expandKeywords — espande _KEYWORD: in strutture markdown ============================================= */ -// Converte tutto il testo grezzo in markdown formattato +// ============================================================================= +// STEP 1 — normalizeMarkdownSymbols +// ============================================================================= + +/** + * Corregge simboli markdown scritti a mano in modo approssimativo (output OCR). + * Opera riga per riga, saltando completamente il contenuto dei blocchi codice. + */ +export function normalizeMarkdownSymbols(rawText: string): string { + // Rimuove BOM e caratteri zero-width Unicode che Gemini inserisce all'inizio + // del testo o all'inizio di righe (U+FEFF, U+200B, U+200C, U+200D, U+2060) + const cleaned = rawText.replace(/[\uFEFF\u200B\u200C\u200D\u2060]/g, ''); + + const lines = cleaned.split('\n'); + const out: string[] = []; + let inCodeBlock = false; + + for (const line of lines) { + // Delimitatore blocco codice (``` opzionalmente seguito da linguaggio) + if (line.trim().startsWith('```')) { + inCodeBlock = !inCodeBlock; + out.push(line); + continue; + } + // Dentro un blocco codice: non toccare nulla + if (inCodeBlock) { + out.push(line); + continue; + } + out.push(normalizeLine(line)); + } + + let result = out.join('\n'); + + // Parola ripetuta consecutivamente: "ciao ciao" → "ciao" + // \b(\w+)\s+\1\b cattura la stessa parola due volte separate da spazio + result = result.replace(/\b(\w+)\s+\1\b/gi, '$1'); + + // Massimo una riga vuota tra paragrafi + result = result.replace(/\n{3,}/g, '\n\n'); + + // Converti righe con separatori pipe (|) in tabelle markdown + result = convertPipeTables(result); + + return result; +} + +/** + * Applica le correzioni su una singola riga (non dentro blocchi codice). + */ +function normalizeLine(line: string): string { + // Righe keyword — non modificare, le gestisce expandKeywords + if (/^<[A-Za-z0-9_]+/i.test(line.trim())) return line; + + // Rimuove spazi finali (non iniziali: servono per le liste annidate) + line = line.trimEnd(); + + // Separatori alternativi: ===, ___, *** o già --- → normalizza a --- + // Controlla PRIMA degli heading perché ___ potrebbe confondersi + if (/^(={3,}|_{3,}|\*{3,}|-{3,})$/.test(line.trim())) return '---'; + + // Heading senza spazio: #Ciao → # Ciao, ##titolo → ## Titolo + // Cattura #{1,6} + qualsiasi carattere non-spazio/non-# come primo carattere + line = line.replace(/^(#{1,6})([^\s#].*)$/, (_, hashes: string, content: string) => { + const text = content.trim(); + // Prima lettera maiuscola per H1/H2/H3 (regola di stile) + const cap = hashes.length <= 3 + ? text.charAt(0).toUpperCase() + text.slice(1) + : text; + return `${hashes} ${cap}`; + }); + + // Bullet unicode → lista markdown: •, ·, ∙, ◦, ‣, ⁃, ➤, ●, ○, ▶, ►, ▸ ecc. + line = line.replace(/^[•·∙◦‣⁃➤➢▶►▸▪▫●○]\s*(.+)$/, '- $1'); + + // Lista non ordinata: -elemento (senza spazio) → - elemento + // [^\s\-] assicura che non sia già "- " né "--" + line = line.replace(/^-([^\s\-].+)$/, '- $1'); + + // Lista numerata — tre varianti: + // 1)elemento o 1.elemento (con o senza spazio) → 1. elemento + line = line.replace(/^(\d+)[.)]\s*(\S.*)$/, '$1. $2'); + // 1elemento (numero attaccato a lettera) → 1. elemento + line = line.replace(/^(\d+)([A-Za-zÀ-ÖØ-öø-ÿ].*)$/, '$1. $2'); + + // Checkbox: varianti scritte a mano + // [x], [X], [v], [V], [✓] → - [x] testo + line = line.replace(/^\[\s*[xXvV✓]\s*\]\s*(.*)$/, '- [x] $1'); + // [], [ ] → - [ ] testo + line = line.replace(/^\[\s*\]\s*(.*)$/, '- [ ] $1'); + + // Blockquote senza spazio: >testo → > testo + line = line.replace(/^>([^\s].*)$/, '> $1'); + + // Spazi multipli → spazio singolo, preservando l'indentazione iniziale + const leading = line.match(/^(\s*)/)?.[1] ?? ''; + const body = line.slice(leading.length).replace(/ +/g, ' '); + line = leading + body; + + return line; +} + +/** + * Converte gruppi di 2+ righe consecutive separate da pipe (|) in tabelle markdown. + * Evita righe già formattate come tabella (con riga separatore |---|). + */ +function convertPipeTables(text: string): string { + const lines = text.split('\n'); + const out: string[] = []; + let i = 0; + let inCodeBlock = false; + + while (i < lines.length) { + const line = lines[i]; + + // Rispetta i blocchi codice anche in questa fase + if (line.trim().startsWith('```')) { + inCodeBlock = !inCodeBlock; + out.push(line); i++; continue; + } + if (inCodeBlock) { out.push(line); i++; continue; } + + // Una riga con | potrebbe essere l'inizio di una tabella grezza + const hasPipe = line.includes('|'); + // Salta intestazioni, blockquote e righe separatore già in formato markdown + const isSep = /^\|?\s*:?-+:?\s*\|/.test(line); + const isSpecial = line.trim().startsWith('#') || line.trim().startsWith('>'); + + if (hasPipe && !isSep && !isSpecial) { + // Raccoglie righe consecutive con pipe, saltando eventuali separatori già presenti + const block: string[] = [line]; + let j = i + 1; + while (j < lines.length && lines[j].includes('|') && lines[j].trim() !== '') { + if (!/^\|?\s*:?-+:?\s*\|/.test(lines[j])) block.push(lines[j]); + j++; + } + + // Converte solo se ci sono almeno 2 righe (intestazione + almeno 1 dati) + if (block.length >= 2) { + // Divide le celle: rimuove celle vuote iniziali/finali create da | ai bordi + const rows = block.map(r => { + const parts = r.split('|').map(c => c.trim()); + const start = parts[0] === '' ? 1 : 0; + const end = parts[parts.length - 1] === '' ? parts.length - 1 : parts.length; + return parts.slice(start, end); + }); + const cols = Math.max(...rows.map(r => r.length)); + const headerRow = '| ' + rows[0].join(' | ') + ' |'; + // Riga separatore compatta: |---|---| + const sepRow = '|' + rows[0].map(() => '---|').join(''); + const dataRows = rows.slice(1).map(row => { + const cells = Array.from({ length: cols }, (_, k) => row[k] ?? ''); + return '| ' + cells.join(' | ') + ' |'; + }); + out.push(headerRow, sepRow, ...dataRows); + i = j; + continue; + } + } + + out.push(line); + i++; + } + + return out.join('\n'); +} + +// ============================================================================= +// STEP 2 — expandKeywords +// ============================================================================= + +/** + * Espande le keyword _KEYWORD: in strutture markdown complete. + * Tutte le keyword sono case-insensitive. Lo spazio dopo ':' è opzionale. + * + * @param text - testo già normalizzato + * @param fnStart - numero di partenza per le footnote _FN: (default 1) + */ +export function expandKeywords(text: string, fnStart = 1): string { + const lines = text.split('\n'); + const out: string[] = []; + let fn = fnStart; // contatore footnote corrente + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + + // Pattern: contenuto oppure contenuto (es. ) + // Il parametro extra (linguaggio per CODEBLOCK) è dentro le <>: + // Il contenuto segue > con opzionale : e/o spazio (es.

CIAO,

:CIAO) + const kw = trimmed.match(/^<([A-Za-z0-9_]+)(?:\s+([^>]*?))?\s*>[\s:]*(.*)/i); + + if (!kw) { + out.push(line); + i++; + continue; + } + + const keyword = kw[1].toUpperCase(); // nome keyword normalizzato + const extra = (kw[2] ?? '').trim(); // parametro aggiuntivo (es. linguaggio CODEBLOCK) + const content = (kw[3] ?? '').trim(); // contenuto dopo ':' + + switch (keyword) { + + // --- Titoli --- + case 'H1': out.push(`# ${capitalize(content)}`); break; + case 'H2': out.push(`## ${capitalize(content)}`); break; + case 'H3': out.push(`### ${capitalize(content)}`); break; + case 'H4': out.push(`#### ${content}`); break; + + // --- Inline style --- + case 'B': + case 'BOLD': out.push(`**${content}**`); break; + case 'I': out.push(`*${content}*`); break; + case 'BI': out.push(`***${content}***`); break; + case 'S': + case 'STRIKE': out.push(`~~${content}~~`); break; + case 'HL': out.push(`==${content}==`); break; + case 'CODE': out.push(`\`${content}\``); break; + + // --- Liste --- + case 'LIST': out.push(buildBulletList(content)); break; + case 'NUMLIST': out.push(buildNumList(content)); break; + case 'CHECK': out.push(buildChecklist(content)); break; + + // --- Callout Obsidian --- + case 'NOTE': out.push(buildCallout('NOTE', content)); break; + case 'WARN': out.push(buildCallout('WARNING', content)); break; + case 'TIP': out.push(buildCallout('TIP', content)); break; + case 'INFO': out.push(buildCallout('INFO', content)); break; + case 'ERROR': out.push(buildCallout('ERROR', content)); break; + case 'IMPORTANT': out.push(buildCallout('IMPORTANT', content)); break; + case 'QUOTE': out.push(`> ${content}`); break; + + // --- Link e immagini --- + case 'LINK': { + const [label, url] = splitTwo(content); + out.push(`[${label}](${url})`); + break; + } + case 'IMG': { + const [alt, url] = splitTwo(content); + out.push(`![${alt}](${url})`); + break; + } + + // --- Separatori --- + case 'HR': + case 'SEP': out.push('---'); break; + + // --- Footnote auto-numerata --- + case 'FN': out.push(`[^${fn++}]: ${content}`); break; + + // --- Math --- + case 'MATH': out.push(`$${content}$`); break; + + // --- Tag Obsidian (sostituisce spazi con underscore) --- + case 'TAG': out.push(`#${content.replace(/\s+/g, '_')}`); break; + + // --- Data / Ora (usa ora di sistema) --- + case 'DATE': { + const d = new Date(); + out.push(d.toISOString().slice(0, 10)); + break; + } + case 'TIME': { + const d = new Date(); + out.push(d.toTimeString().slice(0, 5)); + break; + } + case 'DATETIME': { + const d = new Date(); + out.push(`${d.toISOString().slice(0, 10)} ${d.toTimeString().slice(0, 5)}`); + break; + } + + // --- Indent (aggiunge 2 spazi) --- + case 'INDENT': out.push(` ${content}`); break; + + // --- CODEBLOCK multi-riga (termina con riga vuota) --- + // Sintassi: _CODEBLOCK js: (linguaggio nel parametro opzionale prima di ':') + case 'CODEBLOCK': { + const lang = extra; // es. "js", "python", "" + i++; + const codeLines: string[] = []; + // Raccoglie righe fino alla prima riga vuota o fine testo + while (i < lines.length && lines[i].trim() !== '') { + codeLines.push(lines[i]); + i++; + } + out.push(`\`\`\`${lang}\n${codeLines.join('\n')}\n\`\`\``); + continue; // i già avanzato, salta l'i++ finale + } + + // --- MATHBLOCK multi-riga (termina con riga vuota) --- + case 'MATHBLOCK': { + i++; + const mathLines: string[] = []; + while (i < lines.length && lines[i].trim() !== '') { + mathLines.push(lines[i]); + i++; + } + out.push(`$$\n${mathLines.join('\n')}\n$$`); + continue; + } + + // --- TABLE multi-riga --- + // Header: Col1, Col2, Col3 + // Righe: val1, val2, val3 (una per riga, virgola come separatore) + // Fine:
(tag di chiusura) oppure riga vuota / senza virgola + case 'TABLE': { + const headers = content.split(',').map(h => h.trim()); + const rows: string[][] = []; + i++; + while (i < lines.length) { + const rowLine = lines[i].trim(); + // Tag di chiusura: "
" senza contenuto + if (/^
\s*$/i.test(rowLine)) { i++; break; } + // Fine implicita: riga vuota o riga senza virgola + if (!rowLine || !rowLine.includes(',')) break; + rows.push(rowLine.split(',').map(c => c.trim())); + i++; + } + out.push(buildTable(headers, rows)); + continue; + } + + default: + // Keyword non riconosciuta: lascia la riga invariata + out.push(line); + } + + i++; + } + + return out.join('\n'); +} + +// ============================================================================= +// PIPELINE COMPLETA +// ============================================================================= + +/** + * Pipeline completa: normalizzazione simboli → espansione keyword. + * Questa è la funzione principale da usare per l'output OCR grezzo. + */ +export function parseHandwritingToMarkdown(rawOcrText: string): string { + return expandKeywords(normalizeMarkdownSymbols(rawOcrText)); +} + +/** + * Alias di compatibilità con il codice pre-esistente che chiamava parseMarkdown(). + */ export function parseMarkdown(raw: string): string { - const lines = raw.split('\n'); - return lines.map(parseLine).join('\n'); + return parseHandwritingToMarkdown(raw); } -// Processa una singola riga, rilevando il prefisso e applicando la sintassi markdown -function parseLine(line: string): string { - const t = line.trim(); - if (!t) return ''; +// ============================================================================= +// HELPER PRIVATI +// ============================================================================= - // Separatore orizzontale: "---" o più trattini - if (/^-{3,}$/.test(t)) return '---'; - - // Intestazioni H1 / H2 / H3 (es. "# Titolo", "## Sezione") - if (/^#{1,3}\s+/.test(t)) return t; - - // Checkbox spuntata: "- [x] testo" (case insensitive) - if (/^-\s*\[x\]\s*/i.test(t)) { - const content = t.replace(/^-\s*\[x\]\s*/i, ''); - return `- [x] ${content}`; - } - - // Checkbox vuota: "- [ ] testo" - if (/^-\s*\[\s*\]\s*/.test(t)) { - const content = t.replace(/^-\s*\[\s*\]\s*/, ''); - return `- [ ] ${content}`; - } - - // Lista non ordinata: "- testo" o "* testo" - if (/^[-*]\s+/.test(t)) { - const content = t.replace(/^[-*]\s+/, ''); - return `- ${content}`; - } - - // Lista ordinata: "1. testo", "2. testo", ecc. - if (/^\d+\.\s+/.test(t)) return t; - - // Blockquote: "> testo" - if (/^>\s*/.test(t)) return t; - - // Blocco codice: riga che inizia con ``` (anche con linguaggio, es. ```js) - if (t.startsWith('```')) return t; - - // Testo con inline code, grassetto, corsivo, highlight, barrato: - // questi sono già nella forma giusta (es. **testo**), li passiamo invariati. - // L'OCR dovrebbe produrli come scritti a mano con i simboli. - return t; +/** Prima lettera maiuscola, resto invariato */ +function capitalize(s: string): string { + if (!s) return s; + return s.charAt(0).toUpperCase() + s.slice(1); +} + +/** + * Divide "testo, resto" in ["testo", "resto"] usando la PRIMA virgola. + * Usato da _LINK: e _IMG: per separare label/alt da url. + */ +function splitTwo(content: string): [string, string] { + const idx = content.indexOf(','); + if (idx === -1) return [content.trim(), '']; + return [content.slice(0, idx).trim(), content.slice(idx + 1).trim()]; +} + +/** Costruisce lista puntata da "a, b, c" → "- a\n- b\n- c" */ +function buildBulletList(content: string): string { + return content.split(',').map(item => `- ${item.trim()}`).join('\n'); +} + +/** Costruisce lista numerata da "a, b, c" → "1. a\n2. b\n3. c" */ +function buildNumList(content: string): string { + return content.split(',').map((item, i) => `${i + 1}. ${item.trim()}`).join('\n'); +} + +/** Costruisce checklist da "a, b, c" → "- [ ] a\n- [ ] b\n- [ ] c" */ +function buildChecklist(content: string): string { + return content.split(',').map(item => `- [ ] ${item.trim()}`).join('\n'); +} + +/** Costruisce un callout Obsidian: > [!TYPE]\n> contenuto */ +function buildCallout(type: string, content: string): string { + return `> [!${type}]\n> ${content}`; +} + +/** + * Costruisce una tabella markdown con intestazioni e righe opzionali. + * Il separatore usa il formato compatto |---|---| (senza spazi interni). + */ +function buildTable(headers: string[], rows: string[][]): string { + const headerRow = '| ' + headers.join(' | ') + ' |'; + // Separatore compatto: |---|---|---| (necessario per toContain nei test) + const sepRow = '|' + headers.map(() => '---|').join(''); + // Righe dati: padda al numero di colonne dell'intestazione + const dataRows = rows.map(row => { + const cells = Array.from({ length: headers.length }, (_, k) => row[k] ?? ''); + return '| ' + cells.join(' | ') + ' |'; + }); + return [headerRow, sepRow, ...dataRows].join('\n'); } diff --git a/HandTranscriptMd/src/parser.test.ts b/HandTranscriptMd/src/parser.test.ts new file mode 100644 index 0000000..b9ea15e --- /dev/null +++ b/HandTranscriptMd/src/parser.test.ts @@ -0,0 +1,220 @@ +/* ============================================= + parser.test.ts — Test autonomo per md-parser + + Eseguibile senza framework: + npx tsx src/parser.test.ts + + Non richiede dipendenze esterne: usa solo + un helper expect/toBe inline. + ============================================= */ + +import { normalizeMarkdownSymbols as normalize, expandKeywords as expand, parseHandwritingToMarkdown as parse } from './md-parser.js'; + +// --- Mini test runner --- + +let passed = 0; +let failed = 0; + +function expect(actual: string) { + return { + toBe(expected: string) { + if (actual === expected) { + passed++; + } else { + failed++; + console.error(`❌ FAIL (toBe)\n actual: ${JSON.stringify(actual)}\n expected: ${JSON.stringify(expected)}`); + } + }, + toContain(substr: string) { + if (actual.includes(substr)) { + passed++; + } else { + failed++; + console.error(`❌ FAIL (toContain)\n actual: ${JSON.stringify(actual)}\n expected to contain: ${JSON.stringify(substr)}`); + } + }, + }; +} + +function describe(name: string, fn: () => void) { + console.log(`\n📋 ${name}`); + fn(); +} + +// ============================================================================= +// normalizeMarkdownSymbols +// ============================================================================= + +describe('normalizeMarkdownSymbols — heading', () => { + expect(normalize('#Ciao')).toBe('# Ciao'); + expect(normalize('##titolo')).toBe('## Titolo'); + expect(normalize('###sub')).toBe('### Sub'); + expect(normalize('# Già corretto')).toBe('# Già corretto'); + expect(normalize('####senza cap')).toBe('#### senza cap'); // H4 non capitalizza +}); + +describe('normalizeMarkdownSymbols — liste', () => { + expect(normalize('-elemento')).toBe('- elemento'); + expect(normalize('- già ok')).toBe('- già ok'); + expect(normalize('• item')).toBe('- item'); + expect(normalize('· item')).toBe('- item'); + expect(normalize('● item')).toBe('- item'); +}); + +describe('normalizeMarkdownSymbols — liste numerate', () => { + expect(normalize('1.elemento')).toBe('1. elemento'); + expect(normalize('1. elemento')).toBe('1. elemento'); // già corretto + expect(normalize('2)elemento')).toBe('2. elemento'); + expect(normalize('3elemento')).toBe('3. elemento'); +}); + +describe('normalizeMarkdownSymbols — checkbox', () => { + expect(normalize('[x]testo')).toBe('- [x] testo'); + expect(normalize('[X]testo')).toBe('- [x] testo'); + expect(normalize('[v]testo')).toBe('- [x] testo'); + expect(normalize('[ ]testo')).toBe('- [ ] testo'); + expect(normalize('[]testo')).toBe('- [ ] testo'); +}); + +describe('normalizeMarkdownSymbols — blockquote', () => { + expect(normalize('>testo')).toBe('> testo'); + expect(normalize('> già ok')).toBe('> già ok'); +}); + +describe('normalizeMarkdownSymbols — separatori', () => { + expect(normalize('===')).toBe('---'); + expect(normalize('___')).toBe('---'); + expect(normalize('***')).toBe('---'); + expect(normalize('---')).toBe('---'); +}); + +describe('normalizeMarkdownSymbols — parole duplicate', () => { + expect(normalize('ciao ciao')).toBe('ciao'); + expect(normalize('il il cliente')).toBe('il cliente'); +}); + +describe('normalizeMarkdownSymbols — blocchi codice protetti', () => { + // Il contenuto dentro ``` non deve essere modificato + expect(normalize('```\n#non titolo\n```')).toBe('```\n#non titolo\n```'); + expect(normalize('```js\n-nolista\n```')).toBe('```js\n-nolista\n```'); +}); + +// ============================================================================= +// expandKeywords +// ============================================================================= + +describe('expandKeywords — titoli', () => { + expect(expand('

Titolo')).toBe('# Titolo'); + expect(expand('

Sezione')).toBe('## Sezione'); + expect(expand('

Sub')).toBe('### Sub'); + expect(expand('

Piccolo')).toBe('#### Piccolo'); +}); + +describe('expandKeywords — inline style', () => { + expect(expand(' parola')).toBe('**parola**'); + expect(expand(' corsivo')).toBe('*corsivo*'); + expect(expand(' entrambi')).toBe('***entrambi***'); + expect(expand(' barrato')).toBe('~~barrato~~'); + expect(expand(' evidenziato')).toBe('==evidenziato=='); + expect(expand(' var x')).toBe('`var x`'); +}); + +describe('expandKeywords — liste', () => { + expect(expand(' a, b, c')).toBe('- a\n- b\n- c'); + expect(expand(' a, b, c')).toBe('1. a\n2. b\n3. c'); + expect(expand(' task1, task2')).toBe('- [ ] task1\n- [ ] task2'); +}); + +describe('expandKeywords — tabella', () => { + expect(expand('

A, B, C')).toContain('| A | B | C |'); + expect(expand('
A, B, C')).toContain('|---|---|---|'); +}); + +describe('expandKeywords — callout', () => { + expect(expand(' testo')).toBe('> [!NOTE]\n> testo'); + expect(expand(' testo')).toBe('> [!WARNING]\n> testo'); + expect(expand(' testo')).toBe('> [!TIP]\n> testo'); + expect(expand(' testo')).toBe('> [!INFO]\n> testo'); + expect(expand(' testo')).toBe('> [!ERROR]\n> testo'); + expect(expand(' testo')).toBe('> [!IMPORTANT]\n> testo'); + expect(expand(' testo')).toBe('> testo'); +}); + +describe('expandKeywords — link e immagini', () => { + expect(expand(' Google, https://google.com')).toBe('[Google](https://google.com)'); + expect(expand(' logo, https://example.com/img.png')).toBe('![logo](https://example.com/img.png)'); +}); + +describe('expandKeywords — separatori', () => { + expect(expand('
')).toBe('---'); + expect(expand('')).toBe('---'); +}); + +describe('expandKeywords — footnote', () => { + expect(expand(' questa è una nota')).toBe('[^1]: questa è una nota'); + // Due footnote consecutive: numeri 1 e 2 + expect(expand(' prima\n seconda')).toBe('[^1]: prima\n[^2]: seconda'); + // Partenza custom + expect(expand(' nota', 5)).toBe('[^5]: nota'); +}); + +describe('expandKeywords — math', () => { + expect(expand(' E=mc^2')).toBe('$E=mc^2$'); +}); + +describe('expandKeywords — tag e date/ora', () => { + expect(expand(' progetto')).toBe('#progetto'); + expect(expand(' due parole')).toBe('#due_parole'); + // , testo')).toBe('~~testo~~'); // alias STRIKE = S + expect(expand('
')).toBe('---'); // lowercase + expect(expand('
A, B')).toContain('| A | B |'); // mixed case +}); + +describe('expandKeywords — colon opzionale dopo >', () => { + expect(expand('testo')).toBe('**testo**'); // nessun separatore + expect(expand('

Titolo')).toBe('# Titolo'); // nessun separatore + expect(expand(': testo')).toBe('**testo**'); // con colon +}); + +describe('expandKeywords — CODEBLOCK multi-riga', () => { + const input = '\nconsole.log(\'ciao\')\nconst x = 1\n'; + const out = expand(input); + expect(out).toContain('```js'); + expect(out).toContain('console.log(\'ciao\')'); + expect(out).toContain('```'); +}); + +describe('expandKeywords — indent', () => { + expect(expand(' testo')).toBe(' testo'); +}); + +// ============================================================================= +// Pipeline completa parseHandwritingToMarkdown +// ============================================================================= + +describe('pipeline completa', () => { + expect(parse('#Ciao\n a, b')).toBe('# Ciao\n- a\n- b'); + expect(parse('##sezione\n parola chiave')).toBe('## Sezione\n**parola chiave**'); + // Il blocco codice non viene toccato dalla normalizzazione + expect(parse('```\n#non toccare\n```')).toBe('```\n#non toccare\n```'); +}); + +// ============================================================================= +// Report finale +// ============================================================================= + +console.log(`\n${'─'.repeat(40)}`); +if (failed === 0) { + console.log(`✅ Tutti i ${passed} test passati.`); +} else { + console.log(`❌ ${failed} test falliti su ${passed + failed} totali.`); + process.exit(1); +} diff --git a/HandTranscriptMd/src/settings.ts b/HandTranscriptMd/src/settings.ts index bf186d9..5d163bb 100644 --- a/HandTranscriptMd/src/settings.ts +++ b/HandTranscriptMd/src/settings.ts @@ -207,5 +207,71 @@ export class HandwritingSettingTab extends PluginSettingTab { this.plugin.settings.debugMode = value; await this.plugin.saveSettings(); })); + + // --- Riferimento keyword OCR (sezione espandibile) --- + // NOTA SVILUPPATORI: se aggiungi una keyword in md-parser.ts, aggiornala anche qui! + const details = containerEl.createEl('details', { cls: 'hwm_keyword-ref' }); + details.createEl('summary', { + text: 'Keyword riconosciute dal parser OCR', + cls: 'hwm_keyword-summary', + }); + details.createEl('p', { + text: 'Scrivi queste keyword nel disegno per generare la struttura markdown corrispondente. Tutte sono case-insensitive (

=

). Il contenuto segue direttamente dopo >.', + cls: 'setting-item-description', + }); + + // Tabella keyword: [nome, sintassi, output] + const KEYWORDS: [string, string, string][] = [ + ['

', '

Titolo', '# Titolo'], + ['

', '

Titolo', '## Titolo'], + ['

', '

Titolo', '### Titolo'], + ['

', '

Titolo', '#### Titolo'], + [' / ', ' testo', '**testo**'], + ['', ' testo', '*testo*'], + ['', ' testo', '***testo***'], + [' / ', ' testo', '~~testo~~'], + ['', ' testo', '==testo=='], + ['', ' testo', '`testo`'], + ['', '', '```js\n...\n```'], + ['', ' a, b, c', '- a\n- b\n- c'], + ['', ' a, b, c', '1. a\n2. b\n3. c'], + ['', ' a, b, c', '- [ ] a\n- [ ] b\n- [ ] c'], + ['

', '
Col1, Col2', '| Col1 | Col2 |\n|---|---|\n| ... |'], + ['', ' testo', '> [!NOTE]\n> testo'], + ['', ' testo', '> [!WARNING]\n> testo'], + ['', ' testo', '> [!TIP]\n> testo'], + ['', ' testo', '> [!INFO]\n> testo'], + ['', ' testo', '> [!ERROR]\n> testo'], + ['', ' testo', '> [!IMPORTANT]\n> testo'], + ['', ' testo', '> testo'], + ['', ' testo, url', '[testo](url)'], + ['', ' alt, url', '![alt](url)'], + ['
/ ', '
', '---'], + ['', ' nota a piè pagina', '[^1]: nota a piè pagina'], + ['', ' formula', '$formula$'], + ['', '', '$$\n...\n$$'], + ['', ' parola', '#parola'], + ['', '', 'YYYY-MM-DD'], + ['