ReviewOS

also looking at this

stacks/ts-syntax-highlighter

fix: browser-safe config loading and correct dual-theme token colors

#477
Open jmadore-payfacto wants to merge fix/browser-safe-config-and-dual-theme-colors into main
6 files +237 -21
packages/ts-syntax-highlighter/src/dual-theme.tsmodified+40-11
Changes to packages/ts-syntax-highlighter/src/dual-theme.ts
@@ -9,33 +9,36 @@ export interface DualThemeOptions extends RenderOptions {
99
1010/**
1111 * Render code with dual theme support (light and dark)
12 *
13 * The two themes render the same markup - same tokens, same scopes, same
14 * classes - so the light pass's `html` describes both. What differs is which
15 * stylesheet is active: both renders run in `colorViaClass` mode so neither
16 * bakes a color into `style="..."` (which no later selector could override),
17 * then the dark stylesheet is rescoped under `selector` and left to override
18 * the light one, which stays the page's unscoped default.
1219 */
1320export function renderDualTheme(
1421 tokens: TokenLine[],
1522 options: DualThemeOptions,
1623): RenderedCode {
1724 const { lightTheme, darkTheme, selector = 'html', ...renderOptions } = options
25 const colorViaClassOptions = { ...renderOptions, colorViaClass: true }
1826
19 // Render with both themes
2027 const lightRenderer = new Renderer(lightTheme)
2128 const darkRenderer = new Renderer(darkTheme)
2229
23 const lightResult = lightRenderer.render(tokens, renderOptions)
24 const darkResult = darkRenderer.render(tokens, renderOptions)
30 const lightResult = lightRenderer.render(tokens, colorViaClassOptions)
31 const darkResult = darkRenderer.render(tokens, colorViaClassOptions)
32
33 const darkPrefixes = [`${selector}.dark`, `${selector}[data-theme="dark"]`]
34 const scopedDarkRules = darkResult.css ? scopeCssRules(darkResult.css, darkPrefixes) : ''
2535
26 // Combine CSS with media query
2736 const combinedCSS = `
2837/* Light theme (default) */
2938${lightResult.css}
3039
3140/* Dark theme */
32${selector}.dark .syntax,
33${selector}[data-theme="dark"] .syntax,
34@media (prefers-color-scheme: dark) {
35 ${selector}:not(.light) .syntax {
36 ${darkResult.css?.split('\n').filter(line => line.includes(':')).join('\n ')}
37 }
38}
41${scopedDarkRules}
3942`.trim()
4043
4144 return {
@@ -46,6 +49,32 @@ ${selector}[data-theme="dark"] .syntax,
4649 }
4750}
4851
52/**
53 * Prepend each of `prefixes` to every selector in a flat stylesheet (no
54 * nested at-rules - `generateCSS`'s output never has any, so a rule is just
55 * whatever sits between one `{...}` pair, comma-separated selectors and all).
56 */
57function scopeCssRules(css: string, prefixes: string[]): string {
58 return css
59 .split('}')
60 .map(chunk => chunk.trim())
61 .filter(chunk => chunk.length > 0)
62 .map((chunk) => {
63 const braceIndex = chunk.indexOf('{')
64 if (braceIndex === -1) {
65 return null
66 }
67 const selectors = chunk.slice(0, braceIndex).trim().split(',').map(s => s.trim())
68 const body = chunk.slice(braceIndex)
69 const scopedSelector = prefixes
70 .flatMap(prefix => selectors.map(sel => `${prefix} ${sel}`))
71 .join(',\n')
72 return `${scopedSelector} ${body}\n}`
73 })
74 .filter((rule): rule is string => rule !== null)
75 .join('\n\n')
76}
77
4978/**
5079 * Create a dual theme renderer
5180 */