/** * ───────────────────────────────────────────── * SPB Drive — Personal Cloud Drive * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : src/preview/code.mjs * Purpose : Shiki syntax highlighting + markdown-it GFM rendering * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { createHighlighter } from 'shiki'; import MarkdownIt from 'markdown-it'; import anchor from 'markdown-it-anchor'; import taskLists from 'markdown-it-task-lists'; /** Extension → shiki language id. Anything unknown highlights as plaintext. */ export const EXT_LANG = { js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'jsx', ts: 'typescript', tsx: 'tsx', py: 'python', rb: 'ruby', go: 'go', rs: 'rust', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', cs: 'csharp', java: 'java', kt: 'kotlin', swift: 'swift', m: 'objective-c', php: 'php', sh: 'shellscript', bash: 'shellscript', zsh: 'shellscript', fish: 'fish', ps1: 'powershell', sql: 'sql', html: 'html', htm: 'html', xml: 'xml', css: 'css', scss: 'scss', less: 'less', json: 'json', jsonc: 'jsonc', yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini', conf: 'ini', env: 'ini', dockerfile: 'docker', makefile: 'make', mk: 'make', cmake: 'cmake', gradle: 'groovy', groovy: 'groovy', lua: 'lua', r: 'r', jl: 'julia', scala: 'scala', pl: 'perl', ex: 'elixir', exs: 'elixir', erl: 'erlang', hs: 'haskell', ml: 'ocaml', clj: 'clojure', vue: 'vue', svelte: 'svelte', astro: 'astro', graphql: 'graphql', gql: 'graphql', proto: 'proto', tf: 'hcl', hcl: 'hcl', nix: 'nix', zig: 'zig', dart: 'dart', sol: 'solidity', asm: 'asm', s: 'asm', vim: 'viml', diff: 'diff', patch: 'diff', tex: 'latex', bib: 'latex', md: 'markdown', njk: 'html', csv: 'csv', tsv: 'tsv', txt: 'plaintext', log: 'log', bat: 'bat', cmd: 'bat', vb: 'vb', fs: 'fsharp', nim: 'nim', cr: 'crystal', d: 'd', pas: 'pascal', f90: 'fortran-free-form', cob: 'cobol', ada: 'ada', prisma: 'prisma', cue: 'cue', http: 'http', }; let highlighterPromise = null; async function getHighlighter() { if (!highlighterPromise) { highlighterPromise = createHighlighter({ themes: ['dark-plus', 'light-plus'], langs: ['javascript', 'typescript', 'python', 'json', 'yaml', 'markdown', 'shellscript', 'html', 'css', 'sql'], }); } return highlighterPromise; } /** Resolve a shiki language for a filename (plaintext fallback). */ export function langForFile(name) { const base = name.toLowerCase(); if (base === 'dockerfile') return 'docker'; if (base === 'makefile') return 'make'; const ext = base.includes('.') ? base.split('.').pop() : base; return EXT_LANG[ext] ?? 'plaintext'; } /** * Highlight source text → themed HTML (dual theme via CSS variables). * Unknown or unloadable languages degrade to plaintext, never throw. */ export async function highlightCode(text, name, { maxBytes = 2_000_000 } = {}) { const clipped = text.length > maxBytes; const source = clipped ? text.slice(0, maxBytes) : text; const hl = await getHighlighter(); let lang = langForFile(name); if (!hl.getLoadedLanguages().includes(lang)) { try { await hl.loadLanguage(lang); } catch { lang = 'plaintext'; } } const html = hl.codeToHtml(source, { lang, themes: { dark: 'dark-plus', light: 'light-plus' }, defaultColor: false, }); return { html, lang, clipped }; } const md = new MarkdownIt({ html: false, linkify: true, typographer: true }) .use(anchor, { permalink: anchor.permalink.headerLink({ safariReaderFix: true }) }) .use(taskLists, { enabled: false }); // Fenced code inside markdown gets a plain
 with a language class;
// mermaid fences are tagged for client-side rendering.
md.renderer.rules.fence = (tokens, idx) => {
  const token = tokens[idx];
  const info = (token.info || '').trim().split(/\s+/)[0];
  const escaped = md.utils.escapeHtml(token.content);
  if (info === 'mermaid') {
    return `
${escaped}
\n`; } return `
${escaped}
\n`; }; /** Render GFM markdown → sanitized HTML (raw HTML disabled). */ export function renderMarkdown(text) { return md.render(text ?? ''); }