├── .npmignore ├── .flowconfig ├── .gitignore ├── prism └── package.json ├── demo ├── .gitignore ├── src │ ├── styles.js │ └── index.js ├── package.json └── public │ └── index.html ├── src ├── index.js ├── defaultProps.js ├── utils │ ├── themeToDict.js │ ├── __tests__ │ │ ├── themeToDict.test.js │ │ └── normalizeTokens.test.js │ └── normalizeTokens.js ├── vendor │ └── prism │ │ ├── includeLangs.js │ │ ├── index.js │ │ └── prism-core.js ├── types.js └── components │ ├── Highlight.js │ └── __tests__ │ ├── Highlight.test.js │ └── __snapshots__ │ └── Highlight.test.js.snap ├── tools └── themeFromVsCode │ ├── package.json │ ├── README.md │ ├── src │ ├── template.js │ ├── transformSettings.js │ ├── minify.js │ ├── index.js │ ├── scopeScore.js │ ├── collectStyles.js │ └── scopeMapper.js │ └── yarn.lock ├── themes ├── ultramin.js ├── dracula.js ├── vsDark.js ├── vsDarkPlus.js ├── shadesOfPurple.js ├── duotoneDark.js ├── nightOwl.js ├── duotoneLight.js └── oceanicNext.js ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vscode 4 | *.log 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | bundle-stats.html 5 | .vscode 6 | __diff_output__ 7 | lib/ 8 | es/ 9 | sandbox/node_modules 10 | *.log 11 | test-results.json 12 | -------------------------------------------------------------------------------- /prism/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prism-react-renderer/prism", 3 | "private": true, 4 | "main": "../lib/vendor/prism/index.js", 5 | "module": "../es/vendor/prism/index.js" 6 | } 7 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | bundle-stats.html 5 | .vscode 6 | __diff_output__ 7 | lib/ 8 | es/ 9 | sandbox/node_modules 10 | *.log 11 | test-results.json 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Prism from './vendor/prism' 4 | import defaultProps from './defaultProps' 5 | import Highlight from './components/Highlight' 6 | 7 | export { 8 | Prism, 9 | defaultProps 10 | } 11 | 12 | export default Highlight 13 | -------------------------------------------------------------------------------- /src/defaultProps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Prism from './vendor/prism' 4 | import theme from '../themes/duotoneDark' 5 | 6 | import type { PrismLib } from './types' 7 | 8 | const defaultProps = { 9 | // $FlowFixMe 10 | Prism: (Prism: PrismLib), 11 | theme 12 | }; 13 | 14 | export default defaultProps; 15 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prism-react-renderer/theme-from-vscode", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "private": true, 7 | "dependencies": { 8 | "color": "^3.0.0", 9 | "is-equal": "^1.5.5" 10 | }, 11 | "scripts": { 12 | "start": "node ./src/index.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/README.md: -------------------------------------------------------------------------------- 1 | # Generate a Prism Theme from VSCode `.json` Themes 2 | 3 | 1. Open this directory and run `npm install` 4 | 2. Save your VSCode theme in a file called `theme.json` (inside root same as the `README.md` file) 5 | 3. Run `npm start` and your theme will be created in a file called `outputTheme.js` (inside root same as the `README.md` file) 6 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/template.js: -------------------------------------------------------------------------------- 1 | const makeOutput = styles => ` 2 | // @flow 3 | // Converted automatically using ./tools/themeFromVsCode 4 | 5 | /*:: import type { PrismTheme } from '../src/types' */ 6 | 7 | var theme /*: PrismTheme */ = ${JSON.stringify(styles, null, 2)}; 8 | 9 | module.exports = theme; 10 | `.trim() + '\n'; 11 | 12 | module.exports = { makeOutput }; 13 | -------------------------------------------------------------------------------- /demo/src/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | font-family: sans-serif; 5 | text-align: center; 6 | ` 7 | 8 | export const Pre = styled.pre` 9 | text-align: left; 10 | margin: 1em 0; 11 | padding: 0.5em; 12 | line-height: 1.3; 13 | ` 14 | 15 | export const LineNo = styled.span` 16 | display: inline-block; 17 | width: 2em; 18 | user-select: none; 19 | opacity: 0.3; 20 | ` 21 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/transformSettings.js: -------------------------------------------------------------------------------- 1 | const color = require('color'); 2 | 3 | const transformSettings = settings => { 4 | const output = {}; 5 | 6 | if (settings.foreground) { 7 | output.color = color(settings.foreground).string(); 8 | } 9 | 10 | if (settings.background) { 11 | output.backgroundColor = color(settings.background).string(); 12 | } 13 | 14 | if (settings.fontStyle === 'italic') { 15 | output.fontStyle = 'italic'; 16 | } 17 | 18 | return output; 19 | }; 20 | 21 | module.exports = { transformSettings }; 22 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/minify.js: -------------------------------------------------------------------------------- 1 | const isEqual = require('is-equal'); 2 | 3 | const minify = styles => { 4 | const output = []; 5 | 6 | styles.forEach(style => { 7 | const item = output.find(x => { 8 | return isEqual(style.settings, x.style); 9 | }); 10 | 11 | if (!item) { 12 | output.push({ 13 | types: [style.scope], 14 | style: style.settings 15 | }); 16 | } else { 17 | item.types.push(style.scope); 18 | } 19 | }); 20 | 21 | return output; 22 | }; 23 | 24 | module.exports = { minify }; 25 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prism-react-renderer-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "prism-react-renderer": "file:../", 9 | "react": "16.3.2", 10 | "react-dom": "16.3.2", 11 | "react-scripts": "1.1.4", 12 | "styled-components": "latest" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/index.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | const { collectAllSettings } = require('./collectStyles'); 3 | const { makeOutput } = require('./template'); 4 | 5 | // Input 6 | const theme = require('../theme.json'); 7 | const prismTheme = collectAllSettings(theme.tokenColors); 8 | 9 | const json = { 10 | plain: { 11 | color: theme.colors['editor.foreground'], 12 | backgroundColor: theme.colors['editor.background'] 13 | }, 14 | ...prismTheme 15 | }; 16 | 17 | const output = makeOutput(json); 18 | 19 | writeFileSync('./outputTheme.js', output); 20 | -------------------------------------------------------------------------------- /themes/ultramin.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Original: https://github.com/damienstanton/ultramin 3 | // Converted automatically using ./tools/themeFromVsCode 4 | 5 | /*:: import type { PrismTheme } from '../src/types' */ 6 | 7 | var theme /*: PrismTheme */ = { 8 | plain: { 9 | color: "#282a2e", 10 | backgroundColor: "#ffffff" 11 | }, 12 | styles: [ 13 | { 14 | types: ["comment"], 15 | style: { 16 | color: "rgb(197, 200, 198)" 17 | } 18 | }, 19 | { 20 | types: ["string", "number", "builtin", "variable"], 21 | style: { 22 | color: "rgb(150, 152, 150)" 23 | } 24 | }, 25 | { 26 | types: ["class-name", "function", "tag", "attr-name"], 27 | style: { 28 | color: "rgb(40, 42, 46)" 29 | } 30 | } 31 | ] 32 | }; 33 | 34 | module.exports = theme; 35 | -------------------------------------------------------------------------------- /src/utils/themeToDict.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Language, StyleObj, PrismTheme, PrismThemeEntry } from "../types"; 4 | 5 | export type ThemeDict = { 6 | root: StyleObj, 7 | plain: StyleObj, 8 | [type: string]: StyleObj 9 | }; 10 | 11 | const themeToDict = (theme: PrismTheme, language: Language): ThemeDict => { 12 | const { plain } = theme; 13 | 14 | const themeDict = theme.styles.reduce((acc, themeEntry) => { 15 | const { types, languages, style } = themeEntry; 16 | if (languages && !languages.includes(language)) { 17 | return acc; 18 | } 19 | 20 | themeEntry.types.forEach(type => { 21 | // $FlowFixMe 22 | const accStyle: StyleObj = { ...acc[type], ...style }; 23 | 24 | acc[type] = accStyle; 25 | }); 26 | 27 | return acc; 28 | }, Object.create(null)); 29 | 30 | // $FlowFixMe 31 | themeDict.root = (plain: StyleObj); 32 | // $FlowFixMe 33 | themeDict.plain = ({ ...plain, backgroundColor: null }: StyleObj); 34 | 35 | return themeDict; 36 | }; 37 | 38 | export default themeToDict; 39 | -------------------------------------------------------------------------------- /src/vendor/prism/includeLangs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // These are the languages that'll be included in the generated 4 | // prism/index.js file 5 | 6 | module.exports = { 7 | "markup": true, 8 | "bash": true, 9 | "clike": true, 10 | "c": true, 11 | "cpp": true, 12 | "css": true, 13 | "javascript": true, 14 | "jsx": true, 15 | "coffeescript": true, 16 | "actionscript": true, 17 | "css-extras": true, 18 | "diff": true, 19 | "docker": true, 20 | "elixir": true, 21 | "erlang": true, 22 | "git": true, 23 | "go": true, 24 | "graphql": true, 25 | "handlebars": true, 26 | "haskell": true, 27 | "java": true, 28 | "json": true, 29 | "latex": true, 30 | "less": true, 31 | "makefile": true, 32 | "markdown": true, 33 | "objectivec": true, 34 | "ocaml": true, 35 | "php": true, 36 | "php-extras": true, 37 | "python": true, 38 | "reason": true, 39 | "ruby": true, 40 | "rust": true, 41 | "sass": true, 42 | "scss": true, 43 | "sql": true, 44 | "stylus": true, 45 | "swift": true, 46 | "typescript": true, 47 | "vim": true, 48 | "yaml": true 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Formidable 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Wrapper, Pre, LineNo } from './styles' 4 | 5 | import Highlight, { defaultProps } from 'prism-react-renderer' 6 | import theme from 'prism-react-renderer/themes/oceanicNext' 7 | 8 | const exampleCode = ` 9 | (function someDemo() { 10 | var test = "Hello World!"; 11 | console.log(test); 12 | })(); 13 | 14 | return () => ; 15 | `.trim() 16 | 17 | const App = () => ( 18 | 19 |

Welcome to prism-react-renderer!

20 | 21 | 22 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 23 |
24 |           {tokens.map((line, i) => (
25 |             
26 | {i + 1} 27 | {line.map((token, key) => )} 28 |
29 | ))} 30 |
31 | )} 32 |
33 |
34 | ) 35 | 36 | render(, document.getElementById('root')) 37 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/scopeScore.js: -------------------------------------------------------------------------------- 1 | const scorePerScope = { 2 | builtin: [ 3 | 'variable.other.constant', 4 | 'constant.language' 5 | ], 6 | punctuation: [ 7 | 'punctuation.accessor' 8 | ], 9 | tag: [ 10 | 'meta.tag.html', 11 | 'entity.name.tag' 12 | ] 13 | } 14 | 15 | // The higher the better 16 | const score = [ 17 | // These are sure matches 18 | 'markup', // diffs 19 | 'comment', 20 | 'punctuation', 21 | 'string', 22 | 'variable', 23 | 24 | // These are more "meta" scopes 25 | 'meta', // good guesses 26 | 'entity', 27 | 'constant', 28 | 'support', 29 | 'variable' 30 | ]; 31 | 32 | const baseScoreSize = score.length 33 | 34 | const getScoreForScope = (scope, mappedScope) => { 35 | // Get scores for specific mapped scopes first 36 | const scoreForMapped = scorePerScope[mappedScope]; 37 | if (scoreForMapped) { 38 | // If the scope is in the specific mapped scope we add the baseScoreSize to this 39 | // score 40 | const mappedIndex = scoreForMapped.findIndex(x => scope.startsWith(x)); 41 | if (mappedIndex !== -1) { 42 | return baseScoreSize + (scoreForMapped.length - mappedIndex); 43 | } 44 | } 45 | 46 | const parentScope = scope.split('.')[0]; 47 | const index = score.indexOf(parentScope); 48 | 49 | if (index === -1) { 50 | // Otherwise it's a negative score based on length 51 | return -1 * scope.length; 52 | } 53 | 54 | // If it's found we return the score from the main `score` arr 55 | return baseScoreSize - index; 56 | }; 57 | 58 | module.exports = { getScoreForScope }; 59 | -------------------------------------------------------------------------------- /themes/dracula.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Original: https://github.com/dracula/visual-studio-code 3 | // Converted automatically using ./tools/themeFromVsCode 4 | 5 | /*:: import type { PrismTheme } from '../src/types' */ 6 | 7 | var theme /*: PrismTheme */ = { 8 | plain: { 9 | color: "#F8F8F2", 10 | backgroundColor: "#282A36" 11 | }, 12 | styles: [ 13 | { 14 | types: ["prolog", "constant", "builtin"], 15 | style: { 16 | color: "rgb(189, 147, 249)" 17 | } 18 | }, 19 | { 20 | types: ["inserted", "function"], 21 | style: { 22 | color: "rgb(80, 250, 123)" 23 | } 24 | }, 25 | { 26 | types: ["deleted"], 27 | style: { 28 | color: "rgb(255, 85, 85)" 29 | } 30 | }, 31 | { 32 | types: ["changed"], 33 | style: { 34 | color: "rgb(255, 184, 108)" 35 | } 36 | }, 37 | { 38 | types: ["punctuation", "symbol"], 39 | style: { 40 | color: "rgb(248, 248, 242)" 41 | } 42 | }, 43 | { 44 | types: ["string", "char", "tag", "selector"], 45 | style: { 46 | color: "rgb(255, 121, 198)" 47 | } 48 | }, 49 | { 50 | types: ["keyword", "variable"], 51 | style: { 52 | color: "rgb(189, 147, 249)", 53 | fontStyle: "italic" 54 | } 55 | }, 56 | { 57 | types: ["comment"], 58 | style: { 59 | color: "rgb(98, 114, 164)" 60 | } 61 | }, 62 | { 63 | types: ["attr-name"], 64 | style: { 65 | color: "rgb(241, 250, 140)" 66 | } 67 | } 68 | ] 69 | }; 70 | 71 | module.exports = theme; 72 | -------------------------------------------------------------------------------- /themes/vsDark.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Converted automatically using ./tools/themeFromVsCode 3 | 4 | /*:: import type { PrismTheme } from '../src/types' */ 5 | 6 | var theme /*: PrismTheme */ = { 7 | plain: { 8 | color: "#D4D4D4", 9 | backgroundColor: "#1E1E1E" 10 | }, 11 | styles: [ 12 | { 13 | types: ["prolog"], 14 | style: { 15 | color: "rgb(0, 0, 128)" 16 | } 17 | }, 18 | { 19 | types: ["comment"], 20 | style: { 21 | color: "rgb(106, 153, 85)" 22 | } 23 | }, 24 | { 25 | types: ["builtin", "tag", "changed", "keyword"], 26 | style: { 27 | color: "rgb(86, 156, 214)" 28 | } 29 | }, 30 | { 31 | types: ["number", "inserted"], 32 | style: { 33 | color: "rgb(181, 206, 168)" 34 | } 35 | }, 36 | { 37 | types: ["constant"], 38 | style: { 39 | color: "rgb(100, 102, 149)" 40 | } 41 | }, 42 | { 43 | types: ["attr-name", "variable"], 44 | style: { 45 | color: "rgb(156, 220, 254)" 46 | } 47 | }, 48 | { 49 | types: ["deleted", "string"], 50 | style: { 51 | color: "rgb(206, 145, 120)" 52 | } 53 | }, 54 | { 55 | types: ["selector"], 56 | style: { 57 | color: "rgb(215, 186, 125)" 58 | } 59 | }, 60 | { 61 | types: ["punctuation"], 62 | style: { 63 | color: "rgb(128, 128, 128)" 64 | } 65 | }, 66 | { 67 | types: ["operator"], 68 | style: { 69 | color: "rgb(212, 212, 212)" 70 | } 71 | } 72 | ] 73 | }; 74 | 75 | module.exports = theme; 76 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/utils/__tests__/themeToDict.test.js: -------------------------------------------------------------------------------- 1 | import themeToDict from "../themeToDict" 2 | 3 | describe("themeToDict", () => { 4 | it("converts entry.types to dictionary", () => { 5 | const input = { 6 | plain: { color: "red" }, 7 | styles: [ 8 | { 9 | types: ["1", "2"], 10 | style: { 11 | color: "green", 12 | }, 13 | }, 14 | { 15 | types: ["3"], 16 | style: { 17 | color: "blue", 18 | }, 19 | }, 20 | { 21 | types: ["2"], 22 | style: { 23 | color: "orange", 24 | }, 25 | }, 26 | ], 27 | } 28 | 29 | const expected = { 30 | root: { 31 | color: "red", 32 | }, 33 | plain: { 34 | color: "red", 35 | backgroundColor: null, 36 | }, 37 | 1: { 38 | color: "green", 39 | }, 40 | 2: { 41 | color: "orange", 42 | }, 43 | 3: { 44 | color: "blue", 45 | }, 46 | } 47 | 48 | expect(themeToDict(input)).toEqual(expected) 49 | // Check order in which keys were added to implicitly test merge strategy 50 | expect(Object.keys(themeToDict(input, 'js'))).toEqual(Object.keys(expected)) 51 | }) 52 | 53 | it("limits entries by entry.languages", () => { 54 | const input = { 55 | plain: {}, 56 | styles: [ 57 | { 58 | types: ['test'], 59 | languages: ['js'], 60 | style: { 61 | color: "green", 62 | }, 63 | } 64 | ], 65 | } 66 | 67 | expect(themeToDict(input, 'js').test).toEqual({ 68 | color: 'green' 69 | }) 70 | 71 | expect(themeToDict(input, 'ocaml').test).toEqual(undefined) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /themes/vsDarkPlus.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Converted automatically using ./tools/themeFromVsCode 3 | 4 | /*:: import type { PrismTheme } from '../src/types' */ 5 | 6 | var theme /*: PrismTheme */ = { 7 | plain: { 8 | color: "#D4D4D4", 9 | backgroundColor: "#1E1E1E" 10 | }, 11 | styles: [ 12 | { 13 | types: ["prolog"], 14 | style: { 15 | color: "rgb(0, 0, 128)" 16 | } 17 | }, 18 | { 19 | types: ["comment"], 20 | style: { 21 | color: "rgb(106, 153, 85)" 22 | } 23 | }, 24 | { 25 | types: ["builtin", "tag", "changed", "punctuation", "keyword"], 26 | style: { 27 | color: "rgb(86, 156, 214)" 28 | } 29 | }, 30 | { 31 | types: ["number", "inserted"], 32 | style: { 33 | color: "rgb(181, 206, 168)" 34 | } 35 | }, 36 | { 37 | types: ["constant"], 38 | style: { 39 | color: "rgb(100, 102, 149)" 40 | } 41 | }, 42 | { 43 | types: ["attr-name", "variable"], 44 | style: { 45 | color: "rgb(156, 220, 254)" 46 | } 47 | }, 48 | { 49 | types: ["deleted", "string"], 50 | style: { 51 | color: "rgb(206, 145, 120)" 52 | } 53 | }, 54 | { 55 | types: ["selector"], 56 | style: { 57 | color: "rgb(215, 186, 125)" 58 | } 59 | }, 60 | { 61 | types: ["operator"], 62 | style: { 63 | color: "rgb(212, 212, 212)" 64 | } 65 | }, 66 | { 67 | types: ["function"], 68 | style: { 69 | color: "rgb(220, 220, 170)" 70 | } 71 | }, 72 | { 73 | types: ["class-name"], 74 | style: { 75 | color: "rgb(78, 201, 176)" 76 | } 77 | }, 78 | { 79 | types: ["char"], 80 | style: { 81 | color: "rgb(209, 105, 105)" 82 | } 83 | } 84 | ] 85 | }; 86 | 87 | module.exports = theme; 88 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/collectStyles.js: -------------------------------------------------------------------------------- 1 | const { mapScope } = require('./scopeMapper'); 2 | const { getScoreForScope } = require('./scopeScore'); 3 | const { transformSettings } = require('./transformSettings'); 4 | const { minify } = require('./minify'); 5 | 6 | const collectAllSettings = tokenColors => { 7 | const output = {} 8 | 9 | tokenColors.forEach(({ name, scope, settings }) => { 10 | // We only care about colouring here 11 | if (!settings.foreground && !settings.fontStyle) { 12 | return; 13 | } 14 | 15 | const normScope = typeof scope === 'string' ? [scope] : scope; 16 | // Return when no input scopes are present 17 | if (!normScope || !normScope.length) { 18 | return; 19 | } 20 | 21 | normScope.forEach(scopeName => { 22 | const mappedScope = mapScope(scopeName); 23 | // Return when no mapping scope has been returned 24 | if (!mappedScope) { 25 | return; 26 | } 27 | 28 | if (output[mappedScope] === undefined) { 29 | output[mappedScope] = []; 30 | } 31 | 32 | output[mappedScope].push({ 33 | scope: scopeName, 34 | settings 35 | }); 36 | }); 37 | }); 38 | 39 | const styles = Object.keys(output).map(mappedScope => { 40 | const matchesArr = output[mappedScope]; 41 | 42 | // Get score for each match 43 | const scored = matchesArr.map(match => { 44 | const score = getScoreForScope(match.scope, mappedScope); 45 | 46 | return { 47 | score, 48 | scope: mappedScope, 49 | settings: transformSettings(match.settings) 50 | }; 51 | }); 52 | 53 | // Sort by score asc 54 | const sorted = scored.sort((a, b) => b.score - a.score); 55 | 56 | // Return highest-scored one 57 | return sorted[0]; 58 | }); 59 | 60 | const themeStyles = minify(styles); 61 | 62 | return { 63 | styles: themeStyles 64 | }; 65 | }; 66 | 67 | module.exports = { collectAllSettings }; 68 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/src/scopeMapper.js: -------------------------------------------------------------------------------- 1 | const scopeMap = { 2 | comment: "comment", 3 | punctuation: "punctuation", 4 | string: "string", 5 | variable: "variable", 6 | constant: "constant", 7 | header: "prolog", 8 | "support.function.magic": "constant", 9 | "support.variable": "constant", 10 | "entity.name.type.namespace": "namespace", 11 | "keyword.operator": "operator", 12 | "constant.numeric": "number", 13 | "constant.character.numeric": "number", 14 | "support.type.vendor.property-name": "property", 15 | "support.type.property-name": "property", 16 | "meta.property-list": "property", 17 | "entity.name.tag": "tag", 18 | "entity.name.function": "function", 19 | "entity.name.class": "class-name", 20 | "entity.name.tag.doctype": "doctype", 21 | "meta.selector": "selector", 22 | "entity.other.attribute-name": "attr-name", 23 | "meta.attribute-selector": "attr-name", 24 | "constant.other": "constant", 25 | "constant.other.symbol": "symbol", 26 | "constant.language.boolean": "boolean", 27 | "constant.character": "char", 28 | "entity.name.tag": "tag", 29 | "meta.tag.html": "tag", 30 | "meta.tag.js": "tag", 31 | "meta.selector": "selector", 32 | "support.function": "builtin", 33 | "support.type.property-name": "builtin", 34 | "variable.other.constant": "builtin", 35 | "constant.language": "builtin", 36 | "keyword.control": "keyword", 37 | "keyword.other": "keyword", 38 | "variable.parameter.url": "url", 39 | "meta.at-rule": "at-rule", 40 | "source.css.scss": "at-rule", 41 | "markup.inserted": "inserted", 42 | "markup.deleted": "deleted", 43 | "markup.changed": "changed", 44 | } 45 | 46 | const mapScope = scope => { 47 | // If the scope includes a whitespace, it's a specific 48 | // type that we don't support 49 | if (scope.includes(" ")) { 50 | return undefined 51 | } 52 | 53 | const scopeAccess = scope.split(".") 54 | 55 | for (let i = scopeAccess.length; i >= 0; i--) { 56 | const searchScope = scopeAccess.slice(0, i).join(".") 57 | const outputScope = scopeMap[searchScope] 58 | if (outputScope !== undefined) { 59 | return outputScope 60 | } 61 | } 62 | 63 | return undefined 64 | } 65 | 66 | module.exports = { mapScope } 67 | -------------------------------------------------------------------------------- /themes/shadesOfPurple.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Shades of Purple 3 | // Author: Ahmad Awais https://twitter.com/MrAhmadAwais 4 | // Original: https://github.com/ahmadawais/shades-of-purple-vscode/ 5 | // Converted automatically using ./tools/themeFromVsCode and then customized manually. 6 | 7 | /*:: import type { PrismTheme } from '../src/types' */ 8 | 9 | var theme /*: PrismTheme */ = { 10 | plain: { 11 | color: "#9EFEFF", 12 | backgroundColor: "#2D2A55" 13 | }, 14 | styles: [ 15 | { 16 | types: ["changed"], 17 | style: { 18 | color: "rgb(255, 238, 128)" 19 | } 20 | }, 21 | { 22 | types: ["deleted"], 23 | style: { 24 | color: "rgba(239, 83, 80, 0.56)" 25 | } 26 | }, 27 | { 28 | types: ["inserted"], 29 | style: { 30 | color: "rgb(173, 219, 103)" 31 | } 32 | }, 33 | { 34 | types: ["comment"], 35 | style: { 36 | color: "rgb(179, 98, 255)", 37 | fontStyle: "italic" 38 | } 39 | }, 40 | { 41 | types: ["punctuation"], 42 | style: { 43 | color: "rgb(255, 255, 255)" 44 | } 45 | }, 46 | { 47 | types: ["constant"], 48 | style: { 49 | color: "rgb(255, 98, 140)" 50 | } 51 | }, 52 | { 53 | types: ["string", "url"], 54 | style: { 55 | color: "rgb(165, 255, 144)" 56 | } 57 | }, 58 | { 59 | types: ["variable"], 60 | style: { 61 | color: "rgb(255, 238, 128)" 62 | } 63 | }, 64 | { 65 | types: ["number", "boolean"], 66 | style: { 67 | color: "rgb(255, 98, 140)" 68 | } 69 | }, 70 | { 71 | types: ["attr-name"], 72 | style: { 73 | color: "rgb(255, 180, 84)" 74 | } 75 | }, 76 | { 77 | types: [ 78 | "keyword", 79 | "operator", 80 | "property", 81 | "namespace", 82 | "tag", 83 | "selector", 84 | "doctype" 85 | ], 86 | style: { 87 | color: "rgb(255, 157, 0)" 88 | } 89 | }, 90 | { 91 | types: ["builtin", "char", "constant", "function", "class-name"], 92 | style: { 93 | color: "rgb(250, 208, 0)" 94 | } 95 | } 96 | ] 97 | }; 98 | 99 | module.exports = theme; 100 | -------------------------------------------------------------------------------- /themes/duotoneDark.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Duotone Dark 3 | // Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes) 4 | // Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-evening-dark.css) 5 | // Generated with Base16 Builder (https://github.com/base16-builder/base16-builder) 6 | 7 | /*:: import type { PrismTheme } from '../src/types' */ 8 | 9 | var theme /*: PrismTheme */ = { 10 | plain: { 11 | backgroundColor: "#2a2734", 12 | color: "#9a86fd" 13 | }, 14 | styles: [ 15 | { 16 | types: ["comment", "prolog", "doctype", "cdata", "punctuation"], 17 | style: { 18 | color: "#6c6783" 19 | } 20 | }, 21 | { 22 | types: ["namespace"], 23 | style: { 24 | opacity: 0.7 25 | } 26 | }, 27 | { 28 | types: ["tag", "operator", "number"], 29 | style: { 30 | color: "#e09142" 31 | } 32 | }, 33 | { 34 | types: ["property", "function"], 35 | style: { 36 | color: "#9a86fd" 37 | } 38 | }, 39 | { 40 | types: ["tag-id", "selector", "atrule-id"], 41 | style: { 42 | color: "#eeebff" 43 | } 44 | }, 45 | { 46 | types: ["attr-name"], 47 | style: { 48 | color: "#c4b9fe" 49 | } 50 | }, 51 | { 52 | types: [ 53 | "boolean", 54 | "string", 55 | "entity", 56 | "url", 57 | "attr-value", 58 | "keyword", 59 | "control", 60 | "directive", 61 | "unit", 62 | "statement", 63 | "regex", 64 | "at-rule", 65 | "placeholder", 66 | "variable" 67 | ], 68 | style: { 69 | color: "#ffcc99" 70 | } 71 | }, 72 | { 73 | types: ["deleted"], 74 | style: { 75 | textDecorationLine: "line-through" 76 | } 77 | }, 78 | { 79 | types: ["inserted"], 80 | style: { 81 | textDecorationLine: "underline" 82 | } 83 | }, 84 | { 85 | types: ["italic"], 86 | style: { 87 | fontStyle: "italic" 88 | } 89 | }, 90 | { 91 | types: ["important", "bold"], 92 | style: { 93 | fontWeight: "bold" 94 | } 95 | }, 96 | { 97 | types: ["important"], 98 | style: { 99 | color: "#c4b9fe" 100 | } 101 | } 102 | ] 103 | }; 104 | 105 | module.exports = theme; 106 | -------------------------------------------------------------------------------- /src/vendor/prism/index.js: -------------------------------------------------------------------------------- 1 | import Prism from './prism-core' 2 | import codegen from 'codegen.macro' 3 | 4 | // Babel Codegen Macro: 5 | // Get a list of all prismjs languages and inline them here. 6 | // They should only depend on "Prism" being present in the current scope. 7 | 8 | codegen` 9 | const { readFileSync } = require('fs') 10 | const { dirname, join } = require('path') 11 | const { languages } = require('prismjs/components') 12 | const prismPath = dirname(require.resolve('prismjs')) 13 | 14 | let output = '/* This content is auto-generated to include some prismjs language components: */\\n' 15 | 16 | const toDependencies = arr => { 17 | if (typeof arr === 'string') { 18 | return [arr] 19 | } 20 | 21 | return arr; 22 | }; 23 | 24 | const addLanguageToOutput = language => { 25 | const pathToLanguage = 'components/prism-' + language 26 | const fullPath = join(prismPath, pathToLanguage + '.js') 27 | const contents = readFileSync(fullPath, 'utf8') 28 | const header = '\\n\\n/* "prismjs/' + pathToLanguage + '" */\\n' 29 | output += header + contents 30 | } 31 | 32 | const visitedLanguages = {} 33 | 34 | const visitLanguage = (language, langEntry) => { 35 | // Mark language as visited or return if it was 36 | if (visitedLanguages[language]) { 37 | return 38 | } else { 39 | visitedLanguages[language] = true 40 | } 41 | 42 | // Required dependencies come before the actual language 43 | const required = toDependencies(langEntry.require) 44 | 45 | if (Array.isArray(required)) { 46 | required.forEach(x => { 47 | if (languages[x]) { 48 | visitLanguage(x, languages[x]) 49 | } else { 50 | console.warn('[prismjs/components]: Language', x, 'does not exist!') 51 | } 52 | }) 53 | } 54 | 55 | // Add current language to output 56 | addLanguageToOutput(language) 57 | 58 | // Peer dependencies come after the actual language 59 | const peerDependencies = toDependencies(langEntry.peerDependencies) 60 | 61 | if (Array.isArray(peerDependencies)) { 62 | peerDependencies.forEach(x => { 63 | if (languages[x]) { 64 | visitLanguage(x, languages[x]) 65 | } else { 66 | console.warn('[prismjs/components]: Language', x, 'does not exist!') 67 | } 68 | }) 69 | } 70 | }; 71 | 72 | // This json defines which languages to include 73 | const includedLangs = require('./includeLangs') 74 | 75 | Object.keys(includedLangs).forEach(language => { 76 | visitLanguage(language, languages[language]) 77 | }) 78 | 79 | module.exports = output 80 | ` 81 | 82 | export default Prism 83 | -------------------------------------------------------------------------------- /themes/nightOwl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Original: https://github.com/sdras/night-owl-vscode-theme 3 | // Converted automatically using ./tools/themeFromVsCode 4 | 5 | /*:: import type { PrismTheme } from '../src/types' */ 6 | 7 | var theme /*: PrismTheme */ = { 8 | plain: { 9 | color: "#d6deeb", 10 | backgroundColor: "#011627" 11 | }, 12 | styles: [ 13 | { 14 | types: ["changed"], 15 | style: { 16 | color: "rgb(162, 191, 252)", 17 | fontStyle: "italic" 18 | } 19 | }, 20 | { 21 | types: ["deleted"], 22 | style: { 23 | color: "rgba(239, 83, 80, 0.56)", 24 | fontStyle: "italic" 25 | } 26 | }, 27 | { 28 | types: ["inserted", "attr-name"], 29 | style: { 30 | color: "rgb(173, 219, 103)", 31 | fontStyle: "italic" 32 | } 33 | }, 34 | { 35 | types: ["comment"], 36 | style: { 37 | color: "rgb(99, 119, 119)", 38 | fontStyle: "italic" 39 | } 40 | }, 41 | { 42 | types: ["string", "url"], 43 | style: { 44 | color: "rgb(173, 219, 103)" 45 | } 46 | }, 47 | { 48 | types: ["variable"], 49 | style: { 50 | color: "rgb(214, 222, 235)" 51 | } 52 | }, 53 | { 54 | types: ["number"], 55 | style: { 56 | color: "rgb(247, 140, 108)" 57 | } 58 | }, 59 | { 60 | types: ["builtin", "char", "constant", "function"], 61 | style: { 62 | color: "rgb(130, 170, 255)" 63 | } 64 | }, 65 | { 66 | // This was manually added after the auto-generation 67 | // so that punctuations are not italicised 68 | types: ["punctuation"], 69 | style: { 70 | color: "rgb(199, 146, 234)", 71 | } 72 | }, 73 | { 74 | types: ["selector", "doctype"], 75 | style: { 76 | color: "rgb(199, 146, 234)", 77 | fontStyle: "italic" 78 | } 79 | }, 80 | { 81 | types: ["class-name"], 82 | style: { 83 | color: "rgb(255, 203, 139)" 84 | } 85 | }, 86 | { 87 | types: ["tag", "operator", "keyword"], 88 | style: { 89 | color: "rgb(127, 219, 202)" 90 | } 91 | }, 92 | { 93 | types: ["boolean"], 94 | style: { 95 | color: "rgb(255, 88, 116)" 96 | } 97 | }, 98 | { 99 | types: ["property"], 100 | style: { 101 | color: "rgb(128, 203, 196)" 102 | } 103 | }, 104 | { 105 | types: ["namespace"], 106 | style: { 107 | color: "rgb(178, 204, 214)" 108 | } 109 | } 110 | ] 111 | }; 112 | 113 | module.exports = theme; 114 | -------------------------------------------------------------------------------- /themes/duotoneLight.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Duotone Light 3 | // Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes) 4 | // Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-evening-dark.css) 5 | // Generated with Base16 Builder (https://github.com/base16-builder/base16-builder) 6 | 7 | /*:: import type { PrismTheme } from '../src/types' */ 8 | 9 | var theme /*: PrismTheme */ = { 10 | plain: { 11 | backgroundColor: "#faf8f5", 12 | color: "#728fcb" 13 | }, 14 | styles: [ 15 | { 16 | types: ["comment", "prolog", "doctype", "cdata", "punctuation"], 17 | style: { 18 | color: "#b6ad9a" 19 | } 20 | }, 21 | { 22 | types: ["namespace"], 23 | style: { 24 | opacity: 0.7 25 | } 26 | }, 27 | { 28 | types: ["tag", "operator", "number"], 29 | style: { 30 | color: "#063289" 31 | } 32 | }, 33 | { 34 | types: ["property", "function"], 35 | style: { 36 | color: "#b29762" 37 | } 38 | }, 39 | { 40 | types: ["tag-id", "selector", "atrule-id"], 41 | style: { 42 | color: "#2d2006" 43 | } 44 | }, 45 | { 46 | types: ["attr-name"], 47 | style: { 48 | color: "#896724" 49 | } 50 | }, 51 | { 52 | types: [ 53 | "boolean", 54 | "string", 55 | "entity", 56 | "url", 57 | "attr-value", 58 | "keyword", 59 | "control", 60 | "directive", 61 | "unit", 62 | "statement", 63 | "regex", 64 | "at-rule" 65 | ], 66 | style: { 67 | color: "#728fcb" 68 | } 69 | }, 70 | { 71 | types: ["placeholder", "variable"], 72 | style: { 73 | color: "#93abdc" 74 | } 75 | }, 76 | { 77 | types: ["deleted"], 78 | style: { 79 | textDecorationLine: "line-through" 80 | } 81 | }, 82 | { 83 | types: ["inserted"], 84 | style: { 85 | textDecorationLine: "underline" 86 | } 87 | }, 88 | { 89 | types: ["italic"], 90 | style: { 91 | fontStyle: "italic" 92 | } 93 | }, 94 | { 95 | types: ["important", "bold"], 96 | style: { 97 | fontWeight: "bold" 98 | } 99 | }, 100 | { 101 | types: ["important"], 102 | style: { 103 | color: "#896724" 104 | } 105 | } 106 | ] 107 | }; 108 | 109 | module.exports = theme; 110 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Key } from "react"; 4 | import includedLangs from "./vendor/prism/includeLangs"; 5 | 6 | export type Language = $Keys; 7 | 8 | type PrismGrammar = { 9 | [key: string]: mixed 10 | }; 11 | 12 | type LanguagesDict = { 13 | [lang: Language]: PrismGrammar 14 | }; 15 | 16 | export type PrismToken = { 17 | type: string, 18 | content: Array | string 19 | }; 20 | 21 | export type Token = { 22 | types: string[], 23 | content: string, 24 | empty?: boolean 25 | }; 26 | 27 | export type PrismLib = { 28 | languages: LanguagesDict, 29 | tokenize: ( 30 | code: string, 31 | grammar: PrismGrammar, 32 | language: Language 33 | ) => Array, 34 | highlight: (code: string, grammar: PrismGrammar, language: Language) => string 35 | }; 36 | 37 | export type StyleObj = { 38 | [key: string]: string | number | null 39 | }; 40 | 41 | export type LineInputProps = { 42 | key?: Key, 43 | style?: StyleObj, 44 | className?: string, 45 | line: Token[], 46 | [key: string]: mixed 47 | }; 48 | 49 | export type LineOutputProps = { 50 | key?: Key, 51 | style?: StyleObj, 52 | className: string, 53 | [key: string]: mixed 54 | }; 55 | 56 | export type TokenInputProps = { 57 | key?: Key, 58 | style?: StyleObj, 59 | className?: string, 60 | token: Token, 61 | [key: string]: mixed 62 | }; 63 | 64 | export type TokenOutputProps = { 65 | key?: Key, 66 | style?: StyleObj, 67 | className: string, 68 | children: string, 69 | [key: string]: mixed 70 | }; 71 | 72 | export type RenderProps = { 73 | tokens: Token[][], 74 | className: string, 75 | getLineProps: (input: LineInputProps) => LineOutputProps, 76 | getTokenProps: (input: TokenInputProps) => TokenOutputProps 77 | }; 78 | 79 | export type PrismThemeEntry = { 80 | color?: string, 81 | backgroundColor?: string, 82 | fontStyle?: "normal" | "italic", 83 | fontWeight?: 84 | | "normal" 85 | | "bold" 86 | | "100" 87 | | "200" 88 | | "300" 89 | | "400" 90 | | "500" 91 | | "600" 92 | | "700" 93 | | "800" 94 | | "900", 95 | textDecorationLine?: 96 | | "none" 97 | | "underline" 98 | | "line-through" 99 | | "underline line-through", 100 | opacity?: number, 101 | [styleKey: string]: string | number | void 102 | }; 103 | 104 | export type PrismTheme = { 105 | plain: PrismThemeEntry, 106 | styles: Array<{ 107 | types: string[], 108 | style: PrismThemeEntry, 109 | languages?: Language[] 110 | }> 111 | }; 112 | -------------------------------------------------------------------------------- /src/utils/normalizeTokens.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { PrismToken, Token } from "../types"; 4 | 5 | const newlineRe = /\r\n|\r|\n/; 6 | 7 | // Empty lines need to contain a single empty token, denoted with { empty: true } 8 | const normalizeEmptyLines = (line: Token[]) => { 9 | if (line.length === 0) { 10 | line.push({ 11 | types: ["plain"], 12 | content: "", 13 | empty: true 14 | }); 15 | } else if (line.length === 1 && line[0].content === "") { 16 | line[0].empty = true; 17 | } 18 | }; 19 | 20 | // Takes an array of Prism's tokens and groups them by line, turning plain 21 | // strings into tokens as well. Tokens can become recursive in some cases, 22 | // which means that their types are concatenated. Plain-string tokens however 23 | // are always of type "plain". 24 | // This is not recursive to avoid exceeding the call-stack limit, since it's unclear 25 | // how nested Prism's tokens can become 26 | const normalizeTokens = (tokens: Array): Token[][] => { 27 | const typeArrStack = [[]]; 28 | const tokenArrStack = [tokens]; 29 | const tokenArrIndexStack = [0]; 30 | const tokenArrSizeStack = [tokens.length]; 31 | 32 | let i = 0; 33 | let stackIndex = 0; 34 | let currentLine = []; 35 | 36 | const acc = [currentLine]; 37 | 38 | while (stackIndex > -1) { 39 | while ( 40 | (i = tokenArrIndexStack[stackIndex]++) < tokenArrSizeStack[stackIndex] 41 | ) { 42 | let content; 43 | let types = typeArrStack[stackIndex]; 44 | const tokenArr = tokenArrStack[stackIndex]; 45 | const token = tokenArr[i]; 46 | 47 | // Determine content and append type to types if necessary 48 | if (typeof token === "string") { 49 | types = stackIndex > 0 ? types : ["plain"]; 50 | content = token; 51 | } else { 52 | types = types[0] === token.type ? types : types.concat(token.type); 53 | content = token.content; 54 | } 55 | 56 | // If token.content is an array, increase the stack depth and repeat this while-loop 57 | if (typeof content !== "string") { 58 | stackIndex++; 59 | typeArrStack.push(types); 60 | tokenArrStack.push(content); 61 | tokenArrIndexStack.push(0); 62 | tokenArrSizeStack.push(content.length); 63 | continue; 64 | } 65 | 66 | // Split by newlines 67 | const splitByNewlines = content.split(newlineRe); 68 | const newlineCount = splitByNewlines.length; 69 | 70 | currentLine.push({ types, content: splitByNewlines[0] }); 71 | 72 | // Create a new line for each string on a new line 73 | for (let i = 1; i < newlineCount; i++) { 74 | normalizeEmptyLines(currentLine); 75 | acc.push((currentLine = [])); 76 | currentLine.push({ types, content: splitByNewlines[i] }); 77 | } 78 | } 79 | 80 | // Decreate the stack depth 81 | stackIndex--; 82 | typeArrStack.pop(); 83 | tokenArrStack.pop(); 84 | tokenArrIndexStack.pop(); 85 | tokenArrSizeStack.pop(); 86 | } 87 | 88 | normalizeEmptyLines(currentLine); 89 | return acc; 90 | }; 91 | 92 | export default normalizeTokens; 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prism-react-renderer", 3 | "version": "0.1.3", 4 | "description": "Renders highlighted Prism output using React", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "license": "MIT", 8 | "files": [ 9 | "es", 10 | "lib", 11 | "prism", 12 | "themes" 13 | ], 14 | "scripts": { 15 | "precommit": "lint-staged", 16 | "build": "run-s build:lib build:es", 17 | "build:lib": "cross-env BABEL_ENV=cjs babel --ignore '**/__tests__/*' --out-dir lib src", 18 | "build:es": "cross-env BABEL_ENV=es babel --ignore '**/__tests__/*' --out-dir es src", 19 | "test": "jest", 20 | "flow": "flow check", 21 | "format": "prettier --write src/**/*.js themes/**/*.js", 22 | "prepublishOnly": "run-p flow build" 23 | }, 24 | "lint-staged": { 25 | "linters": { 26 | "{src,themes}/**/*.js": [ 27 | "jest --bail --findRelatedTests", 28 | "flow focus-check", 29 | "prettier --write", 30 | "git add" 31 | ] 32 | } 33 | }, 34 | "babel": { 35 | "env": { 36 | "test": { 37 | "presets": [ 38 | "@babel/preset-flow", 39 | "@babel/preset-react", 40 | [ 41 | "@babel/preset-env", 42 | { 43 | "loose": true, 44 | "modules": "commonjs" 45 | } 46 | ] 47 | ], 48 | "plugins": [ 49 | "babel-plugin-macros" 50 | ] 51 | }, 52 | "cjs": { 53 | "presets": [ 54 | "@babel/preset-flow", 55 | "@babel/preset-react", 56 | [ 57 | "@babel/preset-env", 58 | { 59 | "loose": true, 60 | "modules": "commonjs" 61 | } 62 | ] 63 | ], 64 | "plugins": [ 65 | "babel-plugin-macros" 66 | ] 67 | }, 68 | "es": { 69 | "presets": [ 70 | "@babel/preset-flow", 71 | "@babel/preset-react", 72 | [ 73 | "@babel/preset-env", 74 | { 75 | "loose": true, 76 | "modules": false 77 | } 78 | ] 79 | ], 80 | "plugins": [ 81 | "babel-plugin-macros" 82 | ] 83 | } 84 | }, 85 | "plugins": [ 86 | [ 87 | "@babel/plugin-proposal-class-properties", 88 | { 89 | "loose": true 90 | } 91 | ] 92 | ] 93 | }, 94 | "peerDependencies": { 95 | "react": ">=0.14.9" 96 | }, 97 | "devDependencies": { 98 | "@babel/cli": "^7.0.0-rc.1", 99 | "@babel/core": "^7.0.0-rc.1", 100 | "@babel/plugin-proposal-class-properties": "^7.0.0-rc.1", 101 | "@babel/preset-env": "^7.0.0-rc.1", 102 | "@babel/preset-flow": "^7.0.0-rc.1", 103 | "@babel/preset-react": "^7.0.0-rc.1", 104 | "babel-core": "^7.0.0-bridge.0", 105 | "babel-jest": "^23.4.2", 106 | "babel-plugin-macros": "^2.4.0", 107 | "codegen.macro": "^3.0.0", 108 | "cross-env": "^5.2.0", 109 | "flow-bin": "^0.79.0", 110 | "husky": "^0.14.3", 111 | "jest": "^23.5.0", 112 | "lint-staged": "^7.2.2", 113 | "npm-run-all": "^4.1.3", 114 | "prettier": "^1.14.2", 115 | "prismjs": "^1.15.0", 116 | "react": "^16.4.2", 117 | "react-dom": "^16.4.2", 118 | "react-testing-library": "^5.0.0" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /themes/oceanicNext.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Oceanic Next 3 | // Author: Dmitri Voronianski (https://github.com/voronianski) 4 | // https://github.com/voronianski/oceanic-next-color-scheme 5 | // Adapted from: https://github.com/reactjs/reactjs.org/blob/428d52b/src/prism-styles.js 6 | 7 | /*:: import type { PrismTheme } from '../src/types' */ 8 | 9 | var colors = { 10 | char: "#D8DEE9", 11 | comment: "#999999", 12 | keyword: "#c5a5c5", 13 | primitive: "#5a9bcf", 14 | string: "#8dc891", 15 | variable: "#d7deea", 16 | boolean: "#ff8b50", 17 | punctuation: "#5FB3B3", 18 | tag: "#fc929e", 19 | function: "#79b6f2", 20 | className: "#FAC863", 21 | method: "#6699CC", 22 | operator: "#fc929e", 23 | } 24 | 25 | var theme /*: PrismTheme */ = { 26 | plain: { 27 | backgroundColor: "#282c34", 28 | color: "#ffffff", 29 | }, 30 | styles: [ 31 | { 32 | types: ["attr-name"], 33 | style: { 34 | color: colors.keyword, 35 | }, 36 | }, 37 | { 38 | types: ["attr-value"], 39 | style: { 40 | color: colors.string, 41 | }, 42 | }, 43 | { 44 | types: ["comment", "block-comment", "prolog", "doctype", "cdata"], 45 | style: { 46 | color: colors.comment, 47 | }, 48 | }, 49 | { 50 | types: [ 51 | "property", 52 | "number", 53 | "function-name", 54 | "constant", 55 | "symbol", 56 | "deleted", 57 | ], 58 | style: { 59 | color: colors.primitive, 60 | }, 61 | }, 62 | { 63 | types: ["boolean"], 64 | style: { 65 | color: colors.boolean, 66 | }, 67 | }, 68 | { 69 | types: ["tag"], 70 | style: { 71 | color: colors.tag, 72 | }, 73 | }, 74 | { 75 | types: ["string"], 76 | style: { 77 | color: colors.string, 78 | }, 79 | }, 80 | { 81 | types: ["punctuation"], 82 | style: { 83 | color: colors.string, 84 | }, 85 | }, 86 | { 87 | types: ["selector", "char", "builtin", "inserted"], 88 | style: { 89 | color: colors.char, 90 | }, 91 | }, 92 | { 93 | types: ["function"], 94 | style: { 95 | color: colors.function, 96 | }, 97 | }, 98 | { 99 | types: ["operator", "entity", "url", "variable"], 100 | style: { 101 | color: colors.variable, 102 | }, 103 | }, 104 | { 105 | types: ["keyword"], 106 | style: { 107 | color: colors.keyword, 108 | }, 109 | }, 110 | { 111 | types: ["at-rule", "class-name"], 112 | style: { 113 | color: colors.className, 114 | }, 115 | }, 116 | { 117 | types: ["important"], 118 | style: { 119 | fontWeight: "400", 120 | }, 121 | }, 122 | { 123 | types: ["bold"], 124 | style: { 125 | fontWeight: "bold", 126 | }, 127 | }, 128 | { 129 | types: ["italic"], 130 | style: { 131 | fontStyle: "italic", 132 | }, 133 | }, 134 | { 135 | types: ["namespace"], 136 | style: { 137 | opacity: 0.7, 138 | }, 139 | }, 140 | ], 141 | } 142 | 143 | module.exports = theme 144 | -------------------------------------------------------------------------------- /src/components/Highlight.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { Component, type Node } from "react"; 4 | import normalizeTokens from "../utils/normalizeTokens"; 5 | import themeToDict, { type ThemeDict } from "../utils/themeToDict"; 6 | 7 | import type { 8 | Language, 9 | Token, 10 | LineInputProps, 11 | LineOutputProps, 12 | TokenInputProps, 13 | TokenOutputProps, 14 | RenderProps, 15 | PrismLib, 16 | PrismTheme 17 | } from "../types"; 18 | 19 | type Props = { 20 | Prism: PrismLib, 21 | theme?: PrismTheme, 22 | language: Language, 23 | code: string, 24 | children: (props: RenderProps) => Node 25 | }; 26 | 27 | class Highlight extends Component { 28 | themeDict: ThemeDict | void; 29 | 30 | constructor(props: Props) { 31 | super(props); 32 | if (props.theme) { 33 | this.themeDict = themeToDict(props.theme, props.language); 34 | } 35 | } 36 | 37 | getLineProps = ({ 38 | key, 39 | className, 40 | style, 41 | line, 42 | ...rest 43 | }: LineInputProps): LineOutputProps => { 44 | const output: LineOutputProps = { 45 | ...rest, 46 | className: "token-line", 47 | style: undefined, 48 | key: undefined 49 | }; 50 | 51 | if (this.themeDict !== undefined) { 52 | output.style = this.themeDict.plain; 53 | } 54 | 55 | if (style !== undefined) { 56 | output.style = 57 | output.style !== undefined ? { ...output.style, ...style } : style; 58 | } 59 | 60 | if (key !== undefined) output.key = key; 61 | if (className) output.className += ` ${className}`; 62 | 63 | return output; 64 | }; 65 | 66 | getStyleForToken = ({ types, empty }: Token) => { 67 | const typesSize = types.length; 68 | 69 | if (this.themeDict === undefined) { 70 | return undefined; 71 | } else if (typesSize === 1 && types[0] === "plain") { 72 | return empty ? { display: "inline-block" } : undefined; 73 | } else if (typesSize === 1 && !empty) { 74 | return this.themeDict[types[0]]; 75 | } 76 | 77 | const baseStyle = empty ? { display: "inline-block" } : {}; 78 | // $FlowFixMe 79 | const typeStyles = types.map(type => this.themeDict[type]); 80 | return Object.assign(baseStyle, ...typeStyles); 81 | }; 82 | 83 | getTokenProps = ({ 84 | key, 85 | className, 86 | style, 87 | token, 88 | ...rest 89 | }: TokenInputProps): TokenOutputProps => { 90 | const output: TokenOutputProps = { 91 | ...rest, 92 | className: `token ${token.types.join(" ")}`, 93 | children: token.content, 94 | style: this.getStyleForToken(token), 95 | key: undefined 96 | }; 97 | 98 | if (style !== undefined) { 99 | output.style = 100 | output.style !== undefined ? { ...output.style, ...style } : style; 101 | } 102 | 103 | if (key !== undefined) output.key = key; 104 | if (className) output.className += ` ${className}`; 105 | 106 | return output; 107 | }; 108 | 109 | render() { 110 | const { Prism, language, code, children } = this.props; 111 | 112 | const grammar = Prism.languages[language]; 113 | const mixedTokens = 114 | grammar !== undefined ? Prism.tokenize(code, grammar, language) : [code]; 115 | const tokens = normalizeTokens(mixedTokens); 116 | 117 | return children({ 118 | tokens, 119 | className: `prism-code language-${language}`, 120 | style: this.themeDict ? this.themeDict.root : {}, 121 | getLineProps: this.getLineProps, 122 | getTokenProps: this.getTokenProps 123 | }); 124 | } 125 | } 126 | 127 | export default Highlight; 128 | -------------------------------------------------------------------------------- /tools/themeFromVsCode/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | color-convert@^1.9.1: 6 | version "1.9.2" 7 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147" 8 | dependencies: 9 | color-name "1.1.1" 10 | 11 | color-name@1.1.1: 12 | version "1.1.1" 13 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689" 14 | 15 | color-name@^1.0.0: 16 | version "1.1.3" 17 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 18 | 19 | color-string@^1.5.2: 20 | version "1.5.3" 21 | resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" 22 | dependencies: 23 | color-name "^1.0.0" 24 | simple-swizzle "^0.2.2" 25 | 26 | color@^3.0.0: 27 | version "3.0.0" 28 | resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" 29 | dependencies: 30 | color-convert "^1.9.1" 31 | color-string "^1.5.2" 32 | 33 | define-properties@^1.1.2: 34 | version "1.1.3" 35 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" 36 | dependencies: 37 | object-keys "^1.0.12" 38 | 39 | es-abstract@^1.6.1: 40 | version "1.12.0" 41 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" 42 | dependencies: 43 | es-to-primitive "^1.1.1" 44 | function-bind "^1.1.1" 45 | has "^1.0.1" 46 | is-callable "^1.1.3" 47 | is-regex "^1.0.4" 48 | 49 | es-to-primitive@^1.1.1: 50 | version "1.1.1" 51 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" 52 | dependencies: 53 | is-callable "^1.1.1" 54 | is-date-object "^1.0.1" 55 | is-symbol "^1.0.1" 56 | 57 | function-bind@^1.1.0, function-bind@^1.1.1: 58 | version "1.1.1" 59 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 60 | 61 | has@^1.0.1: 62 | version "1.0.3" 63 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 64 | dependencies: 65 | function-bind "^1.1.1" 66 | 67 | is-arrayish@^0.3.1: 68 | version "0.3.2" 69 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" 70 | 71 | is-arrow-function@^2.0.3: 72 | version "2.0.3" 73 | resolved "https://registry.yarnpkg.com/is-arrow-function/-/is-arrow-function-2.0.3.tgz#29be2c2d8d9450852b8bbafb635ba7b8d8e87ec2" 74 | dependencies: 75 | is-callable "^1.0.4" 76 | 77 | is-boolean-object@^1.0.0: 78 | version "1.0.0" 79 | resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" 80 | 81 | is-callable@^1.0.4, is-callable@^1.1.1, is-callable@^1.1.3: 82 | version "1.1.4" 83 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" 84 | 85 | is-date-object@^1.0.1: 86 | version "1.0.1" 87 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" 88 | 89 | is-equal@^1.5.5: 90 | version "1.5.5" 91 | resolved "https://registry.yarnpkg.com/is-equal/-/is-equal-1.5.5.tgz#5e85f1957e052883247feb386965a3bba15fbb3d" 92 | dependencies: 93 | has "^1.0.1" 94 | is-arrow-function "^2.0.3" 95 | is-boolean-object "^1.0.0" 96 | is-callable "^1.1.3" 97 | is-date-object "^1.0.1" 98 | is-generator-function "^1.0.6" 99 | is-number-object "^1.0.3" 100 | is-regex "^1.0.3" 101 | is-string "^1.0.4" 102 | is-symbol "^1.0.1" 103 | object.entries "^1.0.4" 104 | 105 | is-generator-function@^1.0.6: 106 | version "1.0.7" 107 | resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" 108 | 109 | is-number-object@^1.0.3: 110 | version "1.0.3" 111 | resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799" 112 | 113 | is-regex@^1.0.3, is-regex@^1.0.4: 114 | version "1.0.4" 115 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 116 | dependencies: 117 | has "^1.0.1" 118 | 119 | is-string@^1.0.4: 120 | version "1.0.4" 121 | resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64" 122 | 123 | is-symbol@^1.0.1: 124 | version "1.0.1" 125 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" 126 | 127 | object-keys@^1.0.12: 128 | version "1.0.12" 129 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" 130 | 131 | object.entries@^1.0.4: 132 | version "1.0.4" 133 | resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f" 134 | dependencies: 135 | define-properties "^1.1.2" 136 | es-abstract "^1.6.1" 137 | function-bind "^1.1.0" 138 | has "^1.0.1" 139 | 140 | simple-swizzle@^0.2.2: 141 | version "0.2.2" 142 | resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" 143 | dependencies: 144 | is-arrayish "^0.3.1" 145 | -------------------------------------------------------------------------------- /src/components/__tests__/Highlight.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, cleanup } from "react-testing-library"; 3 | 4 | import Highlight from "../Highlight"; 5 | import defaultProps from "../../defaultProps"; 6 | 7 | const exampleCode = ` 8 | (function someDemo() { 9 | var test = "Hello World!"; 10 | console.log(test); 11 | })(); 12 | 13 | return () => ; 14 | `.trim(); 15 | 16 | describe("", () => { 17 | afterEach(cleanup); 18 | 19 | describe("snapshots", () => { 20 | it("renders correctly", () => { 21 | const { container } = render( 22 | 23 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 24 |
 25 |               {tokens.map((line, i) => (
 26 |                 
27 | {line.map((token, key) => ( 28 | 29 | ))} 30 |
31 | ))} 32 |
33 | )} 34 |
35 | ); 36 | 37 | expect(container).toMatchSnapshot(); 38 | }); 39 | 40 | it("renders unsupported languages correctly", () => { 41 | const { container } = render( 42 | 47 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 48 |
 49 |               {tokens.map((line, i) => (
 50 |                 
51 | {line.map((token, key) => ( 52 | 53 | ))} 54 |
55 | ))} 56 |
57 | )} 58 |
59 | ); 60 | 61 | expect(container).toMatchSnapshot(); 62 | }); 63 | 64 | it("renders without style props when no theme is passed", () => { 65 | const { container } = render( 66 | 72 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 73 |
 74 |               {tokens.map((line, i) => (
 75 |                 
76 | {line.map((token, key) => ( 77 | 78 | ))} 79 |
80 | ))} 81 |
82 | )} 83 |
84 | ); 85 | 86 | expect(container.innerHTML.includes("style")).toBeFalsy(); 87 | }); 88 | }); 89 | 90 | describe("getLineProps", () => { 91 | it("transforms lineProps inputs correctly", () => { 92 | const input = { 93 | key: "line-1", 94 | style: { cssProp: "test" }, 95 | className: "line-class", 96 | line: [{ types: ["punctuation"], content: "!" }], 97 | restPropsTest: true 98 | }; 99 | 100 | render( 101 | 102 | {({ getLineProps }) => { 103 | const output = getLineProps(input); 104 | 105 | expect(output).toEqual({ 106 | key: "line-1", 107 | style: { 108 | cssProp: "test", 109 | backgroundColor: null, 110 | color: expect.any(String) 111 | }, 112 | className: "token-line line-class", 113 | restPropsTest: true 114 | }); 115 | 116 | return null; 117 | }} 118 | 119 | ); 120 | }); 121 | }); 122 | 123 | describe("getTokenProps", () => { 124 | it("transforms tokenProps inputs correctly", () => { 125 | const input = { 126 | key: "token-1", 127 | style: { cssProp: "test" }, 128 | className: "token-class", 129 | token: { types: ["punctuation"], content: "!" }, 130 | restPropsTest: true 131 | }; 132 | 133 | render( 134 | 135 | {({ getTokenProps }) => { 136 | const output = getTokenProps(input); 137 | 138 | expect(output).toEqual({ 139 | key: "token-1", 140 | style: { cssProp: "test", color: expect.any(String) }, 141 | className: "token punctuation token-class", 142 | restPropsTest: true, 143 | children: "!" 144 | }); 145 | 146 | return null; 147 | }} 148 | 149 | ); 150 | }); 151 | 152 | it("transforms constructor token style correctly", () => { 153 | // From https://github.com/FormidableLabs/prism-react-renderer/issues/11 154 | render( 155 | 156 | {({ tokens, getTokenProps }) => { 157 | const line = tokens[0]; 158 | const token = line[2]; 159 | const output = getTokenProps({ token, key: 2 }); 160 | 161 | expect(typeof output.style).not.toBe("function"); 162 | 163 | return null; 164 | }} 165 | 166 | ); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/utils/__tests__/normalizeTokens.test.js: -------------------------------------------------------------------------------- 1 | import normalizeTokens from "../normalizeTokens"; 2 | 3 | describe("normalizeTokens", () => { 4 | it("handles plain strings", () => { 5 | const input = ["hello", "world"]; 6 | const output = normalizeTokens(input); 7 | 8 | expect(output).toEqual([ 9 | [ 10 | { types: ["plain"], content: "hello" }, 11 | { types: ["plain"], content: "world" } 12 | ] 13 | ]); 14 | }); 15 | 16 | it("handles flat tokens", () => { 17 | const input = [ 18 | { type: "test1", content: "hello" }, 19 | { type: "test2", content: "world" } 20 | ]; 21 | const output = normalizeTokens(input); 22 | 23 | expect(output).toEqual([ 24 | [ 25 | { types: ["test1"], content: "hello" }, 26 | { types: ["test2"], content: "world" } 27 | ] 28 | ]); 29 | }); 30 | 31 | it("handles nested tokens", () => { 32 | const input = [ 33 | { 34 | type: "test1", 35 | content: [ 36 | { type: "nest1", content: "he" }, 37 | { type: "nest2", content: "llo" } 38 | ] 39 | }, 40 | { type: "test2", content: "world" } 41 | ]; 42 | const output = normalizeTokens(input); 43 | 44 | expect(output).toEqual([ 45 | [ 46 | { types: ["test1", "nest1"], content: "he" }, 47 | { types: ["test1", "nest2"], content: "llo" }, 48 | { types: ["test2"], content: "world" } 49 | ] 50 | ]); 51 | }); 52 | 53 | it("handles nested & mixed tokens", () => { 54 | const input = [ 55 | { 56 | type: "test1", 57 | content: [{ type: "nest", content: "he" }, "llo"] 58 | }, 59 | { type: "test2", content: "world" }, 60 | "!" 61 | ]; 62 | const output = normalizeTokens(input); 63 | 64 | expect(output).toEqual([ 65 | [ 66 | { types: ["test1", "nest"], content: "he" }, 67 | { types: ["test1"], content: "llo" }, 68 | { types: ["test2"], content: "world" }, 69 | { types: ["plain"], content: "!" } 70 | ] 71 | ]); 72 | }); 73 | 74 | it("handles deeply nested tokens", () => { 75 | const input = [ 76 | { 77 | type: "1", 78 | content: [ 79 | { 80 | type: "2", 81 | content: [{ type: "3", content: "hello" }] 82 | } 83 | ] 84 | } 85 | ]; 86 | const output = normalizeTokens(input); 87 | 88 | expect(output).toEqual([[{ types: ["1", "2", "3"], content: "hello" }]]); 89 | }); 90 | 91 | it("handles plain strings with newlines", () => { 92 | const input = ["hello", " \nworld"]; 93 | const output = normalizeTokens(input); 94 | 95 | expect(output).toEqual([ 96 | [ 97 | { types: ["plain"], content: "hello" }, 98 | { types: ["plain"], content: " " } 99 | ], 100 | [{ types: ["plain"], content: "world" }] 101 | ]); 102 | }); 103 | 104 | it("handles flat tokens with newlines", () => { 105 | const input = [ 106 | { type: "test1", content: "hello" }, 107 | { type: "test2", content: "wor\nld" } 108 | ]; 109 | const output = normalizeTokens(input); 110 | 111 | expect(output).toEqual([ 112 | [ 113 | { types: ["test1"], content: "hello" }, 114 | { types: ["test2"], content: "wor" } 115 | ], 116 | [{ types: ["test2"], content: "ld" }] 117 | ]); 118 | }); 119 | 120 | it("handles nested tokens with newlines", () => { 121 | const input = [ 122 | { 123 | type: "test1", 124 | content: [ 125 | { type: "nest1", content: "he" }, 126 | { type: "nest2", content: "l\nlo" } 127 | ] 128 | }, 129 | { type: "test2", content: "wor\nld" } 130 | ]; 131 | const output = normalizeTokens(input); 132 | 133 | expect(output).toEqual([ 134 | [ 135 | { types: ["test1", "nest1"], content: "he" }, 136 | { types: ["test1", "nest2"], content: "l" } 137 | ], 138 | [ 139 | { types: ["test1", "nest2"], content: "lo" }, 140 | { types: ["test2"], content: "wor" } 141 | ], 142 | [{ types: ["test2"], content: "ld" }] 143 | ]); 144 | }); 145 | 146 | it("handles nested & mixed tokens with newlines", () => { 147 | const input = [ 148 | { 149 | type: "test1", 150 | content: [{ type: "nest", content: "h\ne" }, "l\nlo"] 151 | }, 152 | "world\n!" 153 | ]; 154 | const output = normalizeTokens(input); 155 | 156 | expect(output).toEqual([ 157 | [{ types: ["test1", "nest"], content: "h" }], 158 | [ 159 | { types: ["test1", "nest"], content: "e" }, 160 | { types: ["test1"], content: "l" } 161 | ], 162 | [ 163 | { types: ["test1"], content: "lo" }, 164 | { types: ["plain"], content: "world" } 165 | ], 166 | [{ types: ["plain"], content: "!" }] 167 | ]); 168 | }); 169 | 170 | it("handles deeply nested tokens with newlines", () => { 171 | const input = [ 172 | { 173 | type: "1", 174 | content: [ 175 | { 176 | type: "2", 177 | content: [{ type: "3", content: "hel\nlo" }] 178 | } 179 | ] 180 | } 181 | ]; 182 | const output = normalizeTokens(input); 183 | 184 | expect(output).toEqual([ 185 | [{ types: ["1", "2", "3"], content: "hel" }], 186 | [{ types: ["1", "2", "3"], content: "lo" }] 187 | ]); 188 | }); 189 | 190 | it("handles empty lines gracefully", () => { 191 | const input = ["\n\n"]; 192 | const output = normalizeTokens(input); 193 | 194 | expect(output).toEqual([ 195 | [{ types: ["plain"], content: "", empty: true }], 196 | [{ types: ["plain"], content: "", empty: true }], 197 | [{ types: ["plain"], content: "", empty: true }] 198 | ]); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Highlight.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` snapshots renders correctly 1`] = ` 4 |
5 |
  9 |     
13 | 17 | ( 18 | 19 | 23 | function 24 | 25 | 28 | 29 | 30 | 34 | someDemo 35 | 36 | 40 | ( 41 | 42 | 46 | ) 47 | 48 | 51 | 52 | 53 | 57 | { 58 | 59 | 62 |
63 |
67 | 70 | 71 | 72 | 76 | var 77 | 78 | 81 | test 82 | 83 | 87 | = 88 | 89 | 92 | 93 | 94 | 98 | "Hello World!" 99 | 100 | 104 | ; 105 | 106 | 109 |
110 |
114 | 117 | console 118 | 119 | 123 | . 124 | 125 | 129 | log 130 | 131 | 135 | ( 136 | 137 | 140 | test 141 | 142 | 146 | ) 147 | 148 | 152 | ; 153 | 154 | 157 |
158 |
162 | 165 | 169 | } 170 | 171 | 175 | ) 176 | 177 | 181 | ( 182 | 183 | 187 | ) 188 | 189 | 193 | ; 194 | 195 | 198 |
199 |
203 | 207 |
208 |
212 | 215 | 219 | return 220 | 221 | 224 | 225 | 226 | 230 | ( 231 | 232 | 236 | ) 237 | 238 | 241 | 242 | 243 | 247 | => 248 | 249 | 252 | 253 | 254 | 258 | < 259 | 260 | 264 | App 265 | 266 | 270 | 271 | 272 | 276 | /> 277 | 278 | 282 | ; 283 | 284 |
285 |
286 |
287 | `; 288 | 289 | exports[` snapshots renders unsupported languages correctly 1`] = ` 290 |
291 |
295 |     
299 | 302 | (function someDemo() { 303 | 304 |
305 |
309 | 312 | var test = "Hello World!"; 313 | 314 |
315 |
319 | 322 | console.log(test); 323 | 324 |
325 |
329 | 332 | })(); 333 | 334 |
335 |
339 | 343 |
344 |
348 | 351 | return () => <App />; 352 | 353 |
354 |
355 |
356 | `; 357 | -------------------------------------------------------------------------------- /src/vendor/prism/prism-core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */ 6 | 7 | /** 8 | * prism-react-renderer: 9 | * This file has been modified to remove: 10 | * - globals and window dependency 11 | * - worker support 12 | * - highlightAll and other element dependent methods 13 | * - _.hooks helpers 14 | * - UMD/node-specific hacks 15 | * It has also been run through prettier 16 | */ 17 | 18 | var Prism = (function() { 19 | // Private helper vars 20 | var lang = /\blang(?:uage)?-([\w-]+)\b/i 21 | var uniqueId = 0 22 | 23 | var _ = { 24 | util: { 25 | encode: function(tokens) { 26 | if (tokens instanceof Token) { 27 | return new Token( 28 | tokens.type, 29 | _.util.encode(tokens.content), 30 | tokens.alias 31 | ) 32 | } else if (_.util.type(tokens) === "Array") { 33 | return tokens.map(_.util.encode) 34 | } else { 35 | return tokens 36 | .replace(/&/g, "&") 37 | .replace(/ text.length) { 239 | // Something went terribly wrong, ABORT, ABORT! 240 | return 241 | } 242 | 243 | if (str instanceof Token) { 244 | continue 245 | } 246 | 247 | if (greedy && i != strarr.length - 1) { 248 | pattern.lastIndex = pos 249 | var match = pattern.exec(text) 250 | if (!match) { 251 | break 252 | } 253 | 254 | var from = match.index + (lookbehind ? match[1].length : 0), 255 | to = match.index + match[0].length, 256 | k = i, 257 | p = pos 258 | 259 | for ( 260 | var len = strarr.length; 261 | k < len && 262 | (p < to || (!strarr[k].type && !strarr[k - 1].greedy)); 263 | ++k 264 | ) { 265 | p += strarr[k].length 266 | // Move the index i to the element in strarr that is closest to from 267 | if (from >= p) { 268 | ++i 269 | pos = p 270 | } 271 | } 272 | 273 | // If strarr[i] is a Token, then the match starts inside another Token, which is invalid 274 | if (strarr[i] instanceof Token) { 275 | continue 276 | } 277 | 278 | // Number of tokens to delete and replace with the new match 279 | delNum = k - i 280 | str = text.slice(pos, p) 281 | match.index -= pos 282 | } else { 283 | pattern.lastIndex = 0 284 | 285 | var match = pattern.exec(str), 286 | delNum = 1 287 | } 288 | 289 | if (!match) { 290 | if (oneshot) { 291 | break 292 | } 293 | 294 | continue 295 | } 296 | 297 | if (lookbehind) { 298 | lookbehindLength = match[1] ? match[1].length : 0 299 | } 300 | 301 | var from = match.index + lookbehindLength, 302 | match = match[0].slice(lookbehindLength), 303 | to = from + match.length, 304 | before = str.slice(0, from), 305 | after = str.slice(to) 306 | 307 | var args = [i, delNum] 308 | 309 | if (before) { 310 | ++i 311 | pos += before.length 312 | args.push(before) 313 | } 314 | 315 | var wrapped = new Token( 316 | token, 317 | inside ? _.tokenize(match, inside) : match, 318 | alias, 319 | match, 320 | greedy 321 | ) 322 | 323 | args.push(wrapped) 324 | 325 | if (after) { 326 | args.push(after) 327 | } 328 | 329 | Array.prototype.splice.apply(strarr, args) 330 | 331 | if (delNum != 1) 332 | _.matchGrammar(text, strarr, grammar, i, pos, true, token) 333 | 334 | if (oneshot) break 335 | } 336 | } 337 | } 338 | }, 339 | 340 | hooks: { 341 | add: function () {} 342 | }, 343 | 344 | tokenize: function(text, grammar, language) { 345 | var strarr = [text] 346 | 347 | var rest = grammar.rest 348 | 349 | if (rest) { 350 | for (var token in rest) { 351 | grammar[token] = rest[token] 352 | } 353 | 354 | delete grammar.rest 355 | } 356 | 357 | _.matchGrammar(text, strarr, grammar, 0, 0, false) 358 | 359 | return strarr 360 | }, 361 | } 362 | 363 | var Token = (_.Token = function(type, content, alias, matchedStr, greedy) { 364 | this.type = type 365 | this.content = content 366 | this.alias = alias 367 | // Copy of the full string this token was created from 368 | this.length = (matchedStr || "").length | 0 369 | this.greedy = !!greedy 370 | }) 371 | 372 | Token.stringify = function(o, language, parent) { 373 | if (typeof o == "string") { 374 | return o 375 | } 376 | 377 | if (_.util.type(o) === "Array") { 378 | return o 379 | .map(function(element) { 380 | return Token.stringify(element, language, o) 381 | }) 382 | .join("") 383 | } 384 | 385 | var env = { 386 | type: o.type, 387 | content: Token.stringify(o.content, language, parent), 388 | tag: "span", 389 | classes: ["token", o.type], 390 | attributes: {}, 391 | language: language, 392 | parent: parent, 393 | } 394 | 395 | if (o.alias) { 396 | var aliases = _.util.type(o.alias) === "Array" ? o.alias : [o.alias] 397 | Array.prototype.push.apply(env.classes, aliases) 398 | } 399 | 400 | var attributes = Object.keys(env.attributes) 401 | .map(function(name) { 402 | return ( 403 | name + 404 | '="' + 405 | (env.attributes[name] || "").replace(/"/g, """) + 406 | '"' 407 | ) 408 | }) 409 | .join(" ") 410 | 411 | return ( 412 | "<" + 413 | env.tag + 414 | ' class="' + 415 | env.classes.join(" ") + 416 | '"' + 417 | (attributes ? " " + attributes : "") + 418 | ">" + 419 | env.content + 420 | "" 423 | ) 424 | } 425 | 426 | return _ 427 | })() 428 | 429 | module.exports = Prism 430 | Prism.default = Prism 431 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | prism-react-renderer 🖌️ 3 |
4 |

5 |

6 | A lean Prism highlighter component for React
7 | Comes with everything to render Prismjs highlighted code directly to React (Native) elements, global-pollution-free! 8 |

9 | 10 | ## Why? 11 | 12 | Maybe you need to render some extra UI with your Prismjs-highlighted code, 13 | or maybe you'd like to manipulate what Prism renders completely, 14 | or maybe you're just using Prism with React and are searching for an easier, 15 | global-pollution-free way? 16 | 17 | Then you're right where you want to be! 18 | 19 | ## How? 20 | 21 | This library tokenises code using Prism and provides a small render-props-driven 22 | component to quickly render it out into React. This is why it even works with 23 | React Native! It's bundled with a modified version of Prism that won't pollute 24 | the global namespace and comes with 25 | [a couple of common language syntaxes](./src/vendor/prism/includeLangs.js). 26 | 27 | _(There's also an [escape-hatch](https://github.com/FormidableLabs/prism-react-renderer#prism) to use your own Prism setup, just in case)_ 28 | 29 | It also comes with its own [VSCode-like theming format](#theming), which means by default 30 | you can easily drop in different themes, use the ones this library ships with, or 31 | create new ones programmatically on the fly. 32 | 33 | _(If you just want to use your Prism CSS-file themes, that's also no problem)_ 34 | 35 | ## Table of Contents 36 | 37 | 38 | 39 | 40 | - [Installation](#installation) 41 | - [Usage](#usage) 42 | - [Basic Props](#basic-props) 43 | - [children](#children) 44 | - [language](#language) 45 | - [code](#code) 46 | - [Advanced Props](#advanced-props) 47 | - [theme](#theme) 48 | - [Prism](#prism) 49 | - [Children Function](#children-function) 50 | - [state](#state) 51 | - [prop getters](#prop-getters) 52 | - [`getLineProps`](#getlineprops) 53 | - [`getTokenProps`](#gettokenprops) 54 | - [Theming](#theming) 55 | - [FAQ](#faq) 56 | - [LICENSE](#license) 57 | 58 | 59 | 60 | ## Installation 61 | 62 | This module is distributed via npm which is bundled with node and 63 | should be installed as one of your project's `dependencies`: 64 | 65 | ```sh 66 | # npm 67 | npm install --save prism-react-renderer 68 | # yarn 69 | yarn add prism-react-renderer 70 | ``` 71 | 72 | > This package also depends on `react`. Please make sure you 73 | > have those installed as well. 74 | 75 | ## Usage 76 | 77 | > [Try it out in the browser](https://codesandbox.io/s/00o4wx0jqv) 78 | 79 | ```jsx 80 | import React from "react"; 81 | import { render } from "react-dom"; 82 | import Highlight, { defaultProps } from "prism-react-renderer"; 83 | 84 | const exampleCode = ` 85 | (function someDemo() { 86 | var test = "Hello World!"; 87 | console.log(test); 88 | })(); 89 | 90 | return () => ; 91 | `; 92 | 93 | render(( 94 | 95 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 96 |
 97 |         {tokens.map((line, i) => (
 98 |           
99 | {line.map((token, key) => ( 100 | 101 | ))} 102 |
103 | ))} 104 |
105 | )} 106 |
, 107 | document.getElementById('root') 108 | ); 109 | ``` 110 | 111 | `` is the only component exposed by this package, as inspired by 112 | [downshift](https://github.com/paypal/downshift). 113 | 114 | It also exports a `defaultProps` object which for basic usage can simply be spread 115 | onto the `` component. It also provides some default theming. 116 | 117 | It doesn't render anything itself, it just calls the render function and renders that. 118 | ["Use a render prop!"](https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce)! 119 | `{highlight =>
/* your JSX here! */
}
` 120 | 121 | ## Basic Props 122 | 123 | This is the list of props that you should probably know about. There are some 124 | [advanced props](#advanced-props) below as well. 125 | 126 | Most of these [advanced props](#advanced-props) are included in the `defaultProps`. 127 | 128 | ### children 129 | 130 | > `function({})` | _required_ 131 | 132 | This is called with an object. Read more about the properties of this object in 133 | the section "[Children Function](#children-function)". 134 | 135 | ### language 136 | 137 | > `string` | _required_ 138 | 139 | This is the language that your code will be highlighted as. You can see a list 140 | of all languages that are supported out of the box [here](./src/vendor/prism/includeLangs.js). 141 | 142 | ### code 143 | 144 | > `string` | _required_ 145 | 146 | This is the code that will be highlighted. 147 | 148 | ## Advanced Props 149 | 150 | ### theme 151 | 152 | > `PrismTheme` | _required; default is provided in `defaultProps` export_ 153 | 154 | If a theme is passed, it is used to generate style props which can be retrieved 155 | via the prop-getters which are described in "[Children Function](#children-function)". 156 | 157 | A default theme is provided by the `defaultProps` object. 158 | 159 | Read more about how to theme `react-prism-renderer` in 160 | the section "[Theming](#theming)". 161 | 162 | ### Prism 163 | 164 | > `PrismLib` | _required; default is provided in `defaultProps` export_ 165 | 166 | This is the [Prismjs](https://github.com/PrismJS/prism) library itself. 167 | A vendored version of Prism is provided (and also exported) as part of this library. 168 | This vendored version doesn't pollute the global namespace, is slimmed down, 169 | and doesn't conflict with any installation of `prismjs` you might have. 170 | 171 | If you're only using `Prism.highlight` you can choose to use `prism-react-renderer`'s 172 | exported, vendored version of Prism instead. 173 | 174 | But if you choose to use your own Prism setup, simply pass Prism as a prop: 175 | 176 | ```jsx 177 | // Whichever way you're retrieving Prism here: 178 | import Prism from 'prismjs/components/prism-core'; 179 | 180 | 181 | ``` 182 | 183 | ## Children Function 184 | 185 | This is where you render whatever you want to based on the output of ``. 186 | You use it like so: 187 | 188 | ```js 189 | const ui = ( 190 | 191 | {highlight => ( 192 | // use utilities and prop getters here, like highlight.className, highlight.getTokenProps, etc. 193 |
{/* more jsx here */}
194 | )} 195 |
196 | ); 197 | ``` 198 | 199 | The properties of this `highlight` object can be split into two categories as indicated below: 200 | 201 | ### state 202 | 203 | These properties are the flat output of ``. They're generally "state" and are what 204 | you'd usually expect from a render-props-based API. 205 | 206 | | property | type | description | 207 | | ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------- | 208 | | `tokens` | `Token[][]` | This is a doubly nested array of tokens. The outer array is for separate lines, the inner for tokens, so the actual content. | 209 | | `className` | `string` | This is the class you should apply to your wrapping element, typically a `
`                                             |
210 | 
211 | A "Token" is an object that represents a piece of content for Prism. It has a `types` property, which is an array
212 | of types that indicate the purpose and styling of a piece of text, and a `content` property, which is the actual
213 | text.
214 | 
215 | You'd typically iterate over `tokens`, rendering each line, and iterate over its items, rendering out each token, which is a piece of
216 | this line.
217 | 
218 | ### prop getters
219 | 
220 | > See
221 | > [Kent C. Dodds' blog post about prop getters](https://blog.kentcdodds.com/how-to-give-rendering-control-to-users-with-prop-getters-549eaef76acf)
222 | 
223 | These functions are used to apply props to the elements that you render. This
224 | gives you maximum flexibility to render what, when, and wherever you like.
225 | 
226 | You'd typically call these functions with some dictated input and add on all other
227 | props that it should pass through. It'll correctly override and modify the props
228 | that it returns to you, so passing props to it instead of adding them directly is
229 | advisable.
230 | 
231 | | property        | type           | description                                                                                           |
232 | | --------------- | -------------- | ----------------------------------------------------------------------------------------------------- |
233 | | `getLineProps`  | `function({})` | returns the props you should apply to any list of tokens, i.e. the element that contains your tokens. |
234 | | `getTokenProps` | `function({})` | returns the props you should apply to the elements displaying tokens that you render.                 |
235 | 
236 | #### `getLineProps`
237 | 
238 | You need to add a `line` property (type: `Token[]`) to the object you're passing to
239 | `getLineProps`; It's also advisable to add a `key`.
240 | 
241 | This getter will return you props to spread onto your line elements (typically `
s`). 242 | 243 | It will typically return a `className` (if you pass one it'll be appended), `children`, 244 | `style` (if you pass one it'll be merged). It also passes on all other props you pass 245 | to the input. 246 | 247 | The `className` will always contain `.token-line`. 248 | 249 | #### `getTokenProps` 250 | 251 | You need to add a `token` property (type: `Token`) to the object you're passing to 252 | `getTokenProps`; It's also advisable to add a `key`. 253 | 254 | This getter will return you props to spread onto your token elements (typically `s`). 255 | 256 | It will typically return a `className` (if you pass one it'll be appended), `children`, 257 | `style` (if you pass one it'll be merged). It also passes on all other props you pass 258 | to the input. 259 | 260 | The `className` will always contain `.token`. This also provides full compatibility with 261 | your old Prism CSS-file themes. 262 | 263 | ## Theming 264 | 265 | The `defaultProps` you'd typically apply in a basic use-case, contain a default theme. 266 | This theme is [duotoneDark](./themes/duotoneDark.js). 267 | 268 | While all `className`s are provided with ``, so that you could use your good 269 | old Prism CSS-file themes, you can also choose to use `react-prism-renderer`'s themes. 270 | 271 | These themes are JSON-based and are heavily inspired by VSCode's theme format. 272 | 273 | Their syntax, expressed in Flow looks like the following: 274 | 275 | ```js 276 | { 277 | plain: StyleObj, 278 | styles: Array<{ 279 | types: string[], 280 | languages?: string[], 281 | style: StyleObj 282 | }> 283 | } 284 | ``` 285 | 286 | The `plain` property provides a base style-object. This style object is directly used 287 | in the `style` props that you'll receive from the prop getters, if a `theme` prop has 288 | been passed to ``. 289 | 290 | The `styles` property contains an array of definitions. Each definition contains a `style` 291 | property, that is also just a style object. These styles are limited by the `types` 292 | and `languages` properties. 293 | 294 | The `types` properties is an array of token types that Prism outputs. The `languages` 295 | property limits styles to highlighted languages. 296 | 297 | When converting a Prism CSS theme it's mostly just necessary to use classes as 298 | `types` and convert the declarations to object-style-syntax and put them on `style`. 299 | 300 | ## FAQ 301 | 302 |
303 | 304 | How do I use my old Prism css themes? 305 | 306 | `prism-react-renderer` still returns you all proper `className`s via the prop getters, 307 | when you use it. By default however it uses its new theming system, which output a 308 | couple of `style` props as well. 309 | 310 | If you don't pass `theme` to the `` component it will default to not 311 | outputting any `style` props, while still returning you the `className` props, like 312 | so: 313 | 314 | ```js 315 | 321 | {highlight => null /* ... */} 322 | 323 | ``` 324 | 325 |
326 | 327 |
328 | 329 | How do I prevent a theme and the vendored Prism to be bundled? 330 | 331 | Since the default theme and the vendored Prism library in `prism-react-renderer` 332 | come from `defaultProps`, if you wish to pass your own Prism library in, and not 333 | use the built-in theming, you simply need to leave it out to allow your bundler 334 | to tree-shake those: 335 | 336 | ```js 337 | import Highlight from "prism-react-renderer"; 338 | import Prism from "prismjs"; // Different source 339 | 340 | 341 | {highlight => null /* ... */} 342 | ; 343 | ``` 344 | 345 | You can also import the vendored Prism library on its own: 346 | 347 | ```js 348 | import { Prism } from "prism-react-renderer"; 349 | // or 350 | import Prism from "prism-react-renderer/prism"; 351 | ``` 352 | 353 |
354 | 355 | ## LICENSE 356 | 357 | MIT 358 | --------------------------------------------------------------------------------