Skip to content

Commit

Permalink
feat: add support to line numbers in markdown (#217)
Browse files Browse the repository at this point in the history
closes #178 

* Fix #178: Add support to line numbers in markdown

* highlight-plugin.js now read global and local { lineNumber: true}.
* Added tests and snapshots for the feature.
* Receive and validate lineNumber from markdown settings.
* Add some CSS for the line numbers.
* Document both the markdown config and the inline option.

* fixed line numbers column

* should be aria-hidden

* fix test

* undo unnecessary changes

* tweaks
  • Loading branch information
lubien authored and egoist committed May 29, 2019
1 parent bb17c3b commit 1561c61
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 54 deletions.
33 changes: 29 additions & 4 deletions packages/saber-highlight-css/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`code block markdown.lineNumbers = true 1`] = `"<div class=\\"saber-highlight has-line-numbers\\" v-pre=\\"\\" data-lang=\\"js\\"><pre class=\\"saber-highlight-code language-js\\"><code class=\\"language-js\\"><span aria-hidden=\\"true\\" class=\\"saber-highlight-line-numbers\\"><span></span></span>const cry = Array(3).fill('ora').join(' ')</code></pre></div>"`;
exports[`code block with {lineNumbers:true} 1`] = `"<div class=\\"saber-highlight has-line-numbers\\" v-pre=\\"\\" data-lang=\\"js\\"><pre class=\\"saber-highlight-code language-js\\"><code class=\\"language-js\\"><span aria-hidden=\\"true\\" class=\\"saber-highlight-line-numbers\\"><span></span></span>const cry = Array(3).fill('ora').join(' ')</code></pre></div>"`;
exports[`main 1`] = `"<div class=\\"saber-highlight\\" v-pre=\\"\\" data-lang=\\"vue\\"><pre class=\\"saber-highlight-code language-vue\\"><code class=\\"language-vue\\">&lt;div&gt;hehe&lt;/div&gt;</code></pre></div>"`;
30 changes: 30 additions & 0 deletions packages/saber/lib/markdown/__test__/highlight-plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
127 changes: 78 additions & 49 deletions packages/saber/lib/markdown/highlight-plugin.js
Original file line number Diff line number Diff line change
@@ -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 =>
'<span aria-hidden="true" class="saber-highlight-line-numbers">' +
code
.trim()
.split('\n')
.map(() => `<span></span>`)
.join('') +
'</span>'

module.exports = (
md,
{ highlightedLineBackground, lineNumbers = false } = {}
) => {
const renderPreWrapper = ({
preWrapperAttrs,
preAttrs,
codeAttrs,
code,
codeMask = ''
) =>
`<div${preWrapperAttrs}>${codeMask}<pre${preAttrs}><code${codeAttrs}>${code.trim()}</code></pre></div>`
codeMask = '',
lines = ''
}) =>
`<div${preWrapperAttrs}>${codeMask}<pre${preAttrs}><code${codeAttrs}>${lines}${code.trim()}</code></pre></div>`

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
? ''
: `<div class="saber-highlight-mask${
langClass ? ` ${langClass}` : ''
}">` +
md.utils
.escapeHtml(token.content)
.split('\n')
.map((split, index) => {
split = split || '&#8203;'
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 `<div class="code-line highlighted"${style}>${split}</div>`
}

return `<div class="code-line">${split}</div>`
})
.join('') +
'</div>'

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(' ')]
Expand All @@ -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 =
`<div class="saber-highlight-mask${langClass ? ` ${langClass}` : ''}">` +
md.utils
.escapeHtml(token.content)
.split('\n')
.map((split, index) => {
split = split || '&#8203;'
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 `<div class="code-line highlighted"${style}>${split}</div>`
}

return `<div class="code-line">${split}</div>`
})
.join('') +
'</div>'

return renderPreWrapper(
return renderPreWrapper({
preWrapperAttrs,
preAttrs,
codeAttrs,
code,
codeMask
)
codeMask,
lines
})
}
}
5 changes: 4 additions & 1 deletion packages/saber/lib/plugins/transformer-markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/saber/lib/utils/validateConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = (config, { dev }) => {
options: 'object?',
headings: 'object?',
highlighter: 'string?',
lineNumbers: 'boolean?',
// Same as the type of Saber plugins
plugins
},
Expand Down
48 changes: 48 additions & 0 deletions website/pages/docs/markdown-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 7 additions & 0 deletions website/pages/docs/saber-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ interface MarkdownPlugin {
}
```

### lineNumbers

- Type: `boolean`
- Default: `false`

Show line numbers in code blocks.

## permalinks

- Type: `Permalinks` `(page: Page) => Permalinks`
Expand Down

0 comments on commit 1561c61

Please sign in to comment.