spb/drive Public
SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.
JavaScript 82.7%
CSS 10.6%
Nunjucks 3.6%
Shell 1.8%
SQL 1.3%
1/**2 * ─────────────────────────────────────────────3 * SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : src/preview/code.mjs8 * Purpose : Shiki syntax highlighting + markdown-it GFM rendering9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { createHighlighter } from 'shiki';14import MarkdownIt from 'markdown-it';15import anchor from 'markdown-it-anchor';16import taskLists from 'markdown-it-task-lists';1718/** Extension → shiki language id. Anything unknown highlights as plaintext. */19export const EXT_LANG = {20 js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'jsx',21 ts: 'typescript', tsx: 'tsx', py: 'python', rb: 'ruby', go: 'go', rs: 'rust',22 c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp', cs: 'csharp', java: 'java',23 kt: 'kotlin', swift: 'swift', m: 'objective-c', php: 'php', sh: 'shellscript',24 bash: 'shellscript', zsh: 'shellscript', fish: 'fish', ps1: 'powershell',25 sql: 'sql', html: 'html', htm: 'html', xml: 'xml', css: 'css', scss: 'scss',26 less: 'less', json: 'json', jsonc: 'jsonc', yaml: 'yaml', yml: 'yaml',27 toml: 'toml', ini: 'ini', conf: 'ini', env: 'ini', dockerfile: 'docker',28 makefile: 'make', mk: 'make', cmake: 'cmake', gradle: 'groovy', groovy: 'groovy',29 lua: 'lua', r: 'r', jl: 'julia', scala: 'scala', pl: 'perl', ex: 'elixir',30 exs: 'elixir', erl: 'erlang', hs: 'haskell', ml: 'ocaml', clj: 'clojure',31 vue: 'vue', svelte: 'svelte', astro: 'astro', graphql: 'graphql', gql: 'graphql',32 proto: 'proto', tf: 'hcl', hcl: 'hcl', nix: 'nix', zig: 'zig', dart: 'dart',33 sol: 'solidity', asm: 'asm', s: 'asm', vim: 'viml', diff: 'diff', patch: 'diff',34 tex: 'latex', bib: 'latex', md: 'markdown', njk: 'html', csv: 'csv', tsv: 'tsv',35 txt: 'plaintext', log: 'log', bat: 'bat', cmd: 'bat', vb: 'vb', fs: 'fsharp',36 nim: 'nim', cr: 'crystal', d: 'd', pas: 'pascal', f90: 'fortran-free-form',37 cob: 'cobol', ada: 'ada', prisma: 'prisma', cue: 'cue', http: 'http',38};3940let highlighterPromise = null;4142async function getHighlighter() {43 if (!highlighterPromise) {44 highlighterPromise = createHighlighter({45 themes: ['dark-plus', 'light-plus'],46 langs: ['javascript', 'typescript', 'python', 'json', 'yaml', 'markdown', 'shellscript', 'html', 'css', 'sql'],47 });48 }49 return highlighterPromise;50}5152/** Resolve a shiki language for a filename (plaintext fallback). */53export function langForFile(name) {54 const base = name.toLowerCase();55 if (base === 'dockerfile') return 'docker';56 if (base === 'makefile') return 'make';57 const ext = base.includes('.') ? base.split('.').pop() : base;58 return EXT_LANG[ext] ?? 'plaintext';59}6061/**62 * Highlight source text → themed HTML (dual theme via CSS variables).63 * Unknown or unloadable languages degrade to plaintext, never throw.64 */65export async function highlightCode(text, name, { maxBytes = 2_000_000 } = {}) {66 const clipped = text.length > maxBytes;67 const source = clipped ? text.slice(0, maxBytes) : text;68 const hl = await getHighlighter();69 let lang = langForFile(name);70 if (!hl.getLoadedLanguages().includes(lang)) {71 try {72 await hl.loadLanguage(lang);73 } catch {74 lang = 'plaintext';75 }76 }77 const html = hl.codeToHtml(source, {78 lang,79 themes: { dark: 'dark-plus', light: 'light-plus' },80 defaultColor: false,81 });82 return { html, lang, clipped };83}8485const md = new MarkdownIt({ html: false, linkify: true, typographer: true })86 .use(anchor, { permalink: anchor.permalink.headerLink({ safariReaderFix: true }) })87 .use(taskLists, { enabled: false });8889// Fenced code inside markdown gets a plain <pre> with a language class;90// mermaid fences are tagged for client-side rendering.91md.renderer.rules.fence = (tokens, idx) => {92 const token = tokens[idx];93 const info = (token.info || '').trim().split(/\s+/)[0];94 const escaped = md.utils.escapeHtml(token.content);95 if (info === 'mermaid') {96 return `<pre class="mermaid-block" data-mermaid>${escaped}</pre>\n`;97 }98 return `<pre class="code-fence"><code class="language-${md.utils.escapeHtml(info || 'text')}">${escaped}</code></pre>\n`;99};100101/** Render GFM markdown → sanitized HTML (raw HTML disabled). */102export function renderMarkdown(text) {103 return md.render(text ?? '');104}105