diff --git a/packages/saber-highlight-css/default.css b/packages/saber-highlight-css/default.css index 98b7c847d..bb98186ad 100644 --- a/packages/saber-highlight-css/default.css +++ b/packages/saber-highlight-css/default.css @@ -11,11 +11,12 @@ position: absolute; right: 10px; top: 5px; - font-size: .75rem; + font-size: 0.75rem; color: #bdc5d1; } -.saber-highlight-mask, .saber-highlight-code { +.saber-highlight-mask, +.saber-highlight-code { line-height: 1.5; background-color: transparent !important; text-shadow: none !important; @@ -52,10 +53,34 @@ .saber-highlight-code code, .saber-highlight-mask { - font-size: .875rem; + font-size: 0.875rem; } .code-line.highlighted { background: rgba(3, 169, 244, 0.121); - box-shadow: inset 2px 0 0 0 rgba(3, 169, 244, 0.278) +} + +/* Line numbers */ +.saber-highlight-line-numbers { + pointer-events: none; + font-size: 100%; + float: left; + letter-spacing: -1px; + border-right: 1px solid #999; + user-select: none; + text-align: right; + padding-right: 0.8rem; + margin-right: .8rem; + counter-reset: linenumber; +} + +.saber-highlight-line-numbers > span { + counter-increment: linenumber; + display: block; +} + +.saber-highlight-line-numbers > span:before { + content: counter(linenumber); + color: #999; + display: block; } diff --git a/packages/saber/lib/markdown/__test__/__snapshots__/highlight-plugin.test.js.snap b/packages/saber/lib/markdown/__test__/__snapshots__/highlight-plugin.test.js.snap index 7418952ea..7510b15cb 100644 --- a/packages/saber/lib/markdown/__test__/__snapshots__/highlight-plugin.test.js.snap +++ b/packages/saber/lib/markdown/__test__/__snapshots__/highlight-plugin.test.js.snap @@ -1,3 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`code block markdown.lineNumbers = true 1`] = `"
const cry = Array(3).fill('ora').join(' ')
"`; + +exports[`code block with {lineNumbers:true} 1`] = `"
const cry = Array(3).fill('ora').join(' ')
"`; + exports[`main 1`] = `"
<div>hehe</div>
"`; diff --git a/packages/saber/lib/markdown/__test__/highlight-plugin.test.js b/packages/saber/lib/markdown/__test__/highlight-plugin.test.js index 6dda36792..b731af847 100644 --- a/packages/saber/lib/markdown/__test__/highlight-plugin.test.js +++ b/packages/saber/lib/markdown/__test__/highlight-plugin.test.js @@ -16,3 +16,33 @@ test('main', () => { ) expect(html).toMatchSnapshot() }) + +test('code block with {lineNumbers:true}', () => { + const md = new Markdown() + const { env } = createEnv() + md.use(fenceOptionsPlugin) + const html = md.render( + ` +\`\`\`js {lineNumbers:true} +const cry = Array(3).fill('ora').join(' ') +\`\`\` + `, + env + ) + expect(html).toMatchSnapshot() +}) + +test('code block markdown.lineNumbers = true', () => { + const md = new Markdown() + const { env } = createEnv() + md.use(fenceOptionsPlugin, { lineNumbers: true }) + const html = md.render( + ` +\`\`\`js {lineNumbers:true} +const cry = Array(3).fill('ora').join(' ') +\`\`\` + `, + env + ) + expect(html).toMatchSnapshot() +}) diff --git a/packages/saber/lib/markdown/highlight-plugin.js b/packages/saber/lib/markdown/highlight-plugin.js index d01d934dc..2ff429437 100644 --- a/packages/saber/lib/markdown/highlight-plugin.js +++ b/packages/saber/lib/markdown/highlight-plugin.js @@ -1,34 +1,98 @@ const RE = /\s*{([^}]+)}/ const parseOptions = str => { + if (!RE.test(str)) { + return {} + } + const [, options] = RE.exec(str) const fn = new Function(`return {${options}}`) // eslint-disable-line no-new-func return fn() } -module.exports = (md, { highlightedLineBackground } = {}) => { - const renderPreWrapper = ( +const generateLineNumbers = code => + '' + +module.exports = ( + md, + { highlightedLineBackground, lineNumbers = false } = {} +) => { + const renderPreWrapper = ({ preWrapperAttrs, preAttrs, codeAttrs, code, - codeMask = '' - ) => - `${codeMask}${code.trim()}` + codeMask = '', + lines = '' + }) => + `${codeMask}${lines}${code.trim()}` md.renderer.rules.fence = (...args) => { const [tokens, idx, options, env, self] = args const token = tokens[idx] + const fenceOptions = parseOptions(token.info) const langName = token.info.replace(RE, '').trim() + const langClass = `language-${langName || 'text'}` + token.info = langName const code = options.highlight ? options.highlight(token.content, langName, env) : md.utils.escapeHtml(token.content) + const highlightLines = fenceOptions.highlightLines + ? fenceOptions.highlightLines.map(v => + `${v}`.split('-').map(v => parseInt(v, 10)) + ) + : [] + + const codeMask = + highlightLines.length === 0 + ? '' + : `
` + + md.utils + .escapeHtml(token.content) + .split('\n') + .map((split, index) => { + split = split || '​' + const lineNumber = index + 1 + const inRange = highlightLines.some(([start, end]) => { + if (start && end) { + return lineNumber >= start && lineNumber <= end + } + + return lineNumber === start + }) + if (inRange) { + const style = highlightedLineBackground + ? ` style="background-color: ${highlightedLineBackground}"` + : '' + return `
${split}
` + } + + return `
${split}
` + }) + .join('') + + '
' + const renderAttrs = attrs => self.renderAttrs({ attrs }) + const shouldGenerateLineNumbers = + // It might be false so check for undefined + fenceOptions.lineNumbers === undefined + ? // Defaults to global config + lineNumbers + : // If it's set to false, even if the global config says true, ignore + fenceOptions.lineNumbers + const lines = shouldGenerateLineNumbers ? generateLineNumbers(code) : '' - const langClass = `language-${langName || 'text'}` const preAttrs = renderAttrs([ ...(token.attrs || []), ['class', ['saber-highlight-code', langClass].filter(Boolean).join(' ')] @@ -38,56 +102,21 @@ module.exports = (md, { highlightedLineBackground } = {}) => { ['class', langClass] ]) const preWrapperAttrs = renderAttrs([ - ['class', 'saber-highlight'], + [ + 'class', + `saber-highlight${shouldGenerateLineNumbers ? ' has-line-numbers' : ''}` + ], ['v-pre', ''], ['data-lang', langName] ]) - if (!token.info || !RE.test(token.info)) { - return renderPreWrapper(preWrapperAttrs, preAttrs, codeAttrs, code) - } - - const fenceOptions = parseOptions(token.info) - const highlightLines = - fenceOptions.highlightLines && - fenceOptions.highlightLines.map(v => - `${v}`.split('-').map(v => parseInt(v, 10)) - ) - token.info = langName - - const codeMask = - `
` + - md.utils - .escapeHtml(token.content) - .split('\n') - .map((split, index) => { - split = split || '​' - const lineNumber = index + 1 - const inRange = highlightLines.some(([start, end]) => { - if (start && end) { - return lineNumber >= start && lineNumber <= end - } - - return lineNumber === start - }) - if (inRange) { - const style = highlightedLineBackground - ? ` style="background-color: ${highlightedLineBackground}"` - : '' - return `
${split}
` - } - - return `
${split}
` - }) - .join('') + - '
' - - return renderPreWrapper( + return renderPreWrapper({ preWrapperAttrs, preAttrs, codeAttrs, code, - codeMask - ) + codeMask, + lines + }) } } diff --git a/packages/saber/lib/plugins/transformer-markdown.js b/packages/saber/lib/plugins/transformer-markdown.js index 374d156e4..0911a3748 100644 --- a/packages/saber/lib/plugins/transformer-markdown.js +++ b/packages/saber/lib/plugins/transformer-markdown.js @@ -77,7 +77,10 @@ function transformMarkdown(api, page) { }, { name: 'highlight', - resolve: require.resolve('../markdown/highlight-plugin') + resolve: require.resolve('../markdown/highlight-plugin'), + options: { + lineNumbers: markdown.lineNumbers + } }, { name: 'link', diff --git a/packages/saber/lib/utils/validateConfig.js b/packages/saber/lib/utils/validateConfig.js index fc11fe33c..c9104ff8f 100644 --- a/packages/saber/lib/utils/validateConfig.js +++ b/packages/saber/lib/utils/validateConfig.js @@ -36,6 +36,7 @@ module.exports = (config, { dev }) => { options: 'object?', headings: 'object?', highlighter: 'string?', + lineNumbers: 'boolean?', // Same as the type of Saber plugins plugins }, diff --git a/website/pages/docs/markdown-features.md b/website/pages/docs/markdown-features.md index af63c59bc..c50e02dc0 100644 --- a/website/pages/docs/markdown-features.md +++ b/website/pages/docs/markdown-features.md @@ -276,6 +276,54 @@ If you want to override the font size or font family, you need to add CSS for bo } ``` +### Line Numbers in Code Blocks + +Input: + +````markdown +```js {lineNumbers:true,highlightLines:['2-5']} +[ + { + text: 'A page', + slug: 'a-page', + level: 1 + }, + { + text: 'A section', + slug: 'a-section', + level: 2 + }, + { + text: 'Another section', + slug: 'another-section', + level: 3 + } +] +``` +```` + +Output: + +```js {lineNumbers:true,highlightLines:['2-5']} +[ + { + text: 'A page', + slug: 'a-page', + level: 1 + }, + { + text: 'A section', + slug: 'a-section', + level: 2 + }, + { + text: 'Another section', + slug: 'another-section', + level: 3 + } +] +``` + ## Configure markdown-it Check out [markdown.options](./saber-config.md#options) for setting markdown-it options and [markdown.plugins](./saber-config.md#plugins-2) for adding markdown-it plugins. diff --git a/website/pages/docs/saber-config.md b/website/pages/docs/saber-config.md index df158ec20..d221b6b3d 100644 --- a/website/pages/docs/saber-config.md +++ b/website/pages/docs/saber-config.md @@ -167,6 +167,13 @@ interface MarkdownPlugin { } ``` +### lineNumbers + +- Type: `boolean` +- Default: `false` + +Show line numbers in code blocks. + ## permalinks - Type: `Permalinks` `(page: Page) => Permalinks`