1
0
Fork 0
freesewing/packages/rehype-highlight-lines/src/index.mjs
2023-05-19 10:39:21 +02:00

155 lines
4.7 KiB
JavaScript

import { visit } from 'unist-util-visit'
import { remove } from 'unist-util-remove'
const dflts = {
commentTag: 'span',
commentClass: 'hljs-comment',
openingCommentHighlight: 'highlight-start',
closingCommentHighlight: 'highlight-end',
openingCommentStrikeout: 'strikeout-start',
closingCommentStrikeout: 'strikeout-end',
highlightTag: 'section',
highlightClass: 'highlight-lines',
strikeoutClass: 'strikeout-lines',
swallow: true,
}
export default (userOptions = {}) => {
// Merge defaults with user-supplied options
const options = {
...dflts,
...userOptions,
}
const splitParams = (node) => {
if (node.children.length === 1 && node.children[0].type === 'text') {
const content = node.children[0].value.split('\n')
node.children = content.map((value) =>
value.includes('//')
? {
type: 'element',
tagName: 'span',
properties: {
className: options.commentClass,
},
children: [{ type: 'text', value }],
}
: { type: 'text', value: value + '\n', isParamLine: true }
)
}
}
// Keep track of whether we've opened a highlight block
let isOpen = 0
let children = {}
// Detect opening or closing comment
const isOpeningOrClosingComment = (node) => {
if (
node &&
node.tagName === options.commentTag &&
node.properties?.className.includes(options.commentClass)
) {
if (node.children[0].value.includes(options.openingCommentHighlight))
return ['opening', 'highlight']
if (node.children[0].value.includes(options.closingCommentHighlight))
return ['closing', 'highlight']
if (node.children[0].value.includes(options.openingCommentStrikeout))
return ['opening', 'strikeout']
if (node.children[0].value.includes(options.closingCommentStrikeout))
return ['closing', 'strikeout']
}
return [false, 'highlight']
}
// Visitor method
const visitor = (node, i, parent) => {
const [type, variant] = isOpeningOrClosingComment(node)
if (type) {
if (type === 'opening') {
isOpen = 1
// Deal with extra linebreaks in the element before the start
const prev = parent.children[i - 1]
if (prev?.type === 'text' && prev.value.includes('\n'))
prev.value = prev.value.replace(/\n/g, '') + '\n'
if (options.swallow) node.__remove_dupes = true
} else if (type === 'closing') {
// Deal with extra linebreaks in the element before the end
const prev = children[i - 1]
if (prev.type === 'text' && prev.value.includes('\n') && !node.isParamLine)
prev.value = prev.value.trimEnd()
isOpen = 0
const curNode = { ...node }
parent.children[i] = {
type: 'element',
tagName: options.highlightTag,
properties: {
className: Array.isArray(options[`${variant}Class`])
? options[`${variant}Class`]
: [options[`${variant}Class`]],
},
children: [
{
type: 'element',
tagName: 'div',
properties: {
className: ['code-section-inner'],
},
children: [...Object.values(children)],
},
],
}
if (!options.swallow) parent[i].children.push(curNode)
children = {}
}
} else if (isOpen) {
if (
parent.tagName === 'code' ||
(parent.tagName === 'span' &&
parent.properties?.className &&
parent.properties.className.includes('hljs-params'))
) {
if (node.type === 'text' && node.value === '\n') {
// Linebreak
node.type = 'element'
node.tagName = 'span'
node.children = [
{
type: 'text',
value: '\n',
},
{
type: 'element',
tagName: 'span',
properties: {
className: ['code-line-break'],
},
},
]
}
// On the first highlighted line, trim the previous linebreak (not for param lines)
if (isOpen === 1 && node.type === 'text' && !node.isParamLine)
node.value = node.value.replace('\n', '')
children[i] = { ...node }
isOpen++
node.__remove_dupes = true
}
}
}
const isParamsNode = (node) =>
node?.properties?.className && node.properties.className.includes('hljs-params')
const transform = (tree) => {
visit(tree, (node) => isParamsNode(node), splitParams)
visit(tree, () => true, visitor)
remove(tree, (node) => node.__remove_dupes === true)
}
return transform
}