diff --git a/docs/sqlrunner-test.mdx b/docs/sqlrunner-test.mdx new file mode 100644 index 0000000..6ff3828 --- /dev/null +++ b/docs/sqlrunner-test.mdx @@ -0,0 +1,183 @@ +--- +unlisted: true +title: SQLRunner — pagina di test +description: Sandbox di verifica del componente SQLRunner (SQL Live Blocks) +--- + +# SQLRunner — sandbox di test + +Pagina temporanea per validare gli SQL Live Blocks. Non è linkata dalla sidebar +(`unlisted: true`). Il dataset di prova è `static/sql-datasets/archivista/biblioteca.sql` +(autori, libri, prestiti). Il motore (sql.js, ~660 KB di WASM self-hosted) viene +scaricato **solo al primo Esegui**: questa pagina a riposo non deve caricare nulla. + +## 1. Fence `sql live` — SELECT base con dataset + +```sql live dataset=archivista/biblioteca +SELECT titolo, anno, genere +FROM libri +WHERE anno > 1950 +ORDER BY anno; +``` + +## 2. JOIN tra tabelle + +```sql live dataset=archivista/biblioteca title="Chi ha scritto cosa" +SELECT l.titolo, a.nome AS autore, a.paese +FROM libri AS l +JOIN autori AS a ON a.id = l.autore_id +ORDER BY a.nome; +``` + +## 3. Errore SQL (deve mostrare il messaggio in rosso) + +```sql live dataset=archivista/biblioteca +SELECT titolo FROM romanzi; -- la tabella si chiama "libri"! +``` + +## 4. Restore trasparente: DROP e poi riesegui + +Esegui il `DROP TABLE`, poi rimetti la `SELECT` originale (bottone ↺) e riesegui: +la tabella **c'è di nuovo**. In modalità stateless il database riparte dal seed +a ogni esecuzione, senza che lo studente debba fare nulla. + +```sql live dataset=archivista/biblioteca +DROP TABLE prestiti; +SELECT name FROM sqlite_master WHERE type = 'table'; +``` + +## 5. Readonly + +```sql live dataset=archivista/biblioteca readonly +SELECT COUNT(*) AS numero_libri FROM libri; +``` + +## 6. Stateful: lo stato sopravvive tra i run + +Con `stateful` il database **non** viene ripristinato a ogni esecuzione: esegui +l'INSERT due volte e i conteggi crescono. In toolbar compare il bottone con il +database per il reset esplicito; dopo il reset (o dopo un timeout) appare la +notice «database ripristinato». + +```sql live dataset=archivista/biblioteca stateful title="biblioteca (stateful)" +INSERT INTO prestiti (libro_id, studente, classe, data_prestito) +VALUES (2, 'Nuovo Studente', '3A', '2026-06-10'); + +SELECT COUNT(*) AS prestiti_totali FROM prestiti; +``` + +## 7. Troncamento risultati (`maxRows`) + +La CTE genera 1000 righe ma `maxRows=15`: il worker taglia il risultato prima +di spedirlo alla pagina e compare il banner di troncamento. + +```sql live maxRows=15 +WITH RECURSIVE numeri(n) AS ( + SELECT 1 + UNION ALL + SELECT n + 1 FROM numeri WHERE n < 1000 +) +SELECT n, n * n AS quadrato FROM numeri; +``` + +## 8. Query runaway → watchdog timeout + +Ricorsione senza condizione di stop: dopo 3 secondi il worker viene terminato, +la pagina resta reattiva e il run successivo funziona normalmente. + +```sql live dataset=archivista/biblioteca timeout=3000 title="bomba ricorsiva" +WITH RECURSIVE c(x) AS ( + SELECT 1 + UNION ALL + SELECT x + 1 FROM c +) +SELECT COUNT(*) FROM c; +``` + +## 9. Senza dataset: DB vuoto per lezioni su CREATE TABLE + +```sql live +CREATE TABLE pianeti ( + id INTEGER PRIMARY KEY, + nome TEXT NOT NULL, + lune INTEGER DEFAULT 0 +); + +INSERT INTO pianeti (nome, lune) VALUES ('Marte', 2), ('Giove', 95); + +SELECT * FROM pianeti; +``` + +## 10. Vincoli: foreign key attive (`PRAGMA foreign_keys = ON`) + +L'INSERT viola la foreign key (`autore_id = 999` non esiste): deve fallire con +un errore di vincolo, non inserire silenziosamente. + +```sql live dataset=archivista/biblioteca +INSERT INTO libri (titolo, autore_id, anno) +VALUES ('Libro fantasma', 999, 2026); +``` + +## 11. NULL e valori particolari + +I `NULL` (prestiti non ancora restituiti) devono comparire in corsivo tenue. + +```sql live dataset=archivista/biblioteca +SELECT studente, data_prestito, data_restituzione +FROM prestiti +ORDER BY data_prestito; +``` + +## 12. Più statement → più tabelle di risultati + +```sql live dataset=archivista/biblioteca +SELECT genere, COUNT(*) AS quanti FROM libri GROUP BY genere ORDER BY quanti DESC; +SELECT paese, COUNT(*) AS autori FROM autori GROUP BY paese; +``` + +## 13. `maxLines` piccolo per forzare lo scroll interno + +```sql live dataset=archivista/biblioteca maxLines=5 +SELECT + a.nome, + COUNT(l.id) AS libri_scritti, + MIN(l.anno) AS primo_libro, + MAX(l.anno) AS ultimo_libro +FROM autori AS a +LEFT JOIN libri AS l ON l.autore_id = a.id +GROUP BY a.id +HAVING libri_scritti > 1 +ORDER BY libri_scritti DESC; +``` + +## 14. `noExplain` — niente bacchetta magica + +```sql live dataset=archivista/biblioteca noExplain +SELECT nome FROM autori WHERE paese = 'Italia'; +``` + +## 15. Componente JSX diretto con prompt custom + +import SQLRunner from '@site/src/theme/SQLRunner'; + + + +## 16. `runOnMount` — esegue da solo al caricamento + + diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 1bbd852..05dbfe9 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -5,6 +5,8 @@ import type * as Preset from '@docusaurus/preset-classic'; // eslint-disable-next-line @typescript-eslint/no-require-imports const remarkPyRunner = require('./plugins/pyrunner/remark.js'); // eslint-disable-next-line @typescript-eslint/no-require-imports +const remarkSqlRunner = require('./plugins/sqlrunner/remark.js'); +// eslint-disable-next-line @typescript-eslint/no-require-imports const { protectCode, restoreCode } = require('./plugins/smartypants-guard.js'); // Keyword admonition custom (Notion-style callouts). Vedi @@ -79,7 +81,7 @@ export default async function createConfig(): Promise { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], + beforeDefaultRemarkPlugins: [remarkPyRunner, remarkSqlRunner], remarkPlugins: [protectCode, smartypants, restoreCode], admonitions, }, @@ -125,7 +127,7 @@ export default async function createConfig(): Promise { routeBasePath: 'programmatore', sidebarPath: './sidebars/programmatore.ts', editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], + beforeDefaultRemarkPlugins: [remarkPyRunner, remarkSqlRunner], remarkPlugins: [protectCode, smartypants, restoreCode], admonitions, }, @@ -138,7 +140,7 @@ export default async function createConfig(): Promise { routeBasePath: 'artefice', sidebarPath: './sidebars/artefice.ts', editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], + beforeDefaultRemarkPlugins: [remarkPyRunner, remarkSqlRunner], remarkPlugins: [protectCode, smartypants, restoreCode], admonitions, }, @@ -151,7 +153,7 @@ export default async function createConfig(): Promise { routeBasePath: 'archivista', sidebarPath: './sidebars/archivista.ts', editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], + beforeDefaultRemarkPlugins: [remarkPyRunner, remarkSqlRunner], remarkPlugins: [protectCode, smartypants, restoreCode], admonitions, }, @@ -164,7 +166,7 @@ export default async function createConfig(): Promise { routeBasePath: 'apprendista', sidebarPath: './sidebars/apprendista.ts', editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], + beforeDefaultRemarkPlugins: [remarkPyRunner, remarkSqlRunner], remarkPlugins: [protectCode, smartypants, restoreCode], admonitions, }, diff --git a/package-lock.json b/package-lock.json index 3af4c3f..981d82c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/language": "^6.12.3", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.43.0", @@ -26,15 +27,18 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "copy-text-to-clipboard": "^3.2.2", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", - "react-dom": "^18.0.0" + "react-dom": "^18.0.0", + "sql.js": "^1.14.1" }, "devDependencies": { "@docusaurus/eslint-plugin": "^3.10.1", "@docusaurus/module-type-aliases": "^3.10.1", "@docusaurus/tsconfig": "^3.10.1", "@docusaurus/types": "^3.10.1", + "@types/sql.js": "^1.4.11", "@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.1", @@ -2018,6 +2022,20 @@ "@lezer/python": "^1.1.4" } }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.12.3", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", @@ -5602,6 +5620,13 @@ "@types/ms": "*" } }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -5919,6 +5944,17 @@ "@types/node": "*" } }, + "node_modules/@types/sql.js": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/sql.js/-/sql.js-1.4.11.tgz", + "integrity": "sha512-QXIx38p2ZThJaK9vP5ZdqdlRe1FG9I8SmCZOS7FHfB/2qPAjZwkL7/vlfPg6N/oWHuuOaGg/P/IRwfP2W0kWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/emscripten": "*", + "@types/node": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -8214,9 +8250,9 @@ "license": "MIT" }, "node_modules/copy-text-to-clipboard": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz", - "integrity": "sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", "license": "MIT", "engines": { "node": ">=12" @@ -21032,6 +21068,12 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" + }, "node_modules/srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", diff --git a/package.json b/package.json index 633bf17..10d5b3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "python-doesnt-byte", - "version": "0.3.0", + "version": "0.4.0", "private": true, "scripts": { "docusaurus": "docusaurus", @@ -21,6 +21,7 @@ "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-sql": "^6.10.0", "@codemirror/language": "^6.12.3", "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.43.0", @@ -37,15 +38,18 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "copy-text-to-clipboard": "^3.2.2", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", - "react-dom": "^18.0.0" + "react-dom": "^18.0.0", + "sql.js": "^1.14.1" }, "devDependencies": { "@docusaurus/eslint-plugin": "^3.10.1", "@docusaurus/module-type-aliases": "^3.10.1", "@docusaurus/tsconfig": "^3.10.1", "@docusaurus/types": "^3.10.1", + "@types/sql.js": "^1.4.11", "@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.1", diff --git a/plugins/pyrunner/index.js b/plugins/pyrunner/index.js index efe7969..cdb53fe 100644 --- a/plugins/pyrunner/index.js +++ b/plugins/pyrunner/index.js @@ -24,7 +24,12 @@ const DEFAULT_OPTIONS = { function walkPyFiles(dir, baseDir) { const out = {}; - if (!fs.existsSync(dir)) return out; + if (!fs.existsSync(dir)) { + console.warn( + `[pyrunner] Directory esempi non trovata: ${dir} — nessun file .py caricato.`, + ); + return out; + } for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { diff --git a/plugins/pyrunner/remark.js b/plugins/pyrunner/remark.js index badc6af..db8e370 100644 --- a/plugins/pyrunner/remark.js +++ b/plugins/pyrunner/remark.js @@ -5,108 +5,15 @@ * ``` * in JSX .... * - * Convive con i code fence normali (```py senza `live` resta un normale code block). + * La logica condivisa con SQLRunner vive in plugins/remark-live-fence.js. */ -function isLiveMeta(meta) { - if (!meta) return false; - // meta è una stringa tipo "live" o "live readonly title=foo" ecc. - return /(^|\s)live(\s|=|$)/.test(meta); -} +const { makeLiveFenceRemark } = require('../remark-live-fence.js'); -function parseMeta(meta) { - // Estrae chiave/valore. Supporta "live" (booleano implicito) e "key=value". - const out = {}; - if (!meta) return out; - const tokens = meta.match(/(?:[^\s"]+|"[^"]*")+/g) || []; - for (const tok of tokens) { - const eq = tok.indexOf('='); - if (eq === -1) { - if (tok === 'live') continue; // marker, niente prop - out[tok] = true; - } else { - const key = tok.slice(0, eq); - let value = tok.slice(eq + 1); - if (value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - if (key === 'live') continue; - out[key] = value; - } - } - return out; -} - -function toMdxAttributes(propsObj) { - const attrs = []; - for (const [name, value] of Object.entries(propsObj)) { - if (value === true) { - attrs.push({ type: 'mdxJsxAttribute', name, value: null }); - } else { - attrs.push({ - type: 'mdxJsxAttribute', - name, - value: String(value), - }); - } - } - return attrs; -} - -function makeCodeAttribute(code) { - // Passa il codice come prop `code={"..."}` con expression letterale. - // Più sicuro di un children text node: MDX non re-interpreta il contenuto. - const literal = JSON.stringify(code); - return { - type: 'mdxJsxAttribute', - name: 'code', - value: { - type: 'mdxJsxAttributeValueExpression', - value: literal, - data: { - estree: { - type: 'Program', - sourceType: 'module', - body: [ - { - type: 'ExpressionStatement', - expression: { - type: 'Literal', - value: code, - raw: literal, - }, - }, - ], - }, - }, - }, - }; -} - -function remarkPyRunner() { - return async (tree) => { - const { visit } = await import('unist-util-visit'); - visit(tree, 'code', (node, index, parent) => { - if (!parent || index == null) return; - if (node.lang !== 'py' && node.lang !== 'python') return; - if (!isLiveMeta(node.meta)) return; - - const props = parseMeta(node.meta); - const attributes = [ - ...toMdxAttributes(props), - makeCodeAttribute(node.value), - ]; - - const jsxNode = { - type: 'mdxJsxFlowElement', - name: 'PyRunner', - attributes, - children: [], - }; - - parent.children.splice(index, 1, jsxNode); - }); - }; -} +const remarkPyRunner = makeLiveFenceRemark({ + langs: ['py', 'python'], + componentName: 'PyRunner', + numericKeys: ['maxLines'], +}); module.exports = remarkPyRunner; diff --git a/plugins/remark-live-fence.js b/plugins/remark-live-fence.js new file mode 100644 index 0000000..e1de837 --- /dev/null +++ b/plugins/remark-live-fence.js @@ -0,0 +1,134 @@ +/** + * Factory condivisa per i remark plugin "live fence": trasforma i code fence + * ``` live [key=value ...] + * in un elemento JSX (, , ...) con il sorgente passato + * come prop `code`. + * + * Usata da plugins/pyrunner/remark.js e plugins/sqlrunner/remark.js. + * Convive con i fence normali (```py senza `live` resta un code block). + */ + +function isLiveMeta(meta) { + if (!meta) return false; + // meta è una stringa tipo "live" o "live readonly title=foo" ecc. + return /(^|\s)live(\s|=|$)/.test(meta); +} + +function parseMeta(meta, { numericKeys, file, langLabel }) { + // Estrae chiave/valore. Supporta "live" (booleano implicito) e "key=value". + const out = {}; + if (!meta) return out; + const tokens = meta.match(/(?:[^\s"]+|"[^"]*")+/g) || []; + for (const tok of tokens) { + const eq = tok.indexOf('='); + if (eq === -1) { + if (tok === 'live') continue; // marker, niente prop + out[tok] = true; + } else { + const key = tok.slice(0, eq); + let value = tok.slice(eq + 1); + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + if (key === 'live') continue; + // Meta numerici (maxLines, maxRows, ...): un valore non numerico + // verrebbe silenziosamente rimpiazzato dal default a runtime — meglio + // un warning a build time, con il file incriminato. + if (numericKeys.has(key) && !/^\d+$/.test(value)) { + console.warn( + `[${langLabel}] ${file?.path ?? '(file sconosciuto)'}: meta "${key}=${value}" non è un numero — verrà usato il default.`, + ); + } + out[key] = value; + } + } + return out; +} + +function toMdxAttributes(propsObj) { + const attrs = []; + for (const [name, value] of Object.entries(propsObj)) { + if (value === true) { + attrs.push({ type: 'mdxJsxAttribute', name, value: null }); + } else { + attrs.push({ + type: 'mdxJsxAttribute', + name, + value: String(value), + }); + } + } + return attrs; +} + +function makeCodeAttribute(code) { + // Passa il codice come prop `code={"..."}` con expression letterale. + // Più sicuro di un children text node: MDX non re-interpreta il contenuto. + const literal = JSON.stringify(code); + return { + type: 'mdxJsxAttribute', + name: 'code', + value: { + type: 'mdxJsxAttributeValueExpression', + value: literal, + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'Literal', + value: code, + raw: literal, + }, + }, + ], + }, + }, + }, + }; +} + +/** + * @param {object} options + * @param {string[]} options.langs lang dei fence da trasformare (es. ['py', 'python']) + * @param {string} options.componentName nome del componente JSX (es. 'PyRunner') + * @param {string[]} [options.numericKeys] meta che devono essere numerici + */ +function makeLiveFenceRemark({ langs, componentName, numericKeys = [] }) { + const langSet = new Set(langs); + const numericSet = new Set(numericKeys); + return function remarkLiveFence() { + return async (tree, file) => { + const { visit } = await import('unist-util-visit'); + visit(tree, 'code', (node, index, parent) => { + if (!parent || index == null) return; + if (!langSet.has(node.lang)) return; + if (!isLiveMeta(node.meta)) return; + + const props = parseMeta(node.meta, { + numericKeys: numericSet, + file, + langLabel: componentName, + }); + const attributes = [ + ...toMdxAttributes(props), + makeCodeAttribute(node.value), + ]; + + const jsxNode = { + type: 'mdxJsxFlowElement', + name: componentName, + attributes, + children: [], + }; + + parent.children.splice(index, 1, jsxNode); + }); + }; + }; +} + +module.exports = { makeLiveFenceRemark }; diff --git a/plugins/smartypants-guard.js b/plugins/smartypants-guard.js index 2c0539e..f23d896 100644 --- a/plugins/smartypants-guard.js +++ b/plugins/smartypants-guard.js @@ -19,8 +19,10 @@ const CODE_COMPONENTS = new Set(['InlineCode']); // Placeholder: un carattere qualunque NON-apice, lunghezza 1, per preservare -// la lunghezza del testo (smartypants splicea per indici). -const PLACEHOLDER = '·'; // middle dot, improbabile nel codice sorgente +// la lunghezza del testo (smartypants splicea per indici). Usiamo un code +// point della Private Use Area: a differenza di un carattere "improbabile" +// (es. ·), per definizione non può comparire in contenuto reale. +const PLACEHOLDER = ''; function collectProtectedTextNodes(node, inside, out) { if (!node || typeof node !== 'object') return; diff --git a/plugins/sqlrunner/remark.js b/plugins/sqlrunner/remark.js new file mode 100644 index 0000000..0645d7f --- /dev/null +++ b/plugins/sqlrunner/remark.js @@ -0,0 +1,19 @@ +/** + * Remark plugin: trasforma i code fence + * ```sql live [dataset=... stateful maxRows=...] + * ... + * ``` + * in JSX .... + * + * La logica condivisa con PyRunner vive in plugins/remark-live-fence.js. + */ + +const { makeLiveFenceRemark } = require('../remark-live-fence.js'); + +const remarkSqlRunner = makeLiveFenceRemark({ + langs: ['sql'], + componentName: 'SQLRunner', + numericKeys: ['maxLines', 'maxRows', 'timeout'], +}); + +module.exports = remarkSqlRunner; diff --git a/scripts/sync-sql-wasm.mjs b/scripts/sync-sql-wasm.mjs new file mode 100644 index 0000000..052f29c --- /dev/null +++ b/scripts/sync-sql-wasm.mjs @@ -0,0 +1,26 @@ +/** + * Copia il runtime sql.js (glue JS + WASM) da node_modules a static/sql-runner/. + * + * I file copiati vengono COMMITTATI: il sito li serve self-hosted (niente CDN — + * `importScripts` dentro un Web Worker non supporta SRI, il self-host same-origin + * elimina il rischio supply-chain a runtime). Rilanciare dopo ogni upgrade di + * sql.js: `node scripts/sync-sql-wasm.mjs`. + */ +import { copyFileSync, readFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const dist = join(root, 'node_modules', 'sql.js', 'dist'); +const dest = join(root, 'static', 'sql-runner'); + +mkdirSync(dest, { recursive: true }); + +const { version } = JSON.parse( + readFileSync(join(root, 'node_modules', 'sql.js', 'package.json'), 'utf8'), +); + +for (const file of ['sql-wasm.js', 'sql-wasm.wasm']) { + copyFileSync(join(dist, file), join(dest, file)); + console.log(`✓ ${file} (sql.js ${version}) → static/sql-runner/`); +} diff --git a/src/components/BentoFeatures/index.tsx b/src/components/BentoFeatures/index.tsx index 219bd84..4be9e1c 100644 --- a/src/components/BentoFeatures/index.tsx +++ b/src/components/BentoFeatures/index.tsx @@ -13,7 +13,17 @@ interface Feature { function PlayIcon() { return ( - diff --git a/src/components/OffPathBanner/index.tsx b/src/components/OffPathBanner/index.tsx index 99d3360..8774f4a 100644 --- a/src/components/OffPathBanner/index.tsx +++ b/src/components/OffPathBanner/index.tsx @@ -5,12 +5,15 @@ * un bottone per switchare il percorso (la sidebar si ricostruisce in * place senza navigare). */ -import React, {type ReactNode} from 'react'; -import {useDoc, useDocsVersion} from '@docusaurus/plugin-content-docs/client'; -import type {PropSidebar, PropSidebarItem} from '@docusaurus/plugin-content-docs'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {usePathContext, type VolumeId} from '@site/src/contexts/PathContext'; -import {pathLabel} from '@site/src/contexts/pathLabels'; +import React, { type ReactNode } from 'react'; +import { useDoc, useDocsVersion } from '@docusaurus/plugin-content-docs/client'; +import type { + PropSidebar, + PropSidebarItem, +} from '@docusaurus/plugin-content-docs'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { usePathContext, type VolumeId } from '@site/src/contexts/PathContext'; +import { pathLabel } from '@site/src/contexts/pathLabels'; import styles from './styles.module.css'; @@ -31,9 +34,9 @@ function sidebarContainsDoc(items: PropSidebar, docId: string): boolean { } function useOffPathInfo() { - const {metadata} = useDoc(); + const { metadata } = useDoc(); const version = useDocsVersion(); - const {getPath} = usePathContext(); + const { getPath } = usePathContext(); if (!VOLUMES.has(version.pluginId)) return null; const volumeId = version.pluginId as VolumeId; @@ -51,25 +54,25 @@ function useOffPathInfo() { const availableIn = Object.entries(sidebars) .filter( ([name, sb]) => - name !== chosen && - sidebarContainsDoc(sb as PropSidebar, metadata.id), + name !== chosen && sidebarContainsDoc(sb as PropSidebar, metadata.id), ) .map(([name]) => name); - return {volumeId, chosen, availableIn}; + return { volumeId, chosen, availableIn }; } export default function OffPathBanner(): ReactNode { const info = useOffPathInfo(); - const {setPath} = usePathContext(); + const { setPath } = usePathContext(); if (!info) return null; - const {volumeId, chosen, availableIn} = info; + const { volumeId, chosen, availableIn } = info; return (