From 108b94acde89b58551b2d17a13b4af169706300f Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Wed, 10 Jun 2026 03:11:13 +0200 Subject: [PATCH 1/3] =?UTF-8?q?refactor(code-health):=20fix=20dalla=20revi?= =?UTF-8?q?ew=20=E2=80=94=20dedup,=20dead=20code,=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FontAwesome library.add registrata solo in Root.tsx (era duplicata in MDXComponents) - Helper clipboard condiviso (clipboard.ts) con errore propagato dal fallback; usato da Toolbar e da "Spiegamelo facile" - Editor PyRunner rifattorizzato a forwardRef + useImperativeHandle (via prop handleRef non standard) + prop opzionale `language` per riuso con altri linguaggi - coerceBool/coerceNumber estratti in coerce.ts (riusabili) - PathContext: rimosso clearPath mai usato; fix hydration mismatch (useState parte da {} come l'HTML server-side, localStorage letto nell'effect) - remark pyrunner: logica estratta nella factory condivisa remark-live-fence.js con warning a build time per meta numerici non validi - plugins/pyrunner: warning se la directory degli esempi non esiste - smartypants-guard: placeholder U+E000 (Private Use Area) al posto di "·" - grid.py: bare except → except Exception con motivazione - Toolbar: prop opzionali showResetDb/onResetDb (per SQLRunner) - Riformattazione Prettier su file con violazioni pre-esistenti (lint ora verde) Co-Authored-By: Claude Fable 5 --- plugins/pyrunner/index.js | 7 +- plugins/pyrunner/remark.js | 107 ++--------------- plugins/remark-live-fence.js | 134 ++++++++++++++++++++++ plugins/smartypants-guard.js | 6 +- src/components/BentoFeatures/index.tsx | 36 +++++- src/components/Epigraph/index.tsx | 11 +- src/components/NavbarIconButton/index.tsx | 5 +- src/components/OffPathBanner/index.tsx | 37 +++--- src/components/PathSelector/index.tsx | 12 +- src/components/Tooltip/index.tsx | 8 +- src/contexts/PathContext.tsx | 34 ++---- src/pages/index.tsx | 48 +++++++- src/pages/playground.tsx | 12 +- src/pyBoot.ts | 10 +- src/theme/ColorModeToggle/index.tsx | 11 +- src/theme/DocItem/Content/index.tsx | 19 +-- src/theme/DocRoot/index.tsx | 19 +-- src/theme/Navbar/Content/index.tsx | 19 +-- src/theme/Navbar/Logo/index.tsx | 8 +- src/theme/PyRunner/Editor.tsx | 54 ++++----- src/theme/PyRunner/Toolbar.tsx | 40 ++++--- src/theme/PyRunner/clipboard.ts | 15 +++ src/theme/PyRunner/coerce.ts | 20 ++++ src/theme/PyRunner/index.tsx | 35 ++---- src/theme/Root.tsx | 12 +- static/bry-libs/grid.py | 9 +- 26 files changed, 445 insertions(+), 283 deletions(-) create mode 100644 plugins/remark-live-fence.js create mode 100644 src/theme/PyRunner/clipboard.ts create mode 100644 src/theme/PyRunner/coerce.ts 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/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 (