diff --git a/examples/using-remark/src/layouts/index.js b/examples/using-remark/src/layouts/index.js index bc841d5f94b93..b9daba51ce719 100644 --- a/examples/using-remark/src/layouts/index.js +++ b/examples/using-remark/src/layouts/index.js @@ -7,6 +7,7 @@ import "typeface-space-mono" import "typeface-spectral" import "prismjs/themes/prism-solarizedlight.css" +import "prismjs/plugins/line-numbers/prism-line-numbers.css" class Layout extends React.Component { render() { diff --git a/examples/using-remark/src/pages/2017-04-04---code-and-syntax-highlighting/index.md b/examples/using-remark/src/pages/2017-04-04---code-and-syntax-highlighting/index.md index 896105b9d7550..99ba281a269ca 100644 --- a/examples/using-remark/src/pages/2017-04-04---code-and-syntax-highlighting/index.md +++ b/examples/using-remark/src/pages/2017-04-04---code-and-syntax-highlighting/index.md @@ -64,13 +64,13 @@ No language indicated, so no syntax highlighting. But let's throw in a tag. ``` -## Line highlighting +## Line highlighting & numbering -[gatsby-remark-prismjs][1] has its own line highlighting implementation which +[gatsby-remark-prismjs][1] has its own line highlighting & numbering implementation which differs a bit from PrismJS's own. You can find out everything about it in the [corresponding README][1]. - ```javascript{1-2,22} + ```javascript{1-2,22}{numberLines: true} // In your gatsby-config.js // Let's make this line very long so that our container has to scroll its overflow… plugins: [ @@ -99,7 +99,7 @@ differs a bit from PrismJS's own. You can find out everything about it in the ] ``` -```javascript{1-2,22} +```javascript{1-2,22}{numberLines: true} // In your gatsby-config.js // Let's make this line very long so that our container has to scroll its overflow… plugins: [ @@ -128,6 +128,20 @@ plugins: [ ] ``` +Line numbers can start from anywhere, here's an example showing a small extract from a larger chunk of code: + + ```{numberLines: 549} + ... + a long imaginary code block + ... + ``` + +```{numberLines: 549} +... + a long imaginary code block +... +``` + Let's do something crazy and add a list with another code example: - **A list item** diff --git a/examples/using-remark/src/utils/typography.js b/examples/using-remark/src/utils/typography.js index bbccbaa2bf8dc..909ebf6ec4219 100644 --- a/examples/using-remark/src/utils/typography.js +++ b/examples/using-remark/src/utils/typography.js @@ -140,6 +140,9 @@ const options = { minWidth: `100%`, textShadow: `none`, }, + ".gatsby-highlight pre[class*='language-'].line-numbers": { + paddingLeft: `2.8em`, + }, ".gatsby-highlight-code-line": { background: `#fff2cc`, display: `block`, diff --git a/packages/gatsby-remark-prismjs/.gitignore b/packages/gatsby-remark-prismjs/.gitignore index 54623385a53ce..a1eea9b0175b0 100644 --- a/packages/gatsby-remark-prismjs/.gitignore +++ b/packages/gatsby-remark-prismjs/.gitignore @@ -1,4 +1,5 @@ /index.js +/add-line-numbers.js /highlight-code.js /parse-line-number-range.js /load-prism-language.js diff --git a/packages/gatsby-remark-prismjs/README.md b/packages/gatsby-remark-prismjs/README.md index 93e2ab9b5647d..93fd60ee0b76b 100644 --- a/packages/gatsby-remark-prismjs/README.md +++ b/packages/gatsby-remark-prismjs/README.md @@ -39,6 +39,12 @@ plugins: [ // the language "sh" which will highlight using the // bash highlighter. aliases: {}, + // This toggles the display of line numbers alongside the code. + // To use it, add the following line in src/layouts/index.js + // right after importing the prism color scheme: + // `require("prismjs/plugins/line-numbers/prism-line-numbers.css");` + // Defaults to false. + showLineNumbers: false }, }, ], @@ -110,6 +116,7 @@ CSS along your PrismJS theme and the styles for `.gatsby-highlight-code-line`: * padding and overflow. * 1. Make the element just wide enough to fit its content. * 2. Always fill the visible space in .gatsby-highlight. + * 3. Adjust the position of the line numbers */ .gatsby-highlight pre[class*="language-"] { background-color: transparent; @@ -119,6 +126,20 @@ CSS along your PrismJS theme and the styles for `.gatsby-highlight-code-line`: float: left; /* 1 */ min-width: 100%; /* 2 */ } +.gatsby-highlight pre[class*="language-"].line-numbers { + paddingLeft: 2.8em; /* 3 */ +} +``` + +#### Optional: Add line numbering + +If you want to add line numbering alongside your code, you need to +import the corresponding CSS file from PrismJS, right after importing your +colorscheme in `layout/index.js`: + +```javascript +// layouts/index.js +require("prismjs/plugins/line-numbers/prism-line-numbers.css"); ``` ### Usage in Markdown @@ -139,6 +160,39 @@ This is some beautiful code: ] ``` +To see the line numbers alongside your code, you can use the `numberLines` option: + + ```javascript{numberLines: true} + // In your gatsby-config.js + plugins: [ + { + resolve: `gatsby-transformer-remark`, + options: { + plugins: [ + `gatsby-remark-prismjs`, + ] + } + } + ] + ``` + +You can also start numbering at any index you wish (here, numbering +will start at index 5): + + ```javascript{numberLines: 5} + // In your gatsby-config.js + plugins: [ + { + resolve: `gatsby-transformer-remark`, + options: { + plugins: [ + `gatsby-remark-prismjs`, + ] + } + } + ] + ``` + You can also add line highlighting. It adds a span around lines of code with a special class `.gatsby-highlight-code-line` that you can target with styles. See this README for more info. @@ -204,9 +258,21 @@ throw our overflow and background on `.gatsby-highlight`, and use `display:block` on `.gatsby-highlight-code-line` – all of this coming together to facilitate the desired line highlight behavior. +### Line numbering + +Because [the line numbering PrismJS plugin][7] runs client-side, a few adaptations were required to make it work: + +* A class `.line-numbers` is dynamically added to the `
` element.
+* A new node `` is added right before the closing `
` +containing as many empty ``s as there are lines. + +See the [client-side PrismJS implementation][8] for reference. + [1]: https://github.com/PrismJS/prism/tree/8eb0ab6f76484ca47fa7acbf77657fab17b03ca7/plugins/line-highlight [2]: https://github.com/facebook/react/blob/00ba97a354e841701b4b83983c3a3904895e7b87/docs/_config.yml#L10 [3]: http://prismjs.com/#plugins [4]: https://facebook.github.io/react/tutorial/tutorial.html [5]: https://github.com/PrismJS/prism/tree/1d5047df37aacc900f8270b1c6215028f6988eb1/themes [6]: http://prismjs.com/ +[7]: https://prismjs.com/plugins/line-numbers/ +[8]: https://github.com/PrismJS/prism/blob/master/plugins/line-numbers/prism-line-numbers.js#L69-L115 diff --git a/packages/gatsby-remark-prismjs/src/__tests__/__snapshots__/index.js.snap b/packages/gatsby-remark-prismjs/src/__tests__/__snapshots__/index.js.snap index 122be2ae1312a..7fc9e4cb89bad 100644 --- a/packages/gatsby-remark-prismjs/src/__tests__/__snapshots__/index.js.snap +++ b/packages/gatsby-remark-prismjs/src/__tests__/__snapshots__/index.js.snap @@ -22,9 +22,7 @@ Object { }, }, "type": "html", - "value": "
-
// Fake
-
", + "value": "
// Fake
", }, ], "position": Object { @@ -65,9 +63,7 @@ Object { }, }, "type": "html", - "value": "
-
// Fake
-
", + "value": "
// Fake
", }, ], "position": Object { @@ -108,9 +104,7 @@ Object { }, }, "type": "html", - "value": "
-
// Fake
-
", + "value": "
// Fake
", }, ], "position": Object { @@ -348,3 +342,47 @@ Object { "type": "root", } `; + +exports[`remark prism plugin numberLines adds line-number markup when necessary 1`] = ` +Object { + "children": Array [ + Object { + "lang": "js{numberLines:5}", + "position": Position { + "end": Object { + "column": 4, + "line": 4, + "offset": 46, + }, + "indent": Array [ + 1, + 1, + 1, + ], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "html", + "value": "
//.foo { 
+color: red;
+ }\`
", + }, + ], + "position": Object { + "end": Object { + "column": 4, + "line": 4, + "offset": 46, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", +} +`; diff --git a/packages/gatsby-remark-prismjs/src/__tests__/add-line-numbers.js b/packages/gatsby-remark-prismjs/src/__tests__/add-line-numbers.js new file mode 100644 index 0000000000000..c95dcb6046388 --- /dev/null +++ b/packages/gatsby-remark-prismjs/src/__tests__/add-line-numbers.js @@ -0,0 +1,17 @@ +const addLineNumbers = require(`../add-line-numbers`) + +describe(`returns the line numbers container`, () => { + it(`should return the container with the right classes`, () => { + expect(addLineNumbers(``)).toEqual( + `` + ) + }) + it(`should return return as many children as there are code lines`, () => { + expect(addLineNumbers(`line1\nline2\nline3`)).toEqual( + `` + ) + }) +}) \ No newline at end of file diff --git a/packages/gatsby-remark-prismjs/src/__tests__/index.js b/packages/gatsby-remark-prismjs/src/__tests__/index.js index 271aed83b77e4..1f511940cff22 100644 --- a/packages/gatsby-remark-prismjs/src/__tests__/index.js +++ b/packages/gatsby-remark-prismjs/src/__tests__/index.js @@ -57,4 +57,13 @@ describe(`remark prism plugin`, () => { expect(markdownAST).toMatchSnapshot() }) }) + + describe(`numberLines`, () => { + it(`adds line-number markup when necessary`, () => { + const code = `\`\`\`js{numberLines:5}\n//.foo { \ncolor: red;\n }\`` + const markdownAST = remark.parse(code) + plugin({ markdownAST }) + expect(markdownAST).toMatchSnapshot() + }) + }) }) diff --git a/packages/gatsby-remark-prismjs/src/__tests__/parse-line-number-range.js b/packages/gatsby-remark-prismjs/src/__tests__/parse-line-number-range.js index fe2b9580b45fa..b1b605aff6498 100644 --- a/packages/gatsby-remark-prismjs/src/__tests__/parse-line-number-range.js +++ b/packages/gatsby-remark-prismjs/src/__tests__/parse-line-number-range.js @@ -39,6 +39,67 @@ describe(`parses numeric ranges from the languages markdown code directive`, () ]) }) + describe(`parses line numbering options from the languages markdown code directive`, () => { + it(`parses the right line number start index from the languages variable`, () => { + expect(parseLineNumberRange(`jsx{numberLines: true}`).numberLines).toEqual(true) + expect(parseLineNumberRange(`jsx{numberLines: true}`).numberLinesStartAt).toEqual(1) + expect(parseLineNumberRange(`jsx{numberLines: 3}`).numberLines).toEqual(true) + expect(parseLineNumberRange(`jsx{numberLines: 3}`).numberLinesStartAt).toEqual(3) + }) + + it(`parses the right line number start index without a specified language`, () => { + expect(parseLineNumberRange(`{numberLines: true}`).numberLines).toEqual(true) + expect(parseLineNumberRange(`{numberLines: true}`).numberLinesStartAt).toEqual(1) + expect(parseLineNumberRange(`{numberLines: 3}`).numberLines).toEqual(true) + expect(parseLineNumberRange(`{numberLines: 3}`).numberLinesStartAt).toEqual(3) + }) + + it(`ignores non-true or non-number values`, () => { + expect(parseLineNumberRange(`jsx{numberLines: false}`).numberLines).toEqual(false) + expect(parseLineNumberRange(`jsx{numberLines: NaN}`).numberLines).toEqual(false) + }) + + it(`casts decimals line number start into the nearest lower integer`, () => { + expect(parseLineNumberRange(`jsx{numberLines: 1.2}`).numberLinesStartAt).toEqual(1) + expect(parseLineNumberRange(`jsx{numberLines: 1.8}`).numberLinesStartAt).toEqual(1) + }) + }) + + describe(`parses both line numbering and line highlighting options`, () => { + it(`one line highlighted`, () => { + expect(parseLineNumberRange(`jsx{1}{numberLines: 3}`)).toEqual({ + splitLanguage: `jsx`, + highlightLines: [1], + numberLines: true, + numberLinesStartAt: 3, + }) + }) + it(`multiple lines highlighted`, () => { + expect(parseLineNumberRange(`jsx{1,5,7-8}{numberLines: 3}`)).toEqual({ + splitLanguage: `jsx`, + highlightLines: [1,5,7,8], + numberLines: true, + numberLinesStartAt: 3, + }) + }) + it(`numberLines: true`, () => { + expect(parseLineNumberRange(`jsx{1,5,7-8}{numberLines: true}`)).toEqual({ + splitLanguage: `jsx`, + highlightLines: [1,5,7,8], + numberLines: true, + numberLinesStartAt: 1, + }) + }) + it(`reverse ordering`, () => { + expect(parseLineNumberRange(`jsx{numberLines: 4}{2}`)).toEqual({ + splitLanguage: `jsx`, + highlightLines: [2], + numberLines: true, + numberLinesStartAt: 4, + }) + }) + }) + it(`handles bad inputs`, () => { expect(parseLineNumberRange(`jsx{-1`).highlightLines).toEqual([]) expect(parseLineNumberRange(`jsx{-1....`).highlightLines).toEqual([]) diff --git a/packages/gatsby-remark-prismjs/src/add-line-numbers.js b/packages/gatsby-remark-prismjs/src/add-line-numbers.js new file mode 100644 index 0000000000000..18519ff622106 --- /dev/null +++ b/packages/gatsby-remark-prismjs/src/add-line-numbers.js @@ -0,0 +1,22 @@ +module.exports = (code = []) => { + + // Generate as many `` as there are code lines + const generateSpans = (numberOfLines) => { + let spans = `` + for (let i=0; i` + } + return spans + } + + const numberOfLines = code.length === 0 ? 0 : code.split(`\n`).length + + // Generate the container for the line numbers. + // Relevant code in the Prism Line Numbers plugin can be found here: + // https://github.com/PrismJS/prism/blob/f356dfe71bf126e6dc060c03f3e042de28a9bec4/plugins/line-numbers/prism-line-numbers.js#L99-L115 + const lineNumbersWrapper = + `` + return lineNumbersWrapper +} \ No newline at end of file diff --git a/packages/gatsby-remark-prismjs/src/index.js b/packages/gatsby-remark-prismjs/src/index.js index 9e99a78cc5065..7d12bcf03487b 100644 --- a/packages/gatsby-remark-prismjs/src/index.js +++ b/packages/gatsby-remark-prismjs/src/index.js @@ -2,6 +2,7 @@ const visit = require(`unist-util-visit`) const parseLineNumberRange = require(`./parse-line-number-range`) const highlightCode = require(`./highlight-code`) +const addLineNumbers = require(`./add-line-numbers`) module.exports = ( { markdownAST }, @@ -14,7 +15,12 @@ module.exports = ( visit(markdownAST, `code`, node => { let language = node.lang - let { splitLanguage, highlightLines } = parseLineNumberRange(language) + let { + splitLanguage, + highlightLines, + numberLines, + numberLinesStartAt, + } = parseLineNumberRange(language) language = splitLanguage // PrismJS's theme styles are targeting pre[class*="language-"] @@ -24,9 +30,7 @@ module.exports = ( // // @see https://github.com/PrismJS/prism/blob/1d5047df37aacc900f8270b1c6215028f6988eb1/themes/prism.css#L49-L54 let languageName = `text` - if (language) { - languageName = normalizeLanguage(language) - } + if (language) { languageName = normalizeLanguage(language) } // Allow users to specify a custom class prefix to avoid breaking // line highlights if Prism is required by any other code. @@ -35,17 +39,28 @@ module.exports = ( // @see https://github.com/gatsbyjs/gatsby/issues/1486 const className = `${classPrefix}${languageName}` + let numLinesStyle, numLinesClass, numLinesNumber + numLinesStyle = numLinesClass = numLinesNumber = `` + if (numberLines) { + numLinesStyle = ` style="counter-reset: linenumber ${numberLinesStartAt - 1}"` + numLinesClass = ` line-numbers` + numLinesNumber = addLineNumbers(node.value) + } + // Replace the node with the markup we need to make // 100% width highlighted code lines work node.type = `html` - node.value = `
-
${highlightCode(
-      language,
-      node.value,
-      highlightLines
-    )}
-
` - }) + // prettier-ignore + node.value = `` + + `
` + + `` + + `` + + `${highlightCode(language, node.value, highlightLines)}` + + `` + + `${numLinesNumber}` + + `` + + `
` + }) visit(markdownAST, `inlineCode`, node => { let languageName = `text` @@ -61,9 +76,6 @@ module.exports = ( const className = `${classPrefix}${languageName}` node.type = `html` - node.value = `${highlightCode( - languageName, - node.value - )}` + node.value = `${highlightCode(languageName, node.value)}` }) } diff --git a/packages/gatsby-remark-prismjs/src/parse-line-number-range.js b/packages/gatsby-remark-prismjs/src/parse-line-number-range.js index 33f8473fded7e..2602b85a32a4c 100644 --- a/packages/gatsby-remark-prismjs/src/parse-line-number-range.js +++ b/packages/gatsby-remark-prismjs/src/parse-line-number-range.js @@ -5,11 +5,34 @@ module.exports = language => { return `` } if (language.split(`{`).length > 1) { - let [splitLanguage, rangeStr] = language.split(`{`) - rangeStr = rangeStr.slice(0, -1) + let [splitLanguage, ...options] = language.split(`{`) + let highlightLines = [], numberLines = false, numberLinesStartAt + // Options can be given in any order and are optional + options.forEach((option) => { + option = option.slice(0, -1) + // Test if the option is for line hightlighting + if (rangeParser.parse(option).length > 0) { + highlightLines = rangeParser.parse(option).filter(n => n > 0) + } + option = option.split(`:`) + // Test if the option is for line numbering + // Option must look like `numberLines: true` or `numberLines: ` + // Otherwise we disable line numbering + if ( + option.length === 2 + && option[0] === `numberLines` + && (option[1].trim() === `true` || Number.isInteger(parseInt(option[1].trim(), 10))) + ) { + numberLines = true + numberLinesStartAt = option[1].trim() === `true` ? 1 : parseInt(option[1].trim(), 10) + } + }) + return { splitLanguage, - highlightLines: rangeParser.parse(rangeStr).filter(n => n > 0), + highlightLines, + numberLines, + numberLinesStartAt, } }