Skip to content

Commit 724185a

Browse files
authored
Expand functionality to better support theme variable exports (#21)
1 parent d7f5699 commit 724185a

4 files changed

Lines changed: 353 additions & 78 deletions

File tree

cli.js

Lines changed: 126 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,138 @@
11
#!/usr/bin/env node
2+
import {exit} from 'node:process';
23
import meow from 'meow';
34
import githubMarkdownCss from './index.js';
45

5-
const cli = meow(`
6-
Usage
7-
github-markdown-css > <filename>
8-
9-
Options
10-
--type Theme name: 'light', 'dark', 'auto' or other --list values.
11-
'auto' means using the media query (prefers-color-scheme)
12-
to switch between the 'light' and 'dark' theme.
13-
--list List available themes.
14-
15-
Examples
16-
$ github-markdown-css --list
17-
light
18-
dark
19-
dark_dimmed
20-
dark_high_contrast
21-
dark_colorblind
22-
light_colorblind
23-
`, {
24-
importMeta: import.meta,
25-
flags: {
26-
type: {
27-
type: 'string',
28-
},
29-
list: {
30-
type: 'boolean',
6+
const cli = meow(
7+
`
8+
Usage
9+
github-markdown-css > <filename>
10+
11+
Options
12+
--list List available themes
13+
14+
Set theme:
15+
-l, --light Light theme name from --list values
16+
-d, --dark Dark theme name from --list values
17+
-t, --type, --theme Theme name: 'auto', light', 'dark', or another from --list values.
18+
'auto' means using the media query (prefers-color-scheme)
19+
to switch between the 'light' and 'dark' theme.
20+
21+
Output options:
22+
--preserveVars Preserve variables in the output. Only applies if light
23+
and dark themes match or if type is not 'auto'
24+
--onlyStyle Only output the styles, forces preserveVars on
25+
--onlyVars Only output the variables for the specified themes
26+
--rootSelector Specify the root selector when outputting styles, default '.markdown-body'
27+
28+
Examples
29+
$ github-markdown-css --list
30+
light
31+
dark
32+
dark_dimmed
33+
dark_high_contrast
34+
dark_colorblind
35+
light_colorblind
36+
37+
$ github-markdown-css --light=light --dark=dark
38+
[CSS with variable blocks for 'light' and 'dark' themes]
39+
40+
$ github-markdown-css --theme=dark_dimmed --onlyVars
41+
[CSS with single variable block for 'dark_dimmed' theme with no element styles]
42+
43+
$ github-markdown-css --onlyStyles
44+
[CSS with only element styles using variables but no variables set.
45+
Use in combination with output from setting --onlyVars]
46+
`,
47+
{
48+
importMeta: import.meta,
49+
flags: {
50+
theme: {
51+
alias: ['t', 'type'],
52+
type: 'string',
53+
},
54+
light: {
55+
alias: 'l',
56+
type: 'string',
57+
},
58+
dark: {
59+
alias: 'd',
60+
type: 'string',
61+
},
62+
list: {
63+
type: 'boolean',
64+
},
65+
onlyStyle: {
66+
type: 'boolean',
67+
},
68+
onlyVars: {
69+
type: 'boolean',
70+
},
71+
preserveVars: {
72+
type: 'boolean',
73+
},
74+
rootSelector: {
75+
type: 'string',
76+
},
3177
},
3278
},
33-
});
79+
);
3480

3581
(async () => {
36-
const {type, list} = cli.flags;
82+
const {theme, list, preserveVars, onlyStyle, onlyVars, rootSelector} = cli.flags;
83+
let {light, dark} = cli.flags;
84+
85+
/*
86+
* | Theme | Light | Dark | Outcome |
87+
* | ----- | ----- | ---- | ---------------------------------- |
88+
* | ✓ | | | Single mode, use Theme |
89+
* | ✓ | ✓ | | Not allowed, can't determine theme |
90+
* | ✓ | | ✓ | Not allowed, can't determine theme |
91+
* | ✓ | ✓ | ✓ | Not allowed, can't determine theme |
92+
* | | | | Auto, default themes |
93+
* | | ✓ | | Single mode, use Light |
94+
* | | | ✓ | Single mode, use Dark |
95+
* | | ✓ | ✓ | Auto, use Light and Dark |
96+
* | | ✓ | ✓ | Single mode if Light === Dark |
97+
* | auto | | | Auto, default themes |
98+
* | auto | ✓ | | Auto, use Light, default dark |
99+
* | auto | | ✓ | Auto, use Dark, default light |
100+
* | auto | ✓ | ✓ | Auto, use Light and Dark |
101+
* | auto | ✓ | ✓ | Single mode if Light === Dark |
102+
*/
103+
104+
// Use "single" mode when type is a theme name other than 'auto'
105+
if (theme && theme !== 'auto') {
106+
if (light || dark) {
107+
console.error('You may not specify light and/or dark unless type/theme is set to "auto"');
108+
exit(1);
109+
}
110+
111+
light = theme;
112+
dark = theme;
113+
}
114+
115+
// If only light or dark was specified set the other to force "single mode"
116+
if (!theme && light && !dark) {
117+
dark = light;
118+
} else if (!theme && !light && dark) {
119+
light = dark;
120+
}
37121

38-
let light = type;
39-
let dark = type;
40-
if (type === 'auto') {
41-
light = 'light';
42-
dark = 'dark';
122+
if (rootSelector === '') {
123+
console.error('--rootSelector cannot be an empty string');
124+
exit(1);
43125
}
44126

45-
console.log(await githubMarkdownCss({light, dark, list}));
127+
console.log(
128+
await githubMarkdownCss({
129+
light,
130+
dark,
131+
list,
132+
preserveVariables: preserveVars,
133+
onlyStyles: onlyStyle,
134+
onlyVariables: onlyVars,
135+
rootSelector,
136+
}),
137+
);
46138
})();

index.js

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -283,15 +283,62 @@ function applyColors(colors, rules) {
283283
return rules;
284284
}
285285

286-
async function getCSS({light = 'light', dark = 'dark', list = false} = {}) {
286+
/**
287+
* Extract markdown styles from github.com
288+
*
289+
* If the `light` and `dark` themes are different the CSS returned will include
290+
* `prefers-color-scheme` blocks for light and dark that match the specified
291+
* `light` and `dark` themes (considered "auto" mode). This mode will always
292+
* `preserveVars` as they are necessary for the `prefers-color-scheme` blocks
293+
*
294+
* If the `light` and `dark` themes are equal the output will only contain one
295+
* theme (considered "single" mode)
296+
*
297+
* In "single" mode the output will apply the values of all variables to the
298+
* rules themselves.The output will not contain any `var(--variable)` statements.
299+
* You can disable this by setting `preserveVariables` to true
300+
*
301+
* @param {Object} options optional options object
302+
* @param {string} [options.light=light] The theme to use for light theme
303+
* @param {string} [options.dark=dark] The theme to use for dark theme
304+
* @param {boolean} [options.list=false] If `true` will return a list of available themes instead of the CSS
305+
* @param {boolean} [options.preserveVariables=false] If `true` will preserve the block of variables for a given theme even if only exporting one theme. By default variables are applied to the rules themselves and the resulting CSS will not contain any `var(--variable)`
306+
* @param {boolean} [options.onlyVariables=false] Only output the color variables part of the css. forces `preserveVariables` to be `true`
307+
* @param {boolean} [options.onlyStyles=false] Only output the style part of the css without any variables. forces `preserveVariables` to be `true` and ignores the theme values. Useful to get the base styles to use multiple themes
308+
* @param {string} [options.rootSelector=.markdown-body] Set the root selector of the rendered markdown body as it should appear in the output css. Defaults to `.markdown-body`
309+
*/
310+
async function getCSS({
311+
light = 'light',
312+
dark = 'dark',
313+
list = false,
314+
preserveVariables = false,
315+
onlyVariables = false,
316+
onlyStyles = false,
317+
rootSelector = '.markdown-body',
318+
} = {}) {
319+
if (onlyVariables && onlyStyles) {
320+
// Would result in an empty output
321+
throw new Error('May not specify onlyVariables and onlyStyles at the same time');
322+
}
323+
324+
if (rootSelector === '') {
325+
throw new Error('rootSelector may not be an empty string');
326+
}
327+
328+
if (onlyVariables || onlyStyles) {
329+
preserveVariables = true;
330+
}
331+
287332
const body = await cachedGot('https://github.com');
333+
// Get a list of all css links on the page
288334
const links = unique(body.match(/(?<=href=").+?\.css/g));
289335
const contents = await Promise.all(links.map(url => cachedGot(url)));
290336

291337
const colors = [];
292338
let rules = [];
293339

294340
for (const [url, cssText] of zip(links, contents)) {
341+
// Get the name of a css file without the cache prevention number
295342
const match = url.match(/(?<=\/)\w+(?=-\w+\.css$)/);
296343
if (!match) {
297344
continue;
@@ -300,13 +347,15 @@ async function getCSS({light = 'light', dark = 'dark', list = false} = {}) {
300347
const [name] = match;
301348
const ast = css.parse(cssText);
302349

350+
// If it's a theme variable file extract colors, otherwise extract style
303351
if (/^(light|dark)/.test(name)) {
304352
extractColors(colors, name, ast);
305353
} else {
306354
extractStyles(rules, ast);
307355
}
308356
}
309357

358+
// If asked to list return the list of themes we've discovered
310359
if (list) {
311360
return colors.map(({name}) => name).join('\n');
312361
}
@@ -319,6 +368,7 @@ async function getCSS({light = 'light', dark = 'dark', list = false} = {}) {
319368

320369
({rules} = classifyRules(rules));
321370

371+
// Find all variables used across all styles
322372
const usedVariables = new Set(rules.flatMap(rule => rule.declarations.flatMap(({value}) => {
323373
let match = /var\((?<name>[-\w]+?)\)/.exec(value)?.groups.name;
324374
if (match === '--color-text-primary') {
@@ -331,48 +381,78 @@ async function getCSS({light = 'light', dark = 'dark', list = false} = {}) {
331381
const colorSchemeLight = {type: 'declaration', property: 'color-scheme', value: 'light'};
332382
const colorSchemeDark = {type: 'declaration', property: 'color-scheme', value: 'dark'};
333383

334-
if (light === dark) {
335-
rules = applyColors(colors[light], rules);
384+
const filterColors = (declarations, usedVariables) =>
385+
declarations.filter(({property}) => usedVariables.has(property));
386+
387+
if (onlyVariables) {
388+
rules = [];
389+
}
336390

337-
if (light.startsWith('dark')) {
338-
rules[0].declarations.unshift(colorSchemeDark);
391+
if (!onlyStyles) {
392+
if (light === dark) {
393+
if (preserveVariables) {
394+
rules.unshift({
395+
type: 'rule',
396+
selectors: ['.markdown-body', `[data-theme="${light}"]`],
397+
comment: light,
398+
declarations: [
399+
{type: 'comment', comment: light},
400+
light.startsWith('dark') ? colorSchemeDark : colorSchemeLight,
401+
...filterColors(colors[light], usedVariables),
402+
],
403+
});
404+
} else {
405+
rules = applyColors(colors[light], rules);
406+
407+
if (light.startsWith('dark')) {
408+
rules[0].declarations.unshift(colorSchemeDark);
409+
}
410+
411+
rules.unshift({type: 'comment', comment: light});
412+
}
413+
} else {
414+
rules.unshift({
415+
type: 'media',
416+
media: '(prefers-color-scheme: light)',
417+
rules: [{
418+
type: 'rule',
419+
selectors: ['.markdown-body', `[data-theme="${light}"]`],
420+
declarations: [
421+
{type: 'comment', comment: light},
422+
light.startsWith('dark') ? colorSchemeDark : colorSchemeLight,
423+
...filterColors(colors[light], usedVariables),
424+
],
425+
}],
426+
});
427+
428+
rules.unshift({
429+
type: 'media',
430+
media: '(prefers-color-scheme: dark)',
431+
rules: [{
432+
type: 'rule',
433+
selectors: ['.markdown-body', `[data-theme="${dark}"]`],
434+
declarations: [
435+
{type: 'comment', comment: dark},
436+
dark.startsWith('light') ? colorSchemeLight : colorSchemeDark,
437+
...filterColors(colors[dark], usedVariables),
438+
],
439+
}],
440+
});
339441
}
340-
} else {
341-
const filterColors = (declarations, usedVariables) => declarations.filter(({property}) => usedVariables.has(property));
342-
343-
rules.unshift({
344-
type: 'media',
345-
media: '(prefers-color-scheme: light)',
346-
rules: [{
347-
type: 'rule',
348-
selectors: ['.markdown-body'],
349-
declarations: [
350-
light.startsWith('dark') ? colorSchemeDark : colorSchemeLight,
351-
...filterColors(colors[light], usedVariables),
352-
],
353-
}],
354-
});
355-
356-
rules.unshift({
357-
type: 'media',
358-
media: '(prefers-color-scheme: dark)',
359-
rules: [{
360-
type: 'rule',
361-
selectors: ['.markdown-body'],
362-
declarations: [
363-
dark.startsWith('light') ? colorSchemeLight : colorSchemeDark,
364-
...filterColors(colors[dark], usedVariables),
365-
],
366-
}],
367-
});
368442
}
369443

370444
let string = css.stringify({type: 'stylesheet', stylesheet: {rules}});
371445

372446
const rootBegin = string.indexOf('\n.markdown-body {');
373447
const rootEnd = string.indexOf('}', rootBegin) + 2;
374448

375-
string = string.slice(0, rootEnd) + manuallyAddedStyle + string.slice(rootEnd);
449+
if (!onlyVariables) {
450+
string = string.slice(0, rootEnd) + manuallyAddedStyle + string.slice(rootEnd);
451+
}
452+
453+
if (rootSelector !== '.markdown-body') {
454+
string = string.replaceAll('.markdown-body', rootSelector);
455+
}
376456

377457
return string;
378458
}

0 commit comments

Comments
 (0)