From 9768943bc8a677210ac3ba3c96da87f33c22146a Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Wed, 3 Jun 2026 02:07:43 +0200 Subject: [PATCH 1/2] feat(typography): smart quotes automatici via remark-smartypants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converte a build-time gli apici dritti della prosa in virgolette curve tipografiche ("…" '…' e apostrofo ') su tutti i volumi, docs preset e blog. Lavora sull'AST: blocchi di codice, inline code e attributi JSX restano intatti, quindi il sorgente resta ASCII (comodo da scrivere) e la conversione è permanente per costruzione — niente migrazione né regole Prettier. Il config diventa async per caricare remark-smartypants (ESM-only) con import() dinamico. protectCode/restoreCode (plugins/smartypants-guard.js) proteggono i children di , che renderizza codice via testo e non va smart-quotato. Verificato sul build: H1/prosa con apici curvi, codice e con apici dritti (0 falsi positivi). Limite noto: title/meta/navbar derivano dal config/frontmatter e restano dritti (non passano dalla pipeline remark). Co-Authored-By: Claude Opus 4.8 --- docusaurus.config.ts | 436 ++++++++++++++++++----------------- package-lock.json | 169 +++++++++++++- package.json | 1 + plugins/smartypants-guard.js | 73 ++++++ 4 files changed, 469 insertions(+), 210 deletions(-) create mode 100644 plugins/smartypants-guard.js diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 44e027c..b6d99fe 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -4,6 +4,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 { protectCode, restoreCode } = require('./plugins/smartypants-guard.js'); // Keyword admonition custom (Notion-style callouts). Vedi // src/theme/Admonition/index.tsx per il registry di titoli + icone PNG. @@ -22,237 +24,255 @@ const admonitions = { extendDefaults: true, }; -const config: Config = { - title: 'Python Doesn\'t Byte', - tagline: 'Il libro di testo, reinventato.', - favicon: 'img/icons/favicon.ico', +// Il config è una funzione async perché remark-smartypants è ESM-only e va +// caricato con import() dinamico (il config viene valutato in contesto CJS, +// vedi il require() qui sopra). smartypants converte gli apici dritti della +// prosa in virgolette curve tipografiche ("…" '…' e apostrofo ') lavorando +// sull'AST: salta blocchi di codice e inline code, quindi il codice Python e +// gli attributi JSX restano intatti. protectCode/restoreCode (vedi +// plugins/smartypants-guard.js) lo avvolgono per proteggere anche i children +// di , che renderizza codice e non va "smart-quotato". +export default async function createConfig(): Promise { + const { default: smartypants } = await import('remark-smartypants'); + + const config: Config = { + title: "Python Doesn't Byte", + tagline: 'Il libro di testo, reinventato.', + favicon: 'img/icons/favicon.ico', - // Set the production url of your site here - url: 'https://www.rainbowbits.cloud', - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: '/python-doesnt-byte/', + // Set the production url of your site here + url: 'https://www.rainbowbits.cloud', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: '/python-doesnt-byte/', - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: 'marcofarina', // Usually your GitHub org/username. - projectName: 'python-doesnt-byte', // Usually your repo name. - deploymentBranch: 'gh-pages', - trailingSlash: false, + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'marcofarina', // Usually your GitHub org/username. + projectName: 'python-doesnt-byte', // Usually your repo name. + deploymentBranch: 'gh-pages', + trailingSlash: false, - onBrokenLinks: 'throw', + onBrokenLinks: 'throw', - markdown: { - hooks: { - onBrokenMarkdownLinks: 'warn', + markdown: { + hooks: { + onBrokenMarkdownLinks: 'warn', + }, }, - }, - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: 'it', - locales: ['it'], - }, + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'it', + locales: ['it'], + }, - presets: [ - [ - 'classic', - { - docs: { - sidebarPath: './sidebars.ts', - // 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], - admonitions, - }, - blog: { - showReadingTime: true, - feedOptions: { - type: ['rss', 'atom'], - xslt: true, + presets: [ + [ + 'classic', + { + docs: { + sidebarPath: './sidebars.ts', + // 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], + remarkPlugins: [protectCode, smartypants, restoreCode], + admonitions, }, - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - //editUrl: - // 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', - // Useful options to enforce blogging best practices - onInlineTags: 'warn', - onInlineAuthors: 'warn', - onUntruncatedBlogPosts: 'warn', - }, - theme: { - customCss: './src/css/custom.css', - }, - } satisfies Preset.Options, + blog: { + showReadingTime: true, + feedOptions: { + type: ['rss', 'atom'], + xslt: true, + }, + // Please change this to your repo. + // Remove this to remove the "edit this page" links. + //editUrl: + // 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', + // Useful options to enforce blogging best practices + remarkPlugins: [protectCode, smartypants, restoreCode], + onInlineTags: 'warn', + onInlineAuthors: 'warn', + onUntruncatedBlogPosts: 'warn', + }, + theme: { + customCss: './src/css/custom.css', + }, + } satisfies Preset.Options, + ], ], - ], - stylesheets: [ - { - href: 'https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,300..700;1,300..600&display=swap', - rel: 'stylesheet', - }, - ], - - clientModules: ['./src/fonts.ts'], - - plugins: [ - './plugins/pyrunner/index.js', - [ - '@docusaurus/plugin-content-docs', - { - id: 'programmatore', - path: 'volumes/programmatore', - routeBasePath: 'programmatore', - sidebarPath: './sidebars/programmatore.ts', - editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], - admonitions, - }, - ], - [ - '@docusaurus/plugin-content-docs', - { - id: 'artefice', - path: 'volumes/artefice', - routeBasePath: 'artefice', - sidebarPath: './sidebars/artefice.ts', - editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], - admonitions, - }, - ], - [ - '@docusaurus/plugin-content-docs', - { - id: 'archivista', - path: 'volumes/archivista', - routeBasePath: 'archivista', - sidebarPath: './sidebars/archivista.ts', - editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], - admonitions, - }, - ], - [ - '@docusaurus/plugin-content-docs', + stylesheets: [ { - id: 'apprendista', - path: 'volumes/apprendista', - routeBasePath: 'apprendista', - sidebarPath: './sidebars/apprendista.ts', - editUrl: 'https://github.com/marcofarina/python-doesnt-byte', - beforeDefaultRemarkPlugins: [remarkPyRunner], - admonitions, + href: 'https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,300..700;1,300..600&display=swap', + rel: 'stylesheet', }, ], - ], - themeConfig: { - // Replace with your project's social card - image: 'img/docusaurus-social-card.jpg', - docs: { - sidebar: { - autoCollapseCategories: true, - }, - }, - navbar: { - title: 'Python Doesn\'t Byte', - logo: { - alt: 'Python Doesn\'t Byte Logo', - src: 'img/logo.svg', - }, - items: [ + clientModules: ['./src/fonts.ts'], + + plugins: [ + './plugins/pyrunner/index.js', + [ + '@docusaurus/plugin-content-docs', { - type: 'dropdown', - label: 'Libri', - position: 'left', - items: [ - { - type: 'doc', - docId: 'intro', - docsPluginId: 'programmatore', - label: 'Manuale del Programmatore', - }, - { - type: 'doc', - docId: 'intro', - docsPluginId: 'artefice', - label: 'Manuale dell\'Artefice', - }, - { - type: 'doc', - docId: 'intro', - docsPluginId: 'archivista', - label: 'Manuale dell\'Archivista', - }, - { - type: 'doc', - docId: 'intro', - docsPluginId: 'apprendista', - label: 'Biblioteca dell\'Apprendista', - }, - ], + id: 'programmatore', + path: 'volumes/programmatore', + routeBasePath: 'programmatore', + sidebarPath: './sidebars/programmatore.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + beforeDefaultRemarkPlugins: [remarkPyRunner], + remarkPlugins: [protectCode, smartypants, restoreCode], + admonitions, }, - /* { - to: '/blog', - label: 'Blog', - position: 'left'},*/ - // GitHub e "Offrimi un caffè" sono renderizzati come icone+popup - // (NavbarIconButton) dal swizzle src/theme/Navbar/Content, non - // tramite navbar items standard. ], - }, - footer: { - links: [ + [ + '@docusaurus/plugin-content-docs', { - title: 'Rainbow Bits', - items: [ - { - label: 'Python Doesn\'t Byte', - to: '/docs/intro', - }, - ], + id: 'artefice', + path: 'volumes/artefice', + routeBasePath: 'artefice', + sidebarPath: './sidebars/artefice.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + beforeDefaultRemarkPlugins: [remarkPyRunner], + remarkPlugins: [protectCode, smartypants, restoreCode], + admonitions, }, + ], + [ + '@docusaurus/plugin-content-docs', { - title: 'Contribuisci', - items: [ - { - html: 'Bitcoin on-chainbc1qhll2p0geaeqn4qskl2fk9gnlqusrcqja0v8p2d' - }, - { - label: 'Buy me a coffee', - href: 'https://ko-fi.com/marcofarina', - }, - ], + id: 'archivista', + path: 'volumes/archivista', + routeBasePath: 'archivista', + sidebarPath: './sidebars/archivista.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + beforeDefaultRemarkPlugins: [remarkPyRunner], + remarkPlugins: [protectCode, smartypants, restoreCode], + admonitions, }, + ], + [ + '@docusaurus/plugin-content-docs', { - title: 'More', - items: [ - { - label: 'GitHub', - href: 'https://github.com/marcofarina/', - }, - ], + id: 'apprendista', + path: 'volumes/apprendista', + routeBasePath: 'apprendista', + sidebarPath: './sidebars/apprendista.ts', + editUrl: 'https://github.com/marcofarina/python-doesnt-byte', + beforeDefaultRemarkPlugins: [remarkPyRunner], + remarkPlugins: [protectCode, smartypants, restoreCode], + admonitions, }, ], - logo: { - alt: 'Rainbow Bits Logo', - src: 'img/rb-hero-logo-dark.svg', - href: 'https://www.rainbowbits.cloud', - width: 160, - height: 51, + ], + + themeConfig: { + // Replace with your project's social card + image: 'img/docusaurus-social-card.jpg', + docs: { + sidebar: { + autoCollapseCategories: true, + }, }, - copyright: `Except where otherwise noted, content on this site is licensed under a
Creative Commons Attribution - Non-commercial - Share Alike 4.0 International license. ${new Date().getFullYear()}.
Built with Docusaurus. Icons by Font Awesome.`, - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - }, - } satisfies Preset.ThemeConfig, -}; + navbar: { + title: "Python Doesn't Byte", + logo: { + alt: "Python Doesn't Byte Logo", + src: 'img/logo.svg', + }, + items: [ + { + type: 'dropdown', + label: 'Libri', + position: 'left', + items: [ + { + type: 'doc', + docId: 'intro', + docsPluginId: 'programmatore', + label: 'Manuale del Programmatore', + }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'artefice', + label: "Manuale dell'Artefice", + }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'archivista', + label: "Manuale dell'Archivista", + }, + { + type: 'doc', + docId: 'intro', + docsPluginId: 'apprendista', + label: "Biblioteca dell'Apprendista", + }, + ], + }, + /* { + to: '/blog', + label: 'Blog', + position: 'left'},*/ + // GitHub e "Offrimi un caffè" sono renderizzati come icone+popup + // (NavbarIconButton) dal swizzle src/theme/Navbar/Content, non + // tramite navbar items standard. + ], + }, + footer: { + links: [ + { + title: 'Rainbow Bits', + items: [ + { + label: "Python Doesn't Byte", + to: '/docs/intro', + }, + ], + }, + { + title: 'Contribuisci', + items: [ + { + html: 'Bitcoin on-chainbc1qhll2p0geaeqn4qskl2fk9gnlqusrcqja0v8p2d', + }, + { + label: 'Buy me a coffee', + href: 'https://ko-fi.com/marcofarina', + }, + ], + }, + { + title: 'More', + items: [ + { + label: 'GitHub', + href: 'https://github.com/marcofarina/', + }, + ], + }, + ], + logo: { + alt: 'Rainbow Bits Logo', + src: 'img/rb-hero-logo-dark.svg', + href: 'https://www.rainbowbits.cloud', + width: 160, + height: 51, + }, + copyright: `Except where otherwise noted, content on this site is licensed under a
Creative Commons Attribution - Non-commercial - Share Alike 4.0 International license. ${new Date().getFullYear()}.
Built with Docusaurus. Icons by Font Awesome.`, + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, + }; -export default config; + return config; +} diff --git a/package-lock.json b/package-lock.json index ab68d9e..0b6f077 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "python-doesnt-byte", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "python-doesnt-byte", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-python": "^6.2.1", @@ -42,6 +42,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "prettier": "^3.7.1", + "remark-smartypants": "^3.0.2", "typescript": "~5.5.2" }, "engines": { @@ -5424,6 +5425,16 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", "license": "MIT" }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "22.7.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", @@ -6541,6 +6552,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -15467,6 +15489,20 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -16128,6 +16164,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-numeric-range": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", @@ -18658,6 +18713,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", @@ -18849,6 +18920,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -20959,6 +21095,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-position": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", @@ -21013,6 +21164,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-visit-parents": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", diff --git a/package.json b/package.json index 6d51426..5b906d8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "prettier": "^3.7.1", + "remark-smartypants": "^3.0.2", "typescript": "~5.5.2" }, "overrides": { diff --git a/plugins/smartypants-guard.js b/plugins/smartypants-guard.js new file mode 100644 index 0000000..2c0539e --- /dev/null +++ b/plugins/smartypants-guard.js @@ -0,0 +1,73 @@ +// Guard per remark-smartypants. +// +// smartypants converte gli apici dritti della prosa in virgolette curve, ma +// salta solo i nodi `code`/`inlineCode` del Markdown e gli elementi HTML +// style/script. Non conosce i NOSTRI componenti che renderizzano codice via +// children testuali — in particolare , che produce un e il +// cui contenuto (es. kind="string" con "Hello, World!") DEVE mantenere gli +// apici dritti perché è codice, non prosa. +// +// Strategia: due trasformatori remark da mettere attorno a smartypants +// (`remarkPlugins: [protectCode, smartypants, restoreCode]`). `protectCode` +// sostituisce gli apici dentro questi componenti con un placeholder della +// stessa lunghezza (così smartypants non li tocca e gli indici di splice +// restano validi); `restoreCode` rimette i valori originali. Lo stash viaggia +// su `file.data`, condiviso fra i due trasformatori per lo stesso file. + +// Componenti i cui children testuali sono codice e non vanno "smart-quotati". +// Estendere qui se in futuro si aggiungono altri componenti code-like. +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 + +function collectProtectedTextNodes(node, inside, out) { + if (!node || typeof node !== 'object') return; + + const isCodeComponent = + (node.type === 'mdxJsxTextElement' || node.type === 'mdxJsxFlowElement') && + typeof node.name === 'string' && + CODE_COMPONENTS.has(node.name); + + const within = inside || isCodeComponent; + + if (within && node.type === 'text' && typeof node.value === 'string') { + out.push(node); + } + + if (Array.isArray(node.children)) { + for (const child of node.children) { + collectProtectedTextNodes(child, within, out); + } + } +} + +function protectCode() { + return (tree, file) => { + const nodes = []; + collectProtectedTextNodes(tree, false, nodes); + + const stash = []; + for (const node of nodes) { + if (/['"]/.test(node.value)) { + stash.push([node, node.value]); + node.value = node.value.replace(/['"]/g, PLACEHOLDER); + } + } + file.data.__smartypantsGuard = stash; + }; +} + +function restoreCode() { + return (tree, file) => { + const stash = file.data.__smartypantsGuard; + if (!stash) return; + for (const [node, value] of stash) { + node.value = value; + } + delete file.data.__smartypantsGuard; + }; +} + +module.exports = { protectCode, restoreCode }; From 5ea3461ba152a9512accbef7cf64f295d7e6eea5 Mon Sep 17 00:00:00 2001 From: Marco Farina Date: Wed, 3 Jun 2026 02:17:02 +0200 Subject: [PATCH 2/2] feat(typography): apostrofo curvo anche in titoli, navbar e meta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I titoli non passano dalla pipeline remark: le stringhe del config (titolo sito, navbar, footer, alt logo) non sono Markdown, e il /meta delle pagine deriva dall'H1 grezzo letto prima di smartypants. Essendo testo di display puro (mai codice), il modo corretto è scrivere il carattere curvo U+2019 direttamente nel sorgente: rende curvo ovunque (tab, anteprima social, navbar, sidebar) a costo zero. Curvare l'H1 sistema sia il corpo (smartypants è idempotente) sia il <title> derivato. Verificato sul build: <title>, og:title e navbar tutti con apostrofo curvo. Chiude il "limite noto" della PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- docusaurus.config.ts | 14 +++++++------- volumes/apprendista/intro.mdx | 2 +- volumes/archivista/intro.mdx | 2 +- volumes/artefice/intro.mdx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index b6d99fe..835d417 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -36,7 +36,7 @@ export default async function createConfig(): Promise<Config> { const { default: smartypants } = await import('remark-smartypants'); const config: Config = { - title: "Python Doesn't Byte", + title: 'Python Doesn’t Byte', tagline: 'Il libro di testo, reinventato.', favicon: 'img/icons/favicon.ico', @@ -179,9 +179,9 @@ export default async function createConfig(): Promise<Config> { }, }, navbar: { - title: "Python Doesn't Byte", + title: 'Python Doesn’t Byte', logo: { - alt: "Python Doesn't Byte Logo", + alt: 'Python Doesn’t Byte Logo', src: 'img/logo.svg', }, items: [ @@ -200,19 +200,19 @@ export default async function createConfig(): Promise<Config> { type: 'doc', docId: 'intro', docsPluginId: 'artefice', - label: "Manuale dell'Artefice", + label: 'Manuale dell’Artefice', }, { type: 'doc', docId: 'intro', docsPluginId: 'archivista', - label: "Manuale dell'Archivista", + label: 'Manuale dell’Archivista', }, { type: 'doc', docId: 'intro', docsPluginId: 'apprendista', - label: "Biblioteca dell'Apprendista", + label: 'Biblioteca dell’Apprendista', }, ], }, @@ -231,7 +231,7 @@ export default async function createConfig(): Promise<Config> { title: 'Rainbow Bits', items: [ { - label: "Python Doesn't Byte", + label: 'Python Doesn’t Byte', to: '/docs/intro', }, ], diff --git a/volumes/apprendista/intro.mdx b/volumes/apprendista/intro.mdx index b6171d4..b2b8d72 100644 --- a/volumes/apprendista/intro.mdx +++ b/volumes/apprendista/intro.mdx @@ -3,6 +3,6 @@ slug: / sidebar_position: 1 --- -# Biblioteca dell'Apprendista +# Biblioteca dell’Apprendista Volume 4 — esercizi e laboratori. Placeholder. diff --git a/volumes/archivista/intro.mdx b/volumes/archivista/intro.mdx index ee3936a..fb610ca 100644 --- a/volumes/archivista/intro.mdx +++ b/volumes/archivista/intro.mdx @@ -3,6 +3,6 @@ slug: / sidebar_position: 1 --- -# Manuale dell'Archivista +# Manuale dell’Archivista Volume 3 — dati, persistenza, SQLite. Placeholder. diff --git a/volumes/artefice/intro.mdx b/volumes/artefice/intro.mdx index bb54de3..245e827 100644 --- a/volumes/artefice/intro.mdx +++ b/volumes/artefice/intro.mdx @@ -3,6 +3,6 @@ slug: / sidebar_position: 1 --- -# Manuale dell'Artefice +# Manuale dell’Artefice Volume 2 — programmazione ad oggetti. Placeholder.