Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
6.8 kB · 143 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144// Replaces Docusaurus' Prism fenced code blocks with Expressive Code + the starlight-codeblocks preset.// Only fenced blocks in Markdown/MDX are affected; JSX <CodeBlock> still renders with Prism.//// Load this with a native require (see docusaurus.config.ts): Docusaurus loads its config with jiti,// which transpiles Shiki's lazy theme/grammar import()s to CJS and breaks them.import { createHash } from 'node:crypto';import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';import rehypeExpressiveCode, { createRenderer, MetaOptions } from 'rehype-expressive-code';import { bundledLanguagesInfo } from 'shiki/langs';import { pluginCodeblocks } from 'starlight-codeblocks/expressive-code';
// Same options as starlight-codeblocks' `codeblocks({...})`, e.g. `expandable: { lines: 12, auto: 30 }`.// See https://ewels.github.io/starlight-codeblocks/reference/configuration/const codeblocksOptions = { // allow `// [!code ...]` notation in plain json blocks // and `<-- [!ref] ...` or `# [!ref] ...` annotations in plain text blocks notation: { comments: { json: ['//'], text: ['<--', '#'], caddyfile: ['#'] } }, placeholders: { storage: 'session' }};
// Caddyfile grammar from https://github.com/caddyserver/vscode-caddyfile (MIT, see caddyfile.LICENSE).// Shiki has no bundled Caddyfile language. The grammar's heredoc rules include css/js/json/html/xml;// those are not declared as embeddedLangs (Shiki would require them preloaded), so heredoc bodies render unhighlighted.const caddyfile = { ...JSON.parse(readFileSync(new URL('./caddyfile.tmLanguage.json', import.meta.url), 'utf8')), name: 'caddyfile', aliases: ['Caddyfile', 'caddy'],};
// Expressive Code's own options (defaultProps, styleOverrides, frames, ...) go here.// See https://expressive-code.com/reference/configuration/export const ecOptions = { // match the Prism themes used previously (github / dracula) themes: ['dracula', 'github-light'], // follow Docusaurus' <html data-theme="dark|light"> instead of prefers-color-scheme themeCssSelector: (theme) => `[data-theme='${theme.type}']`, useDarkModeMediaQuery: false, shiki: { langs: [caddyfile] }, plugins: pluginCodeblocks(codeblocksOptions),};
// One renderer for the whole site. Its page-level CSS/JS are written once as static files// by the Docusaurus plugin below, so the rehype plugin must not inline them per page.const renderer = createRenderer(ecOptions);
// Mark EC output as explicit JSX so MDX does not swap its <pre>/<code>/<a>... for @theme/MDXComponentsconst rehypeEcExplicitJsx = () => (tree) => { const mark = (node, inside) => { const isEc = node.type === 'element' && node.properties?.className?.includes?.('expressive-code'); if (node.type === 'element' && (inside || isEc)) node.data = { ...node.data, _mdxExplicitJsx: true }; for (const child of node.children ?? []) mark(child, inside || isEc); }; mark(tree, false);};
// Port of starlight-codeblocks' `:::code-tabs` transform (src/satteri/index.ts `codeTabs()`), which// it only wires up inside its Astro integration. Docusaurus already parses directives (remark-directive).// Wraps the fences in a `.scb-tabs` div and gives each the `scbTabs` meta that pluginCodeTabs() reads.const languageName = (lang) => { if (!lang) return 'Plain text'; return bundledLanguagesInfo.find((info) => info.id === lang || info.aliases?.includes(lang))?.name ?? lang;};// `diff lang="py"` renders as Pythonconst fenceLanguage = (code) => (code.lang === 'diff' && new MetaOptions(code.meta ?? '').getString('lang')) || code.lang;
const remarkCodeTabs = () => (tree, file) => { const visit = (node) => { node.children?.forEach((child, i) => { if (child.type === 'containerDirective' && child.name === 'code-tabs') node.children[i] = codeTabs(child, file.path); else visit(child); }); }; visit(tree);};
function codeTabs(node, path) { const codes = node.children; if (codes.length === 0 || codes.some((child) => child.type !== 'code')) { throw new Error(`${path}: \`:::code-tabs\` can contain only fenced code blocks, and needs at least one.`); } const labels = codes.map((code) => new MetaOptions(code.meta ?? '').getString('label') ?? languageName(fenceLanguage(code))); const repeated = labels.find((label, i) => labels.indexOf(label) !== i); if (repeated !== undefined) { throw new Error(`${path}: two variants in a \`:::code-tabs\` have the label "${repeated}". Give each variant a different \`label="…"\`.`); } const control = node.attributes?.control ?? codeblocksOptions.codeTabs?.control ?? 'tabs'; if (control !== 'tabs' && control !== 'menu') { throw new Error(`${path}: \`:::code-tabs\` has \`control="${control}"\`. Use \`control="tabs"\` or \`control="menu"\`.`); } return { type: 'paragraph', data: { hName: 'div', hProperties: { className: ['scb-tabs'], dataScbCodeTabs: node.attributes?.sync ?? '', dataScbControl: control, dataScbLabels: JSON.stringify(labels), }, }, children: codes.map((code, index) => ({ ...code, meta: `${code.meta ?? ''} scbTabs="${encodeURIComponent(JSON.stringify({ index, labels, control }))}"`.trim(), })), };}
export const remarkPlugins = [remarkCodeTabs];
export const rehypePlugins = [ [rehypeExpressiveCode, { ...ecOptions, customCreateRenderer: async () => ({ ...(await renderer), baseStyles: '', themeStyles: '', jsModules: [] }), }], rehypeEcExplicitJsx,];
// Add this to the site's `staticDirectories` so Docusaurus serves (dev) and copies (build) itexport const ecStaticDir = new URL('../../.docusaurus/expressive-code/', import.meta.url).pathname;
// Writes the page-level CSS/JS once as content-hashed static files that every page links toexport async function expressiveCodePlugin(context) { const { baseStyles, themeStyles, jsModules } = await renderer; const files = { css: baseStyles + themeStyles, js: jsModules.join('\n') }; rmSync(ecStaticDir, { recursive: true, force: true }); mkdirSync(`${ecStaticDir}ec`, { recursive: true }); const urls = {}; for (const [ext, content] of Object.entries(files)) { const name = `ec/ec.${createHash('sha256').update(content).digest('hex').slice(0, 8)}.${ext}`; writeFileSync(ecStaticDir + name, content); urls[ext] = context.baseUrl + name; } return { name: 'expressive-code', getClientModules: () => [new URL('./client.mjs', import.meta.url).pathname], injectHtmlTags: () => ({ headTags: [{ tagName: 'link', attributes: { rel: 'stylesheet', href: urls.css } }], postBodyTags: [{ tagName: 'script', attributes: { type: 'module', src: urls.js } }], }), };}