diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1250f57 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +## Adding a New Theme + +To add your theme, copy your CSS file into the `themes` directory. The name of your file has to match the format `prism-{your-theme-id}.css`. Make sure your theme follows your guidelines. + +Next, you have to register your theme in `themes.json`. Add a new entry like this: + +```json +"{your-theme-id}": { + "title": "Your title", + "owner": "Your GitHub user name" +} +``` + +Additionally, you can also specify who the original owner is (`originalOwner`) or on what theme your theme is based (`basedOn`). +You aren't required to give a GitHub user name. You can also use a different name and link where the link is optional: + +```json +"{your-theme-id}": { + "title": "Your title", + "owner": { + "name": "My real name", + "link": "https://github.com/someUserName" + }, + "originalOwner": { + "name": "Famous artist" + } +} +``` + +At last, you have to rebuild the project by running: + +```bash +npm i && npm run build +``` + +Before making a pull request, you can use the following command to verify that all our checks pass and that your theme follows all our guidelines: + +```bash +npm test +``` + +Thank you so much for contributing!! + + +## Theme guidelines and requirements + +Your theme will be used by Prism and its plugins which can also add CSS rules. +These guidelines will make sure that your theme is compatible with Prism and its plugins. + +### Font size of code blocks + +Some of Prism's plugins assume that the `pre` and `code` elements have the same font size. + +To make sure that this is that case, all themes are __required__ to either include a rule which ensures that the property `font-size: 1em` is applied to all elements which match `pre > code[class*="language-"]` or to not change the font size of code blocks. + +If you want to change the font size of code blocks, use the `font-size` property on the `pre` element. Example: + +```css +pre[class*="language-"] { + font-size: 90%; +} +``` + +### Overflow of `pre` elements + +The `overflow` property of `pre` elements is __required__ to be left unset or set to `auto`. + +This is required because other `overflow` values will cause problems with layouts based on `float` or flexboxes. + +One notable exception is the original Coy theme because its shadows are impossible to implement otherwise. diff --git a/README.md b/README.md index 9ca5c0b..e892095 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,7 @@ To use one of the themes, just include the theme's CSS file in your page. Exampl ## Adding a New Theme -To add your own theme, copy it into the `themes` directory and add your themes to the list of available themes in the readme. -The links for your themes have to be `themes/prism-.css` for the theme itself and `screenshots/prism-.png` for the screenshot. - -The screenshot will be created for you by running the following command: - -```bash -npm i && npx gulp screenshot -``` - -Before making a pull request, you can the following command to verify that all checks pass: - -```bash -npm test -``` - -Thank you so much for contributing!! +See our [Contributing](CONTRIBUTING.md) guide. ## Available themes @@ -54,7 +39,7 @@ Thank you so much for contributing!! [![Xonokai](screenshots/prism-xonokai.png)](themes/prism-xonokai.css) * [__Ateliersulphurpool-light__](themes/prism-base16-ateliersulphurpool.light.css) (by [Bram de Haan](https://github.com/atelierbram))
-[![Ateliersulpherpool-light](screenshots/prism-base16-ateliersulphurpool.light.png)](themes/prism-base16-ateliersulphurpool.light.css) +[![Ateliersulphurpool-light](screenshots/prism-base16-ateliersulphurpool.light.png)](themes/prism-base16-ateliersulphurpool.light.css) * [__Hopscotch__](themes/prism-hopscotch.css) (by [Jan T. Sott](https://github.com/idleberg))
[![Hopscotch](screenshots/prism-hopscotch.png)](themes/prism-hopscotch.css) @@ -83,14 +68,14 @@ Thank you so much for contributing!! * [__VS__](themes/prism-vs.css) (by [andrewlock](https://github.com/andrewlock))
[![VS](screenshots/prism-vs.png)](themes/prism-vs.css) -* [__Darcula__](themes/prism-darcula.css) (by [service-paradis](https://github.com/service-paradis), based on Jetbrains Darcula theme)
+* [__Darcula__](themes/prism-darcula.css) (by [service-paradis](https://github.com/service-paradis), based on Jetbrain's Darcula theme)
[![Darcula](screenshots/prism-darcula.png)](themes/prism-darcula.css) * [__a11y Dark__](themes/prism-a11y-dark.css) (by [ericwbailey](https://github.com/ericwbailey))
[![a11y Dark](screenshots/prism-a11y-dark.png)](themes/prism-a11y-dark.css) * [__Dracula__](themes/prism-dracula.css) (by [Byverdu](https://github.com/byverdu))
-[![a11y Dark](screenshots/prism-dracula.png)](themes/prism-dracula.css) +[![Dracula](screenshots/prism-dracula.png)](themes/prism-dracula.css) * [__Synthwave '84__](themes/prism-synthwave84.css) (originally by [Robb Owen](https://github.com/robb0wen), adapted by [Marc Backes](https://github.com/themarcba))
[![Synthwave '84](screenshots/prism-synthwave84.png)](themes/prism-synthwave84.css) diff --git a/gulpfile.js b/gulpfile.js index 2f5a680..5f9ed72 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,16 +2,33 @@ const fs = require('fs').promises; const { parallel } = require('gulp'); const captureWebsite = require('capture-website'); const path = require('path'); +const { JSDOM } = require('jsdom'); + + +/** + * @type {Object} + * + * @typedef ThemesEntry + * @property {string} title + * @property {Entity} owner + * @property {Entity} [originalOwner] + * @property {Entity} [basedOn] + * + * @typedef {string | { name: string; link?: string }} Entity + */ +const themeCatalog = require('./themes.json').themes; const themesDir = path.join(__dirname, 'themes'); const screenshotDir = path.join(__dirname, 'screenshots'); +const getThemePath = theme => path.join(themesDir, `prism-${theme}.css`); +const getScreenshotPath = theme => path.join(screenshotDir, `prism-${theme}.png`); /** * Returns the names of all themes. This includes the `prism-` prefix. */ async function getThemes() { - return (await fs.readdir(themesDir)).map(f => (/^.+(?=\.css$)/.exec(f) || [''])[0]).filter(f => f); + return Object.keys(themeCatalog); } /** @@ -41,7 +58,7 @@ async function screenshotMissingThemes() { * @param {boolean} overwrite */ async function screenshotTheme(theme, overwrite) { - const file = `${screenshotDir}/${theme}.png`; + const file = getScreenshotPath(theme); if (await fs.stat(file).then(s => s.isFile()).catch(() => false)) { if (overwrite) { @@ -56,39 +73,183 @@ async function screenshotTheme(theme, overwrite) { scaleFactor: 1, element: 'pre', styles: [ - await fs.readFile(`${themesDir}/${theme}.css`, 'utf-8') + await fs.readFile(getThemePath(theme), 'utf-8') ] }); } +/** + * Updates the "Available themes" section in `README.md`. + */ +async function updateReadme() { + const themes = await getThemes(); + + /** + * Returns the credit string of a theme. + * + * @param {ThemesEntry} entry + * @returns {string} + */ + function getCredit(entry) { + if (entry.basedOn) { + return `by ${printEntity(entry.owner)}, based on ${printEntity(entry.basedOn)}`; + } else if (entry.originalOwner) { + return `originally by ${printEntity(entry.originalOwner)}, adapted by ${printEntity(entry.owner)}`; + } else { + return `by ${printEntity(entry.owner)}`; + } + } + /** + * @param {Entity} entity + */ + function printEntity(entity) { + if (typeof entity === 'string') { + return `[${entity}](https://github.com/${entity})`; + } else if (entity.link) { + return `[${entity.name}](${entity.link})`; + } else { + return entity.name; + } + } + + const md = themes.map(theme => { + const css = `themes/prism-${theme}.css`; + const screenshot = `screenshots/prism-${theme}.png`; + + const entry = themeCatalog[theme]; + const title = entry.title; + const credit = getCredit(entry); + + return `* [__${title}__](${css}) (${credit})
\n[![${title}](${screenshot})](${css})`; + }).join('\n\n'); + + const readmePath = path.join(__dirname, 'README.md'); + let readme = await fs.readFile(readmePath, 'utf-8'); + readme = readme.replace(/(## Available themes)[\s\S]*/, (m, header) => { + return `${header}\n\n${md}\n`; + }); + + await fs.writeFile(readmePath, readme, 'utf-8'); +} + /** * Checks that all themes have a screenshot. */ async function checkScreenshots() { - for (const theme of await getThemes()) { - const file = `${screenshotDir}/${theme}.png`; - if (!await fs.stat(file).then(s => s.isFile()).catch(() => false)) { - throw new Error(`The theme "${theme}" doesn't have a screenshot.`); + const themes = new Set(await getThemes()); + const screenshots = new Set(await fs.readdir(screenshotDir)); + screenshots.delete('code.html'); + + for (const theme of themes) { + if (!screenshots.delete(`prism-${theme}.png`)) { + throw new Error(`The theme "${theme}" does not have a screenshot.`); } } + + if (screenshots.size > 0) { + throw new Error(`There are screenshots without a theme: "${[...screenshots].join('", "')}"`); + } } /** - * Checks that all themes are in the list of available themes. + * Checks that all themes have a CSS file. */ -async function checkAvailableThemes() { - const readme = await fs.readFile(path.join(__dirname, 'README.md'), 'utf-8'); +async function checkCSS() { + const themes = new Set(await getThemes()); + const cssFiles = new Set(await fs.readdir(themesDir)); - for (const theme of await getThemes()) { - if (!readme.includes(theme + ".css")) { - throw new Error(`The theme "${theme}" is not included in the list of available themes.`); + for (const theme of themes) { + if (!cssFiles.delete(`prism-${theme}.css`)) { + throw new Error(`The theme "${theme}" does not have a screenshot.`); } - if (!readme.includes(theme + ".png")) { - throw new Error(`The screenshot of "${theme}" is not included in the list of available themes.`); + } + + if (cssFiles.size > 0) { + throw new Error(`There are CSS files without a theme: "${[...cssFiles].join('", "')}"`); + } +} + +/** + * @type {Object>} + */ +const requirements = { + 'pre': { + /* Code block: pre */ + + 'overflow': 'auto' + }, + 'pre > code': { + /* Code block: code */ + + 'font-size': /^(?:1em|)$/ + }, + ':not(pre) > code': { + /* Inline: code */ + + // none + } +}; + +async function checkRequirements() { + const getSource = css => { + return ` + + + + + + + +
var a = 0;
+ + + a++ + + + `; + }; + + let pass = true; + + for (const theme of await getThemes()) { + const dom = new JSDOM(getSource(await fs.readFile(getThemePath(theme), 'utf-8'))); + + for (const selector in requirements) { + const properties = requirements[selector]; + + for (const element of dom.window.document.querySelectorAll(selector)) { + const style = dom.window.getComputedStyle(element); + + for (const property in properties) { + const expected = properties[property]; + const actual = style[property]; + + let valid; + if (typeof expected === 'string') { + valid = expected === actual; + } else { + valid = expected.test(actual); + } + + if (!valid) { + pass = false; + console.error(`${theme} does not meet the requirement for "${selector}":\n` + + ` Expected the ${property} property to be ${expected} but found "${actual}"`); + } + } + } } } + + if (!pass) { + throw new Error('Some checks failed.'); + } } + +exports['update-readme'] = updateReadme; exports.screenshot = screenshotMissingThemes; exports['screenshot-all'] = screenshotAllThemes; -exports.check = parallel(checkScreenshots, checkAvailableThemes) +exports.build = parallel(screenshotMissingThemes, updateReadme); + +exports.check = parallel(checkScreenshots, checkCSS, checkRequirements) diff --git a/package.json b/package.json index 25e6879..c591d57 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Additional themes for the Prism syntax highlighting library.", "main": "README.md", "scripts": { + "build": "npx gulp build", "lint": "stylelint \"themes/*.css\"", "lint-fix": "stylelint \"themes/*.css\" --fix", "test": "npm run lint && gulp check" @@ -21,6 +22,7 @@ "devDependencies": { "capture-website": "^0.4.0", "gulp": "^4.0.2", + "jsdom": "^15.2.1", "stylelint": "^12.0.0", "stylelint-config-standard": "^19.0.0" } diff --git a/themes.json b/themes.json new file mode 100644 index 0000000..5865b4e --- /dev/null +++ b/themes.json @@ -0,0 +1,172 @@ +{ + "themes": { + "cb": { + "title": "CB", + "owner": "atelierbram", + "originalOwner": { + "name": "C. Bavota", + "link": "https://bitbucket.org/cbavota" + } + }, + "ghcolors": { + "title": "GHColors", + "owner": "aviaryan" + }, + "pojoaque": { + "title": "Pojoaque", + "owner": "atelierbram", + "originalOwner": { + "name": "Jason Tate", + "link": "http://web-cms-designs.com/ftopict-10-pojoaque-style-for-highlight-js-code-highlighter.html" + } + }, + "xonokai": { + "title": "Xonokai", + "owner": "atelierbram", + "originalOwner": { + "name": "Maxime Thirouin (MoOx)", + "link": "https://github.com/MoOx" + } + }, + "base16-ateliersulphurpool.light": { + "title": "Ateliersulphurpool-light", + "owner": { + "name": "Bram de Haan", + "link": "https://github.com/atelierbram" + } + }, + "hopscotch": { + "title": "Hopscotch", + "owner": { + "name": "Jan T. Sott", + "link": "https://github.com/idleberg" + } + }, + "atom-dark": { + "title": "Atom Dark", + "owner": "gibsjose", + "basedOn": { + "name": "Atom Dark Syntax theme", + "link": "https://github.com/atom/atom-dark-syntax" + } + }, + "duotone-dark": { + "title": "Duotone Dark", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "Duotone Dark Syntax theme for Atom", + "link": "https://github.com/simurai/duotone-dark-syntax" + } + }, + "duotone-sea": { + "title": "Duotone Sea", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "DuoTone Dark Sea Syntax theme for Atom", + "link": "https://github.com/simurai/duotone-dark-sea-syntax" + } + }, + "duotone-space": { + "title": "Duotone Space", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "DuoTone Dark Space Syntax theme for Atom", + "link": "https://github.com/simurai/duotone-dark-space-syntax" + } + }, + "duotone-earth": { + "title": "Duotone Earth", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "DuoTone Dark Earth Syntax theme for Atom", + "link": "https://github.com/simurai/duotone-dark-earth-syntax" + } + }, + "duotone-forest": { + "title": "Duotone Forest", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "DuoTone Dark Forest Syntax theme for Atom", + "link": "https://github.com/simurai/duotone-dark-forest-syntax" + } + }, + "duotone-light": { + "title": "Duotone Light", + "owner": { + "name": "Simurai", + "link": "https://github.com/simurai" + }, + "basedOn": { + "name": "DuoTone Light Syntax theme", + "link": "https://github.com/simurai/duotone-light-syntax" + } + }, + "vs": { + "title": "VS", + "owner": "andrewlock" + }, + "darcula": { + "title": "Darcula", + "owner": "service-paradis", + "basedOn": { + "name": "Jetbrain's Darcula theme" + } + }, + "a11y-dark": { + "title": "a11y Dark", + "owner": "ericwbailey" + }, + "dracula": { + "title": "Dracula", + "owner": { + "name": "Byverdu", + "link": "https://github.com/byverdu" + } + }, + "synthwave84": { + "title": "Synthwave '84", + "owner": { + "name": "Marc Backes", + "link": "https://github.com/themarcba" + }, + "originalOwner": { + "name": "Robb Owen", + "link": "https://github.com/robb0wen" + } + }, + "shades-of-purple": { + "title": "Shades of Purple", + "owner": { + "name": "Ahmad Awais", + "link": "https://github.com/ahmadawais" + } + }, + "material-dark": { + "title": "Material Dark", + "owner": "dutchenkoOleg" + }, + "material-light": { + "title": "Material Light", + "owner": "dutchenkoOleg" + }, + "material-oceanic": { + "title": "Material Oceanic", + "owner": "dutchenkoOleg" + } + } +} diff --git a/themes/prism-shades-of-purple.css b/themes/prism-shades-of-purple.css index a4a0cd5..76c45bd 100644 --- a/themes/prism-shades-of-purple.css +++ b/themes/prism-shades-of-purple.css @@ -43,6 +43,10 @@ code[class*='language-'] ::selection { background: #a599e9; } +pre > code[class*="language-"] { + font-size: 1em; +} + /* Code blocks. */ pre[class*='language-'] { padding: 2em;