diff --git a/.eleventy.js b/.eleventy.js index 4851d2a..f35eec6 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -32,8 +32,8 @@ async function do_minifyhtml(source, output_path) { module.exports = function(eleventyConfig) { // Plugins - eleventyConfig.addPlugin(require("./src/libs/shiki.js"), { }); - eleventyConfig.addPlugin(require("./src/libs/mermaid.js"), { mermaid_config: {'startOnLoad': false, 'theme': 'default' }}); + eleventyConfig.addPlugin(require("./src/libs/shiki.js")); + eleventyConfig.addPlugin(require("./src/libs/mermaid.js")); // To enable merging of tags eleventyConfig.setDataDeepMerge(true) diff --git a/package.json b/package.json index c78fa23..21a84af 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "markdown-it": "14.1.0", "markdown-it-anchor": "9.0.1", "shiki": "1.10.3", + "@shikijs/transformers": "1.10.3", "tailwindcss": "3.4.4", "html-minifier-terser": "7.2.0", "terser": "5.31.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0fb760..8d16bd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@shikijs/transformers': + specifier: 1.10.3 + version: 1.10.3 '@tailwindcss/typography': specifier: 0.5.13 version: 0.5.13(tailwindcss@3.4.4) @@ -171,6 +174,9 @@ packages: '@shikijs/core@1.10.3': resolution: {integrity: sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==} + '@shikijs/transformers@1.10.3': + resolution: {integrity: sha512-MNjsyye2WHVdxfZUSr5frS97sLGe6G1T+1P41QjyBFJehZphMcr4aBlRLmq6OSPBslYe9byQPVvt/LJCOfxw8Q==} + '@sindresorhus/slugify@1.1.2': resolution: {integrity: sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==} engines: {node: '>=10'} @@ -1663,6 +1669,10 @@ snapshots: dependencies: '@types/hast': 3.0.4 + '@shikijs/transformers@1.10.3': + dependencies: + shiki: 1.10.3 + '@sindresorhus/slugify@1.1.2': dependencies: '@sindresorhus/transliterate': 0.1.2 diff --git a/src/assets/styles/index.css b/src/assets/styles/index.css index aa5d9cb..dbc93ed 100644 --- a/src/assets/styles/index.css +++ b/src/assets/styles/index.css @@ -21,6 +21,19 @@ html.dark .shiki span { text-decoration: var(--shiki-dark-text-decoration) !important; } +/* Shiki code line diffs */ +.shiki span.line.diff.add, +.shiki span.line.diff.add span +{ + background-color: rgba(16, 185, 129, 0.14) !important +} + +.shiki span.line.diff.remove, +.shiki span.line.diff.remove span +{ + background-color: rgba(244, 63, 94, 0.14) !important +} + /* Copy code buttons on code blocks */ /* https://junges.dev/2-how-to-add-github-copy-to-clipboard-button-on-your-docsblog */ pre[class*="shiki"] { @@ -30,9 +43,6 @@ pre[class*="shiki"] { } pre > .button-copy-code { - @apply rounded; - @apply bg-gray-300; - @apply py-2 px-2; position: absolute; top: 32px; right: 16px; @@ -43,21 +53,3 @@ pre > .button-copy-code { display: none; } } - -pre > .button-copy-code:hover { - @apply border-2 border-gray-600; - @apply bg-gray-200; -} - -pre > .button-copy-code:focus { - @apply bg-gray-300; -} - -.copy-docs-icon { - fill: #0a001f; -} - -.docs-copied-icon { - color: #148a25 !important; - fill: #148a25 !important; -} diff --git a/src/libs/mermaid.js b/src/libs/mermaid.js index e1c8a21..bfd6be0 100644 --- a/src/libs/mermaid.js +++ b/src/libs/mermaid.js @@ -1,5 +1,9 @@ module.exports = (eleventyConfig, options) => { - let mermaid_config = {...options?.mermaid_config || {}, ...{loadOnSave: true}}; + let mermaid_config = { + startOnLoad: false, + theme: 'default', + loadOnSave: true + }; let src = options?.mermaid_js_src || "https://unpkg.com/mermaid/dist/mermaid.esm.min.mjs"; eleventyConfig.addLiquidShortcode("mermaid_js_scripts", () => { diff --git a/src/libs/shiki.js b/src/libs/shiki.js index b3eeb16..725e55d 100644 --- a/src/libs/shiki.js +++ b/src/libs/shiki.js @@ -8,6 +8,7 @@ module.exports = (eleventyConfig, options) => { eleventyConfig.on('eleventy.before', async () => { const shiki = await import('shiki'); const fs = await import('fs'); + const { transformerNotationDiff } = await import('@shikijs/transformers'); // Load the theme object from a file, a network request, or anywhere const darkColourTheme = JSON.parse(fs.readFileSync('src/libs/nomos-black-colour-theme.json', 'utf8')) @@ -23,7 +24,9 @@ module.exports = (eleventyConfig, options) => { 'sql', 'csharp', 'fsharp', - 'xml' + 'xml', + 'javascript', + 'css' ], }); @@ -43,7 +46,8 @@ module.exports = (eleventyConfig, options) => { themes: { light: "light-plus", dark: "NomosBlack" - } + }, + transformers: [ transformerNotationDiff() ] }); } } diff --git a/src/posts/2024/07/shiki-and-mermaid-in-11ty.md b/src/posts/2024/07/shiki-and-mermaid-in-11ty.md new file mode 100644 index 0000000..64c856b --- /dev/null +++ b/src/posts/2024/07/shiki-and-mermaid-in-11ty.md @@ -0,0 +1,326 @@ +--- +title: Shiki and Mermaid in 11ty +date: 2024-07-16 +published: true +enableMermaid: true +tags: +- in english +- 11ty +- eleventy +- shiki +- mermaid +--- +See how to integrate Shiki to your 11ty project, with support for Mermaid diagrams. + +[Ler em português](../shiki-e-mermaid-no-11ty) + +## 11ty, Shiki and Mermaid + +[11ty](https://www.11ty.dev/) (Eleventy) is a static-site generator tool that uses templating engines to generate websites. Eleventy has a pipeline execution that makes it easy to add plugins and transformations to HTML, JS and CSS files. + +[Shiki](https://shiki.style/) is a npm package that renders HTML code blocks with syntax highlighting for a specified language. Among its interesting features, it allows dual theming (light and dark switching), code focusing and diffing notations, and custom themes. + +While I was making this blog, which uses 11ty, I wanted to have support for [Mermaid](https://mermaid.js.org) diagrams in my blog posts, alongside code blocks. In this post, let's see how to integrate Shiki and Mermaid into your 11ty project. + +* [Shiki integration](#shiki-integration) +* [Mermaid integration](#mermaid-integration) +* [Final results](#final-results) +* [Copy code buttons](#copy-code-buttons) + +## Shiki integration + +### Add Shiki to your project + +```sh [npm] +npm install -D shiki +``` + +### Shiki custom plug-in + +Create a Javascript file to configure Shiki for your project, for example, at `src/libs/shiki.js`: + +```javascript +module.exports = (eleventyConfig, options) => { + // empty call to notify 11ty that we use this feature + // eslint-disable-next-line no-empty-function + eleventyConfig.amendLibrary('md', () => { }); + + eleventyConfig.on('eleventy.before', async () => { + const shiki = await import('shiki'); + + // highlighter config + const highlighter = await shiki.createHighlighter( + { + themes: ["light-plus", "dark-plus"], + langs: [ + 'shell', 'html', 'yaml', + 'sql', 'xml', 'javascript' + ] + }); + + eleventyConfig.amendLibrary('md', (mdLib) => + mdLib.set({ + highlight: (code, lang) => { + return highlighter.codeToHtml(code, + { + lang: lang, + themes: { + light: "light-plus", + dark: "dark-plus" + } + }); + } + }) + ); + }); +}; +``` + +### Call Shiki custom plug-in in `.eleventy.js` + +```javascript +module.exports = function(eleventyConfig) { + ... + + // IMPORTANT! + // remove 11ty syntax highlighter plugin, if present: + eleventyConfig.addPlugin(syntaxHighlight) // [!code --] + + // Add: + eleventyConfig.addPlugin(require("./src/libs/shiki.js")); // [!code ++] + + ... +} +``` + +## Mermaid integration + +If you want to use Shiki and also want to render Mermaid diagrams, you need to change the Shiki plug-in to render Mermaid as HTML divs, instead of code blocks. + +### Add htmlencode to your project + +```sh [npm] +npm install -D htmlencode +``` + +### Modify Shiki plug-in + +```javascript +const htmlencode = require('htmlencode'); // [!code ++] + +module.exports = (eleventyConfig, options) => { + // empty call to notify 11ty that we use this feature + // eslint-disable-next-line no-empty-function + eleventyConfig.amendLibrary('md', () => { }); + + eleventyConfig.on('eleventy.before', async () => { + const shiki = await import('shiki'); + + // highlighter config + const highlighter = await shiki.createHighlighter( + { + themes: ["light-plus", "dark-plus"], + langs: [ + 'shell', 'html', 'yaml', + 'sql', 'xml', 'javascript' + ] + }); + + eleventyConfig.amendLibrary('md', (mdLib) => + mdLib.set({ + highlight: (code, lang) => { + if (lang === "mermaid") { // [!code ++] + const extra_classes = options?.extra_classes ? ' ' + options.extra_classes : ''; // [!code ++] + return `
${htmlencode.htmlEncode(code)}
`; // [!code ++] + } // [!code ++] + else { // [!code ++] + return highlighter.codeToHtml(code, + { + lang: lang, + themes: { + light: "light-plus", + dark: "dark-plus" + } + }); + } // [!code ++] + } + }) + ); + }); +}; +``` + +### Mermaid custom plug-in + +Create a Javascript file to configure Mermaid for your project, for example, at `src/libs/mermaid.js`: + +```javascript +module.exports = (eleventyConfig, options) => { + let mermaid_config = { + startOnLoad: false, + theme: "default", + loadOnSave: true + }; + let src = options?.mermaid_js_src || "https://unpkg.com/mermaid/dist/mermaid.esm.min.mjs"; + + eleventyConfig.addShortcode("mermaid_js_scripts", () => { + return `` + }); + return {} +}; +``` +### Call Mermaid custom plug-in in `.eleventy.js` +```javascript +module.exports = function(eleventyConfig) { + ... + + eleventyConfig.addPlugin(require("./src/libs/shiki.js")); + eleventyConfig.addPlugin(require("./src/libs/mermaid.js")); // [!code ++] + + ... +} +``` + +### Include Mermaid script in pages + +Add the `mermaid_js_scripts` shortcode to pages and templates that will have diagrams: + +``` +{{ '{%' }} mermaid_js_scripts {{ '%}' }} +``` + +## Final results + +Let's see it working. + +SQL code block with syntax highlight: + +```sql +SELECT * INTO dbo.NewProducts +FROM Production.Product +WHERE ListPrice > 25 +AND ListPrice < 100; +``` + +Mermaid flowchart: + +``` +flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] +``` + +```mermaid +flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] +``` + +If you want to check a full source code example for this blog project, visit our [GitHub repo](https://github.com/alexandrehtrb/alexandrehtrb.github.io). It includes switching between light and dark themes for both Shiki and Mermaid. + +## Copy code buttons + +Shiki doesn't provide copy code buttons by default, but we can add them through a client-side script. I use a slightly modified version of the script shown in [this blog post](https://junges.dev/2-how-to-add-github-copy-to-clipboard-button-on-your-docsblog). + +### Reference the script in templates + +Add the ` +``` + +### Add script to insert copy code buttons + +Create a Javascript file for a client-side script that will find all code blocks and add copy code buttons to them. The icons SVGs are declared inline in this example. + +For example, create your file at `src/scripts/addCopyCodeButtons.js`: + +```javascript +let blocks = document.querySelectorAll("pre.shiki"); +const copyCodeIconSvg = ''; +const codeCopiedIconSvg = ''; + +blocks.forEach((block) => { + if (!navigator.clipboard) { + return; + } + + let button = document.createElement("button"); + button.className = "button-copy-code"; + button.ariaLabel = button.title = "Copy"; + button.innerHTML = copyCodeIconSvg; + block.appendChild(button); + + button.addEventListener("click", async () => { + await copyCode(button, block); + }); +}); + +async function copyCode(button, block) { + let copiedCode = block.cloneNode(true); + copiedCode.removeChild(copiedCode.querySelector("button.button-copy-code")); + + const html = copiedCode.outerHTML.replace(/<[^>]*>?/gm, ""); + + block.querySelector("button.button-copy-code").innerHTML = codeCopiedIconSvg; + button.ariaLabel = button.title = "Copied!"; + setTimeout(function () { + block.querySelector("button.button-copy-code").innerHTML = copyCodeIconSvg; + button.ariaLabel = button.title = "Copy"; + }, 2000); + + const parsedHTML = htmlDecode(html); + + await navigator.clipboard.writeText(parsedHTML); +} + +function htmlDecode(input) { + const doc = new DOMParser().parseFromString(input, "text/html"); + return doc.documentElement.textContent; +} +``` + +### CSS for copy code buttons + +Add the CSS code below to style the button position and visibility: + +```css +pre[class*="shiki"] { + position: relative; + margin: 5px 0; + padding: 1.75rem 0 1.75rem 1rem; +} + +pre > .button-copy-code { + position: absolute; + top: 32px; + right: 16px; +} + +@media only screen and (max-device-width: 480px) { + pre > .button-copy-code { + display: none; + } +} +``` + +## References + +* [11ty docs](https://www.11ty.dev/docs/) +* [Shiki docs](https://shiki.style/guide/) +* [Mermaid docs](https://mermaid.js.org/intro/) +* [Junges.dev - How to add GitHub Copy to Clipboard button on your docs/blog ](https://junges.dev/2-how-to-add-github-copy-to-clipboard-button-on-your-docsblog) diff --git a/src/posts/2024/07/shiki-e-mermaid-no-11ty.md b/src/posts/2024/07/shiki-e-mermaid-no-11ty.md new file mode 100644 index 0000000..1acf460 --- /dev/null +++ b/src/posts/2024/07/shiki-e-mermaid-no-11ty.md @@ -0,0 +1,326 @@ +--- +title: Shiki e Mermaid no 11ty +date: 2024-07-16 +published: true +enableMermaid: true +tags: +- em português +- 11ty +- eleventy +- shiki +- mermaid +--- +Veja como integrar o Shiki no seu projeto 11ty, com suporte a diagramas Mermaid. + +[Read in english](../shiki-and-mermaid-in-11ty) + +## 11ty, Shiki e Mermaid + +O [11ty](https://www.11ty.dev/) (Eleventy) é um gerador de sites estáticos que usa templates. Ele tem uma pipeline de execução que torna fácil adicionar plug-ins e transformações a arquivos HTML, JS e CSS. + +O [Shiki](https://shiki.style/) é um pacote npm que renderiza blocos de código HTML com coloração de sintaxe de acordo com a linguagem do código. Dentre suas vantagens, ele permite temas duais (que alternam entre claro e escuro), notações de código com diferenças e foco, e temas customizados. + +Neste post, vamos ver como integrar o Shiki e diagramas [Mermaid](https://mermaid.js.org) em um projeto 11ty. + +* [Integrando com o Shiki](#integrando-com-o-shiki) +* [Integrando com o Mermaid](#integrando-com-o-mermaid) +* [Resultados finais](#resultados-finais) +* [Botões para copiar código](#botões-para-copiar-código) + +## Integrando com o Shiki + +### Adicionar o Shiki ao projeto + +```sh [npm] +npm install -D shiki +``` + +### Plug-in para o Shiki + +Criar um arquivo Javascript para configuração do Shiki no seu projeto, por exemplo, no caminho `src/libs/shiki.js`: + +```javascript +module.exports = (eleventyConfig, options) => { + // empty call to notify 11ty that we use this feature + // eslint-disable-next-line no-empty-function + eleventyConfig.amendLibrary('md', () => { }); + + eleventyConfig.on('eleventy.before', async () => { + const shiki = await import('shiki'); + + // highlighter config + const highlighter = await shiki.createHighlighter( + { + themes: ["light-plus", "dark-plus"], + langs: [ + 'shell', 'html', 'yaml', + 'sql', 'xml', 'javascript' + ] + }); + + eleventyConfig.amendLibrary('md', (mdLib) => + mdLib.set({ + highlight: (code, lang) => { + return highlighter.codeToHtml(code, + { + lang: lang, + themes: { + light: "light-plus", + dark: "dark-plus" + } + }); + } + }) + ); + }); +}; +``` + +### Chamar plug-in do Shiki no `.eleventy.js` + +```javascript +module.exports = function(eleventyConfig) { + ... + + // IMPORTANTE! + // remover plug-in de syntax highlight padrão do 11ty + eleventyConfig.addPlugin(syntaxHighlight) // [!code --] + + // Adicionar: + eleventyConfig.addPlugin(require("./src/libs/shiki.js")); // [!code ++] + + ... +} +``` + +## Integrando com o Mermaid + +Se você quer usar o Shiki e também quer ter diagramas Mermaid, é necessário alterar o plug-in do Shiki para renderizar blocos Mermaid como divs HTML, ao invés de blocos de código. + +### Adicionar o htmlencode ao projeto + +```sh [npm] +npm install -D htmlencode +``` + +### Modificar o plug-in do Shiki + +```javascript +const htmlencode = require('htmlencode'); // [!code ++] + +module.exports = (eleventyConfig, options) => { + // empty call to notify 11ty that we use this feature + // eslint-disable-next-line no-empty-function + eleventyConfig.amendLibrary('md', () => { }); + + eleventyConfig.on('eleventy.before', async () => { + const shiki = await import('shiki'); + + // highlighter config + const highlighter = await shiki.createHighlighter( + { + themes: ["light-plus", "dark-plus"], + langs: [ + 'shell', 'html', 'yaml', + 'sql', 'xml', 'javascript' + ] + }); + + eleventyConfig.amendLibrary('md', (mdLib) => + mdLib.set({ + highlight: (code, lang) => { + if (lang === "mermaid") { // [!code ++] + const extra_classes = options?.extra_classes ? ' ' + options.extra_classes : ''; // [!code ++] + return `
${htmlencode.htmlEncode(code)}
`; // [!code ++] + } // [!code ++] + else { // [!code ++] + return highlighter.codeToHtml(code, + { + lang: lang, + themes: { + light: "light-plus", + dark: "dark-plus" + } + }); + } // [!code ++] + } + }) + ); + }); +}; +``` + +### Plug-in para o Mermaid + +Criar um arquivo Javascript para configuração do Mermaid no seu projeto, por exemplo, no caminho `src/libs/mermaid.js`: + +```javascript +module.exports = (eleventyConfig, options) => { + let mermaid_config = { + startOnLoad: false, + theme: "default", + loadOnSave: true + }; + let src = options?.mermaid_js_src || "https://unpkg.com/mermaid/dist/mermaid.esm.min.mjs"; + + eleventyConfig.addShortcode("mermaid_js_scripts", () => { + return `` + }); + return {} +}; +``` +### Chamar plug-in do Mermaid no `.eleventy.js` +```javascript +module.exports = function(eleventyConfig) { + ... + + eleventyConfig.addPlugin(require("./src/libs/shiki.js")); + eleventyConfig.addPlugin(require("./src/libs/mermaid.js")); // [!code ++] + + ... +} +``` + +### Incluir script do Mermaid nas páginas + +Adicionar o shortcode `mermaid_js_scripts` nas páginas e templates que vão ter diagramas: + +``` +{{ '{%' }} mermaid_js_scripts {{ '%}' }} +``` + +## Resultados finais + +Vamos ver funcionando. + +Bloco de código SQL com coloração de sintaxe: + +```sql +SELECT * INTO dbo.NewProducts +FROM Production.Product +WHERE ListPrice > 25 +AND ListPrice < 100; +``` + +Diagrama de fluxo do Mermaid: + +``` +flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] +``` + +```mermaid +flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] +``` + +Se quiser conferir um exemplo completo de código para este blog, visite nosso [repo no GitHub](https://github.com/alexandrehtrb/alexandrehtrb.github.io). Lá você encontra como alternar entre tema claro e escuro, tanto para o Shiki como para o Mermaid. + +## Botões para copiar código + +O Shiki não provê de fábrica botões para copiar código, porém, podemos adicioná-los através de um script do lado do cliente. Aqui, usaremos uma versão modificada do exemplo [deste post de blog](https://junges.dev/2-how-to-add-github-copy-to-clipboard-button-on-your-docsblog). + +### Referenciar o script nos templates + +Adicionar a tag ` +``` + +### Script para adicionar botões de copiar código + +Vamos criar um script de lado de cliente que vai pegar todos os blocos de código e inserir botões de copiar neles. Os ícones estão declarados em linha neste exemplo, como SVGs. + +Exemplo de arquivo no caminho `src/scripts/addCopyCodeButtons.js`: + +```javascript +let blocks = document.querySelectorAll("pre.shiki"); +const copyCodeIconSvg = ''; +const codeCopiedIconSvg = ''; + +blocks.forEach((block) => { + if (!navigator.clipboard) { + return; + } + + let button = document.createElement("button"); + button.className = "button-copy-code"; + button.ariaLabel = button.title = "Copiar"; + button.innerHTML = copyCodeIconSvg; + block.appendChild(button); + + button.addEventListener("click", async () => { + await copyCode(button, block); + }); +}); + +async function copyCode(button, block) { + let copiedCode = block.cloneNode(true); + copiedCode.removeChild(copiedCode.querySelector("button.button-copy-code")); + + const html = copiedCode.outerHTML.replace(/<[^>]*>?/gm, ""); + + block.querySelector("button.button-copy-code").innerHTML = codeCopiedIconSvg; + button.ariaLabel = button.title = "Copiado!"; + setTimeout(function () { + block.querySelector("button.button-copy-code").innerHTML = copyCodeIconSvg; + button.ariaLabel = button.title = "Copiar"; + }, 2000); + + const parsedHTML = htmlDecode(html); + + await navigator.clipboard.writeText(parsedHTML); +} + +function htmlDecode(input) { + const doc = new DOMParser().parseFromString(input, "text/html"); + return doc.documentElement.textContent; +} +``` + +### CSS para os botões de copiar código + +O CSS abaixo controla a posição e visibilidade dos botões: + +```css +pre[class*="shiki"] { + position: relative; + margin: 5px 0; + padding: 1.75rem 0 1.75rem 1rem; +} + +pre > .button-copy-code { + position: absolute; + top: 32px; + right: 16px; +} + +@media only screen and (max-device-width: 480px) { + pre > .button-copy-code { + display: none; + } +} +``` + +## Referências + +* [11ty docs](https://www.11ty.dev/docs/) +* [Shiki docs](https://shiki.style/guide/) +* [Mermaid docs](https://mermaid.js.org/intro/) +* [Junges.dev - How to add GitHub Copy to Clipboard button on your docs/blog ](https://junges.dev/2-how-to-add-github-copy-to-clipboard-button-on-your-docsblog)