diff --git a/packages/transformers/src/transformers/notation-highlight-word.ts b/packages/transformers/src/transformers/notation-highlight-word.ts index 1e5cc5cbc..efe922ff4 100644 --- a/packages/transformers/src/transformers/notation-highlight-word.ts +++ b/packages/transformers/src/transformers/notation-highlight-word.ts @@ -1,6 +1,6 @@ import type { ShikiTransformer } from 'shiki' import { highlightWordInLine } from '../shared/highlight-word' -import { createCommentNotationTransformer } from '../utils' +import { createCommentNotationTransformerExperimental } from '../utils' export interface TransformerNotationWordHighlightOptions { /** @@ -13,33 +13,34 @@ export interface TransformerNotationWordHighlightOptions { classActivePre?: string } +const regex = /\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/g + export function transformerNotationWordHighlight( options: TransformerNotationWordHighlightOptions = {}, ): ShikiTransformer { const { classActiveWord = 'highlighted-word', - classActivePre = undefined, + classActivePre, } = options - return createCommentNotationTransformer( + return createCommentNotationTransformerExperimental( '@shikijs/transformers:notation-highlight-word', - // comment-start | marker | word | range | comment-end - /^\s*(?:\/\/|\/\*|)?/, - function ([_, word, range], _line, comment, lines, index) { - const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length - - // escape backslashes - word = word.replace(/\\(.)/g, '$1') - - lines - // Don't include the comment itself - .slice(index + 1, index + 1 + lineNum) - .forEach(line => highlightWordInLine.call(this, line, comment, word, classActiveWord)) - - if (classActivePre) - this.addClassToHast(this.pre, classActivePre) - return true + function (text, _line, comment, lines, index) { + return text.replace(regex, (_, word, range) => { + const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length + + // escape backslashes + word = word.replace(/\\(.)/g, '$1') + + lines + .slice(index, index + lineNum) + .forEach(line => highlightWordInLine.call(this, line, comment, word, classActiveWord)) + + if (classActivePre) + this.addClassToHast(this.pre, classActivePre) + + return '' + }) }, - true, // remove empty lines ) } diff --git a/packages/transformers/src/transformers/notation-map.ts b/packages/transformers/src/transformers/notation-map.ts index 1e2940852..324efb04e 100644 --- a/packages/transformers/src/transformers/notation-map.ts +++ b/packages/transformers/src/transformers/notation-map.ts @@ -1,5 +1,5 @@ import type { ShikiTransformer } from 'shiki' -import { createCommentNotationTransformer } from '../utils' +import { createCommentNotationTransformerExperimental } from '../utils' export interface TransformerNotationMapOptions { classMap?: Record @@ -22,19 +22,25 @@ export function transformerNotationMap( classActivePre = undefined, } = options - return createCommentNotationTransformer( + const regex = new RegExp(`\\[!code (${Object.keys(classMap).map(escapeRegExp).join('|')})(:\\d+)?\\]`, 'g') + + return createCommentNotationTransformerExperimental( name, - new RegExp(`\\s*(?://|/\\*|)?\\s*$`), - function ([_, match, range = ':1'], _line, _comment, lines, index) { - const lineNum = Number.parseInt(range.slice(1), 10) - lines - .slice(index, index + lineNum) - .forEach((line) => { - this.addClassToHast(line, classMap[match]) - }) - if (classActivePre) - this.addClassToHast(this.pre, classActivePre) - return true + function (text, _line, _comment, lines, index) { + return text.replace(regex, (_, group, range = ':1') => { + const lineNum = Number.parseInt(range.slice(1), 10) + + lines + .slice(index, index + lineNum) + .forEach((line) => { + this.addClassToHast(line, classMap[group]) + }) + + if (classActivePre) + this.addClassToHast(this.pre, classActivePre) + + return '' + }) }, ) } diff --git a/packages/transformers/src/utils-legacy.ts b/packages/transformers/src/utils-legacy.ts new file mode 100644 index 000000000..2095b9eb8 --- /dev/null +++ b/packages/transformers/src/utils-legacy.ts @@ -0,0 +1,63 @@ +import type { Element, Text } from 'hast' +import type { ShikiTransformer, ShikiTransformerContext } from 'shiki' + +export function createCommentNotationTransformer( + name: string, + regex: RegExp, + onMatch: ( + this: ShikiTransformerContext, + match: string[], + line: Element, + commentNode: Element, + lines: Element[], + index: number, + ) => boolean, + removeEmptyLines = false, +): ShikiTransformer { + return { + name, + code(code) { + const lines = code.children.filter(i => i.type === 'element') as Element[] + const linesToRemove: (Element | Text)[] = [] + lines.forEach((line, idx) => { + let nodeToRemove: Element | undefined + + for (const child of line.children) { + if (child.type !== 'element') + continue + const text = child.children[0] + if (text.type !== 'text') + continue + + let replaced = false + text.value = text.value.replace(regex, (...match) => { + if (onMatch.call(this, match, line, child, lines, idx)) { + replaced = true + return '' + } + return match[0] + }) + if (replaced && !text.value.trim()) + nodeToRemove = child + } + + if (nodeToRemove) { + line.children.splice(line.children.indexOf(nodeToRemove), 1) + + // Remove if empty + if (line.children.length === 0) { + linesToRemove.push(line) + if (removeEmptyLines) { + const next = code.children[code.children.indexOf(line) + 1] + if (next && next.type === 'text' && next.value === '\n') + linesToRemove.push(next) + } + } + } + }) + + for (const line of linesToRemove) + code.children.splice(code.children.indexOf(line), 1) + }, + } +} diff --git a/packages/transformers/src/utils.ts b/packages/transformers/src/utils.ts index 2095b9eb8..4921cdf2c 100644 --- a/packages/transformers/src/utils.ts +++ b/packages/transformers/src/utils.ts @@ -1,58 +1,67 @@ import type { Element, Text } from 'hast' import type { ShikiTransformer, ShikiTransformerContext } from 'shiki' -export function createCommentNotationTransformer( +/** + * Regex that matches code comments + */ +const regex = /((?:\/\/|\/\*||\*\/|$)/ + +/** + * Create a transformer to process comment notations + * + * @param name transformer name + * @param onMatch function to be called when found a comment in code, return the replaced text. + */ +export function createCommentNotationTransformerExperimental( name: string, - regex: RegExp, onMatch: ( this: ShikiTransformerContext, - match: string[], + commentText: string, line: Element, commentNode: Element, lines: Element[], index: number, - ) => boolean, - removeEmptyLines = false, + ) => string, ): ShikiTransformer { return { name, code(code) { const lines = code.children.filter(i => i.type === 'element') as Element[] const linesToRemove: (Element | Text)[] = [] - lines.forEach((line, idx) => { - let nodeToRemove: Element | undefined - - for (const child of line.children) { - if (child.type !== 'element') - continue - const text = child.children[0] - if (text.type !== 'text') - continue - - let replaced = false - text.value = text.value.replace(regex, (...match) => { - if (onMatch.call(this, match, line, child, lines, idx)) { - replaced = true - return '' - } - return match[0] - }) - if (replaced && !text.value.trim()) - nodeToRemove = child - } - if (nodeToRemove) { - line.children.splice(line.children.indexOf(nodeToRemove), 1) - - // Remove if empty - if (line.children.length === 0) { - linesToRemove.push(line) - if (removeEmptyLines) { - const next = code.children[code.children.indexOf(line) + 1] - if (next && next.type === 'text' && next.value === '\n') - linesToRemove.push(next) - } - } + lines.forEach((line, lineIdx) => { + // comment should be at the end of line (last token) + const last = line.children.findLast(i => i.type === 'element') as Element | undefined + + if (!last || last.children.length === 0) + return + const text = last.children[0] + if (text.type !== 'text') + return + + let deleteComment = false + + const isEmptyLine = line.children.length === 1 + text.value = text.value.replace(regex, (_, prefix, text, end) => { + // no other tokens except the comment + const replaced = onMatch.call(this, text, line, last, lines, + // take the next line if the current line will be removed + isEmptyLine ? lineIdx + 1 : lineIdx) + + if (replaced.trim().length === 0) + deleteComment = true + + return prefix + replaced + end + }) + + if (!deleteComment) + return + + if (isEmptyLine) { + linesToRemove.push(line) + } + else { + line.children.splice(line.children.indexOf(last), 1) } }) @@ -61,3 +70,5 @@ export function createCommentNotationTransformer( }, } } + +export { createCommentNotationTransformer } from './utils-legacy' diff --git a/packages/transformers/test/fixtures/all/a.js b/packages/transformers/test/fixtures/all/a.js index d00d765cc..1f4f6e8b2 100644 --- a/packages/transformers/test/fixtures/all/a.js +++ b/packages/transformers/test/fixtures/all/a.js @@ -1,5 +1,5 @@ function hello(indentSize, type) { if (indentSize === 4 && type !== 'tab') { - console.log('Each next indentation will increase on 4 spaces'); // [!code error] // [!code focus] + console.log('Each next indentation will increase on 4 spaces'); // [!code error] [!code focus] } } diff --git a/packages/transformers/test/fixtures/focus/empty-line-comment.js.output.html b/packages/transformers/test/fixtures/focus/empty-line-comment.js.output.html index d9d8a95d0..284537c34 100644 --- a/packages/transformers/test/fixtures/focus/empty-line-comment.js.output.html +++ b/packages/transformers/test/fixtures/focus/empty-line-comment.js.output.html @@ -1,4 +1,4 @@ -
export function transformerNotationFocus(  options = {},) {  const {    classFocused = 'focused',    classActivePre = 'has-focused',  } = options}
+
export function transformerNotationFocus(  options = {},) {  const {    classFocused = 'focused',    classActivePre = 'has-focused',  } = options}