]> danix's work - danix.xyz-2.git/commitdiff
refactor: syntax highlighting with Catppuccin Macchiato and copy buttons
authorDanilo M. <redacted>
Sat, 18 Apr 2026 17:02:18 +0000 (19:02 +0200)
committerDanilo M. <redacted>
Sat, 18 Apr 2026 17:02:18 +0000 (19:02 +0200)
- 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 <redacted>
hugo.toml
themes/danix-xyz-hacker/assets/css/chroma-custom.css
themes/danix-xyz-hacker/assets/js/code-copy.js [new file with mode: 0644]
themes/danix-xyz-hacker/layouts/_default/_markup/render-codeblock.html [new file with mode: 0644]
themes/danix-xyz-hacker/layouts/_default/baseof.html

index b7e0663be9b047b950e9c91744a0976a7713cd05..c20b6175441ac2b79cf4b17d3d4a585a1de354b2 100644 (file)
--- 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
index 3e91d500e4fb56ea55128a2bb5f682f6f90f85f3..11f4a4acff1ac4b7c8d79cfba54f75c8537cfce8 100644 (file)
-/* 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 (file)
index 0000000..bfcfd4a
--- /dev/null
@@ -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 (file)
index 0000000..813c389
--- /dev/null
@@ -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 -}}
+
+<div class="code-block-wrapper">
+  {{- if $hasLang -}}
+  <div class="code-header" data-lang="{{ $lang }}">
+    <span class="code-lang-label">{{ $lang }}</span>
+    <div class="code-copy-wrapper">
+      <span role="status" aria-live="polite" class="sr-only code-copy-status"></span>
+      <button class="code-copy-btn" aria-label="Copy code" data-copy-target>
+        <i data-feather="copy" aria-hidden="true"></i>
+        <i data-feather="check" aria-hidden="true" class="hidden"></i>
+      </button>
+    </div>
+  </div>
+  {{- end -}}
+  <div class="code-body">
+    {{ highlight .Inner $highlightLang $opts }}
+  </div>
+</div>
index 13f3fd2c51ce083d50628feb347e5735600badde..9370157cf5c8cd9fc61b16052af55b3a03f777df 100644 (file)
   <script src="{{ $progressScript.RelPermalink }}"></script>
   {{ end }}
 
+  <!-- Code block copy button -->
+  {{ if eq .Kind "page" }}
+  {{ $codeScript := resources.Get "js/code-copy.js" | minify }}
+  <script src="{{ $codeScript.RelPermalink }}"></script>
+  {{ end }}
+
   <!-- Matrix rain background effect -->
   {{ with resources.Get "js/matrix-rain.js" }}
   {{ $s := . | minify }}