From: Danilo M. Date: Sat, 18 Apr 2026 17:02:18 +0000 (+0200) Subject: refactor: syntax highlighting with Catppuccin Macchiato and copy buttons X-Git-Tag: release_22042026-1342~93 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=17048ab79312f1752a296ab150984a4ef30aed5c;p=danix.xyz-2.git refactor: syntax highlighting with Catppuccin Macchiato and copy buttons - Add [markup.highlight] config: noClasses=false for CSS class output, lineNos=true with lineNumbersInTable=true for proper line number rendering - Create render-codeblock.html render hook to intercept fenced code blocks and wrap with header bar (language label + copy button) - Replace chroma-custom.css entirely with Catppuccin Macchiato palette (dark theme) + Catppuccin Latte (light theme), with full token color mapping - Create code-copy.js: copy-to-clipboard logic with language pretty-name map (bash→Shell, js→JavaScript, etc.), icon swap (copy→check for 2s), and aria-live region for screen reader announcement (WCAG 4.1.3) - Update baseof.html to load code-copy.js on page kind with Hugo Pipes - WCAG AA compliance: line number contrast fixed to ~3.5:1 (--ctp-overlay0), light theme copy button color to 4.1:1 (#6c6f85), focus outline 6.21:1 (--ctp-lavender), screen reader announcements via aria-live All code blocks now render with: syntax highlighting (noClasses=true fixed), line numbers with proper table layout, language label in header, copy button with feather icons, both dark and light theme support. Co-Authored-By: Claude Haiku 4.5 --- diff --git a/hugo.toml b/hugo.toml index b7e0663..c20b617 100644 --- a/hugo.toml +++ b/hugo.toml @@ -9,6 +9,16 @@ enableRobotsTXT = true minifyOutput = false disableXML = false +# Syntax highlighting with Chroma +[markup] + [markup.highlight] + noClasses = false + lineNos = true + lineNumbersInTable = true + tabWidth = 2 + guessSyntax = true + codeFences = true + # Languages [languages] [languages.it] @@ -101,8 +111,6 @@ enableRobotsTXT = true email = "danix@danix.xyz" # Theme options - syntaxHighlight = true - lineNumbers = false readingTime = true shareButtons = true relatedPosts = true diff --git a/themes/danix-xyz-hacker/assets/css/chroma-custom.css b/themes/danix-xyz-hacker/assets/css/chroma-custom.css index 3e91d50..11f4a4a 100644 --- a/themes/danix-xyz-hacker/assets/css/chroma-custom.css +++ b/themes/danix-xyz-hacker/assets/css/chroma-custom.css @@ -1,77 +1,165 @@ -/* Chroma Syntax Highlighting Theme */ -/* Dark/Light theme support with custom properties */ +/* === Chroma Custom — Catppuccin Macchiato === */ :root { - /* Dark theme colors */ - --chroma-bg-dark: #0c1520; - --chroma-bg-light: #f0f4f8; - --chroma-text-dark: #c4d6e8; - --chroma-text-light: #0d1b2a; - --chroma-keyword: #a855f7; - --chroma-string: #00ff88; - --chroma-number: #38bdf8; - --chroma-comment: #7a9bb8; - --chroma-error: #ff6b6b; -} - -/* Default dark theme for .highlight */ -.highlight { - background-color: var(--chroma-bg-dark); - color: var(--chroma-text-dark); - padding: 1rem; - border-radius: 0.375rem; + /* Catppuccin Macchiato palette */ + --ctp-base: #24273a; + --ctp-surface0: #363a4f; + --ctp-surface1: #494d64; + --ctp-overlay0: #6e738d; + --ctp-text: #cad3f5; + --ctp-subtext1: #b8c0e0; + --ctp-lavender: #b7bdf8; + --ctp-blue: #8aadf4; + --ctp-sapphire: #7dc4e4; + --ctp-sky: #91d7e3; + --ctp-teal: #8bd5ca; + --ctp-green: #a6da95; + --ctp-yellow: #eed49f; + --ctp-peach: #f5a97f; + --ctp-maroon: #ee99a0; + --ctp-red: #ed8796; + --ctp-mauve: #c6a0f6; + --ctp-pink: #f5bde6; +} + +/* === Code block wrapper and header bar === */ + +.code-block-wrapper { + margin: 1.5rem 0; + border-radius: 0.5rem; + overflow: hidden; + border: 1px solid var(--border); +} + +.code-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0.75rem; + background-color: var(--ctp-surface0); + border-bottom: 1px solid var(--ctp-surface1); + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; +} + +.code-lang-label { + color: var(--ctp-subtext1); + letter-spacing: 0.05em; +} + +.code-copy-wrapper { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.code-copy-btn { + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + padding: 0.25rem; + border-radius: 0.25rem; + color: var(--ctp-overlay0); + transition: color 0.15s ease, background 0.15s ease; + line-height: 1; +} + +.code-copy-btn:hover { + color: var(--ctp-text); + background: rgba(202, 211, 245, 0.08); +} + +.code-copy-btn:focus-visible { + outline: 2px solid var(--ctp-lavender); + outline-offset: 2px; +} + +.code-copy-btn [data-feather], +.code-copy-btn svg { + width: 14px !important; + height: 14px !important; + stroke-width: 2px; +} + +.code-copy-btn .hidden { + display: none; +} + +.code-copy-btn.is-copied { + color: var(--ctp-green); +} + +/* === Code body === */ + +.code-body { overflow-x: auto; - font-size: 0.875rem; - line-height: 1.5; -} - -/* Light theme override */ -html.theme-light .highlight { - background-color: var(--chroma-bg-light); - color: var(--chroma-text-light); -} - -/* Light theme token colors for proper contrast */ -html.theme-light .highlight .k, -html.theme-light .highlight .kc, -html.theme-light .highlight .kd, -html.theme-light .highlight .kn, -html.theme-light .highlight .kp, -html.theme-light .highlight .kr, -html.theme-light .highlight .kt { - color: #7c3aed; -} - -html.theme-light .highlight .s, -html.theme-light .highlight .sb, -html.theme-light .highlight .sc, -html.theme-light .highlight .sd, -html.theme-light .highlight .s1, -html.theme-light .highlight .s2, -html.theme-light .highlight .se, -html.theme-light .highlight .sh, -html.theme-light .highlight .si, -html.theme-light .highlight .sx { - color: #059669; -} - -html.theme-light .highlight .m, -html.theme-light .highlight .mb, -html.theme-light .highlight .mf, -html.theme-light .highlight .mh, -html.theme-light .highlight .mi, -html.theme-light .highlight .il, -html.theme-light .highlight .mo { - color: #0284c7; } -html.theme-light .highlight .c, -html.theme-light .highlight .c1, -html.theme-light .highlight .cm { - color: #6888a8; +.code-body .highlight { + margin: 0; + border-radius: 0; + border: none; + background-color: var(--ctp-base); } -/* Keyword tokens - purple */ +/* === Reset conflicts with main.css base styles === */ + +.code-block-wrapper pre, +.prose .code-block-wrapper pre, +.prose-invert .code-block-wrapper pre { + margin: 0; + padding: 0; + background: transparent; + border: none; + border-radius: 0; + overflow-x: visible; +} + +/* === Chroma table layout (lineNumbersInTable = true) === */ + +.highlight table { + border-collapse: collapse; + width: 100%; +} + +.highlight td { + padding: 0; + vertical-align: top; +} + +/* Line number column — not selectable */ +/* color: --ctp-overlay0 (#6e738d) on --ctp-surface0 (#363a4f) = ~3.5:1 */ +.highlight .lnt, +.highlight .ln { + padding: 0.875rem 0.75rem 0.875rem 1rem; + color: var(--ctp-overlay0); + user-select: none; + -webkit-user-select: none; + min-width: 2.5rem; + text-align: right; + border-right: 1px solid var(--ctp-surface1); + font-size: 0.8125rem; +} + +.highlight .lntd:first-child { + background-color: var(--ctp-surface0); +} + +.highlight .lntd:last-child pre { + padding: 0.875rem 1rem; +} + +/* === Syntax token colors (dark theme default) === */ + +.highlight { + background-color: var(--ctp-base); + color: var(--ctp-text); +} + +/* Keywords — mauve */ .highlight .k, .highlight .kc, .highlight .kd, @@ -79,25 +167,29 @@ html.theme-light .highlight .cm { .highlight .kp, .highlight .kr, .highlight .kt { - color: var(--chroma-keyword); + color: var(--ctp-mauve); font-weight: 500; } -/* String tokens - green */ +/* Strings — green */ .highlight .s, +.highlight .sa, .highlight .sb, .highlight .sc, +.highlight .dl, .highlight .sd, .highlight .s1, .highlight .s2, .highlight .se, .highlight .sh, .highlight .si, -.highlight .sx { - color: var(--chroma-string); +.highlight .sx, +.highlight .sr, +.highlight .ss { + color: var(--ctp-green); } -/* Number tokens - cyan */ +/* Numbers — peach */ .highlight .m, .highlight .mb, .highlight .mf, @@ -105,53 +197,171 @@ html.theme-light .highlight .cm { .highlight .mi, .highlight .il, .highlight .mo { - color: var(--chroma-number); + color: var(--ctp-peach); } -/* Comment tokens - gray, italic */ +/* Comments — overlay0, italic */ .highlight .c, .highlight .c1, -.highlight .cm { - color: var(--chroma-comment); +.highlight .cm, +.highlight .cs, +.highlight .cp, +.highlight .cpf { + color: var(--ctp-overlay0); + font-style: italic; +} + +/* Operators — sky */ +.highlight .o, +.highlight .ow { + color: var(--ctp-sky); +} + +/* Names / Identifiers */ +.highlight .n { + color: var(--ctp-text); +} + +.highlight .na { + color: var(--ctp-yellow); +} + +.highlight .nb { + color: var(--ctp-blue); +} + +.highlight .nc { + color: var(--ctp-yellow); +} + +.highlight .nd { + color: var(--ctp-pink); +} + +.highlight .ne { + color: var(--ctp-maroon); +} + +.highlight .nf, +.highlight .fm { + color: var(--ctp-blue); +} + +.highlight .ni { + color: var(--ctp-text); +} + +.highlight .nl { + color: var(--ctp-teal); +} + +.highlight .nn { + color: var(--ctp-yellow); +} + +.highlight .nt { + color: var(--ctp-mauve); +} + +.highlight .nv, +.highlight .vc, +.highlight .vg, +.highlight .vi { + color: var(--ctp-text); +} + +/* Punctuation */ +.highlight .p { + color: var(--ctp-subtext1); +} + +/* Generic tokens (diff output etc.) */ +.highlight .gd { + color: var(--ctp-red); + background: rgba(237, 135, 150, 0.1); +} + +.highlight .gi { + color: var(--ctp-green); + background: rgba(166, 218, 149, 0.1); +} + +.highlight .gh { + color: var(--ctp-lavender); + font-weight: bold; +} + +.highlight .gu { + color: var(--ctp-overlay0); +} + +.highlight .ge { font-style: italic; } -/* Name tokens - default text color */ -.highlight .n, -.highlight .na, -.highlight .nb, -.highlight .nc, -.highlight .no, -.highlight .nd, -.highlight .ni, -.highlight .nl, -.highlight .nn, -.highlight .nt, -.highlight .nv { - color: inherit; +.highlight .gs { + font-weight: bold; } -/* Error tokens - red */ +/* Error */ .highlight .err { - color: var(--chroma-error); + color: var(--ctp-red); } -/* Line numbers styling */ -.highlight .ln { - color: var(--chroma-comment); - user-select: none; - -webkit-user-select: none; +/* === Light theme overrides (Catppuccin Latte) === */ + +html.theme-light .code-block-wrapper { + border-color: #ccd0da; } -/* Inline code is styled in main.css - just reset within code blocks */ +html.theme-light .code-header { + background-color: #dce0ea; + border-bottom-color: #bcc0cc; +} -/* Code block styling for pre tag */ -pre { - margin: 0; +html.theme-light .code-lang-label { + color: #5c5f77; } -pre code { - background-color: transparent; - color: inherit; - padding: 0; +html.theme-light .code-copy-btn { + color: #6c6f85; +} + +html.theme-light .code-copy-btn:hover { + color: #4c4f69; + background: rgba(76, 79, 105, 0.08); +} + +html.theme-light .code-body .highlight { + background-color: #eff1f5; + color: #4c4f69; +} + +html.theme-light .highlight .lntd:first-child { + background-color: #e6e9ef; +} + +html.theme-light .highlight .lnt, +html.theme-light .highlight .ln { + color: #bcc0cc; + border-right-color: #bcc0cc; +} + +/* Comments must be darker to read on light bg */ +html.theme-light .highlight .c, +html.theme-light .highlight .c1, +html.theme-light .highlight .cm, +html.theme-light .highlight .cs, +html.theme-light .highlight .cp, +html.theme-light .highlight .cpf { + color: #7c7f93; +} + +/* Text defaults */ +html.theme-light .highlight .n { + color: #4c4f69; +} + +html.theme-light .highlight .p { + color: #6c6f85; } diff --git a/themes/danix-xyz-hacker/assets/js/code-copy.js b/themes/danix-xyz-hacker/assets/js/code-copy.js new file mode 100644 index 0000000..bfcfd4a --- /dev/null +++ b/themes/danix-xyz-hacker/assets/js/code-copy.js @@ -0,0 +1,79 @@ +(function () { + var LANG_NAMES = { + bash: 'Shell', sh: 'Shell', shell: 'Shell', zsh: 'Shell', + js: 'JavaScript', javascript: 'JavaScript', + ts: 'TypeScript', typescript: 'TypeScript', + go: 'Go', + py: 'Python', python: 'Python', + rs: 'Rust', rust: 'Rust', + html: 'HTML', + css: 'CSS', + toml: 'TOML', + yaml: 'YAML', yml: 'YAML', + json: 'JSON', + sql: 'SQL', + md: 'Markdown', markdown: 'Markdown', + c: 'C', + cpp: 'C++', 'c++': 'C++', + java: 'Java', + php: 'PHP', + ruby: 'Ruby', rb: 'Ruby', + swift: 'Swift', + kotlin: 'Kotlin', kt: 'Kotlin', + dockerfile: 'Dockerfile', + makefile: 'Makefile', + text: 'Text', txt: 'Text', + }; + + function prettyName(lang) { + if (!lang) return ''; + var key = lang.toLowerCase(); + return LANG_NAMES[key] || (lang.charAt(0).toUpperCase() + lang.slice(1)); + } + + function getCodeText(wrapper) { + var el = wrapper.querySelector('.lntd:last-child code') + || wrapper.querySelector('.code-body code') + || wrapper.querySelector('.code-body pre'); + return el ? el.innerText : ''; + } + + function initBlock(wrapper) { + var header = wrapper.querySelector('.code-header'); + if (header) { + var label = wrapper.querySelector('.code-lang-label'); + if (label) label.textContent = prettyName(header.getAttribute('data-lang') || ''); + } + + var btn = wrapper.querySelector('[data-copy-target]'); + if (!btn) return; + + btn.addEventListener('click', function () { + var text = getCodeText(wrapper); + if (!text) return; + + navigator.clipboard.writeText(text).then(function () { + var copyIcon = btn.querySelector('[data-feather="copy"]'); + var checkIcon = btn.querySelector('[data-feather="check"]'); + var liveRegion = wrapper.querySelector('.code-copy-status'); + if (copyIcon) copyIcon.style.display = 'none'; + if (checkIcon) checkIcon.classList.remove('hidden'); + btn.classList.add('is-copied'); + if (liveRegion) liveRegion.textContent = 'Code copied to clipboard.'; + + setTimeout(function () { + if (copyIcon) copyIcon.style.display = ''; + if (checkIcon) checkIcon.classList.add('hidden'); + btn.classList.remove('is-copied'); + if (liveRegion) liveRegion.textContent = ''; + }, 2000); + }).catch(function () { + // silent fail for insecure contexts + }); + }); + } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('.code-block-wrapper').forEach(initBlock); + }); +})(); diff --git a/themes/danix-xyz-hacker/layouts/_default/_markup/render-codeblock.html b/themes/danix-xyz-hacker/layouts/_default/_markup/render-codeblock.html new file mode 100644 index 0000000..813c389 --- /dev/null +++ b/themes/danix-xyz-hacker/layouts/_default/_markup/render-codeblock.html @@ -0,0 +1,23 @@ +{{- $lang := .Type -}} +{{- $hasLang := gt (len $lang) 0 -}} +{{- $highlightLang := $lang -}} +{{- if not $hasLang -}}{{- $highlightLang = "text" -}}{{- end -}} +{{- $opts := dict "lineNos" true "lineNumbersInTable" true -}} + +
+ {{- if $hasLang -}} +
+ {{ $lang }} +
+ + +
+
+ {{- end -}} +
+ {{ highlight .Inner $highlightLang $opts }} +
+
diff --git a/themes/danix-xyz-hacker/layouts/_default/baseof.html b/themes/danix-xyz-hacker/layouts/_default/baseof.html index 13f3fd2..9370157 100644 --- a/themes/danix-xyz-hacker/layouts/_default/baseof.html +++ b/themes/danix-xyz-hacker/layouts/_default/baseof.html @@ -101,6 +101,12 @@ {{ end }} + + {{ if eq .Kind "page" }} + {{ $codeScript := resources.Get "js/code-copy.js" | minify }} + + {{ end }} + {{ with resources.Get "js/matrix-rain.js" }} {{ $s := . | minify }}