From c180fdaa08301dc8cd57691aa3f00cca40f975ec Mon Sep 17 00:00:00 2001 From: Fuma Nama Date: Thu, 14 Nov 2024 15:10:55 +0800 Subject: [PATCH 1/5] merge from `shiki-transformers` --- .../transformers/src/shared/highlight-word.ts | 9 +- .../src/shared/notation-transformer.ts | 75 ++++++++++++ .../transformers/src/shared/parse-comments.ts | 109 ++++++++++++++++++ .../transformers/notation-highlight-word.ts | 14 +-- .../src/transformers/notation-map.ts | 14 +-- ....ts => transformer-meta-highlight-word.ts} | 0 ...light.ts => transformer-meta-highlight.ts} | 13 ++- 7 files changed, 210 insertions(+), 24 deletions(-) create mode 100644 packages/transformers/src/shared/notation-transformer.ts create mode 100644 packages/transformers/src/shared/parse-comments.ts rename packages/transformers/src/transformers/{meta-highlight-word.ts => transformer-meta-highlight-word.ts} (100%) rename packages/transformers/src/transformers/{meta-highlight.ts => transformer-meta-highlight.ts} (80%) diff --git a/packages/transformers/src/shared/highlight-word.ts b/packages/transformers/src/shared/highlight-word.ts index 7aef3b04c..9f9b1ffec 100644 --- a/packages/transformers/src/shared/highlight-word.ts +++ b/packages/transformers/src/shared/highlight-word.ts @@ -24,6 +24,7 @@ function getTextContent(element: ElementContent): string { * @param ignoredElement * @param index highlight beginning index * @param len highlight length + * @param className class name to add to highlighted nodes */ function highlightRange( this: ShikiTransformerContext, @@ -64,14 +65,14 @@ function highlightRange( } } -function hasOverlap(range1: [number, number], range2: [ number, number]): boolean { +function hasOverlap(range1: [number, number], range2: [number, number]): boolean { return (range1[0] <= range2[1]) && (range1[1]) >= range2[0] } function separateToken(span: Element, textNode: Text, index: number, len: number): [ - before: Element | undefined, - med: Element, - after: Element | undefined, + before: Element | undefined, + med: Element, + after: Element | undefined, ] { const text = textNode.value diff --git a/packages/transformers/src/shared/notation-transformer.ts b/packages/transformers/src/shared/notation-transformer.ts new file mode 100644 index 000000000..acbcc4ae4 --- /dev/null +++ b/packages/transformers/src/shared/notation-transformer.ts @@ -0,0 +1,75 @@ +import type { Element, Text } from 'hast' +import type { ShikiTransformer, ShikiTransformerContext } from 'shiki' +import { parseComments, type ParsedComments } from './parse-comments' + +export function createCommentNotationTransformer( + name: string, + regex: RegExp, + onMatch: ( + this: ShikiTransformerContext, + match: string[], + line: Element, + commentNode: Element, + lines: Element[], + index: number + ) => boolean, +): ShikiTransformer { + return { + name, + code(code) { + const lines = code.children.filter(i => i.type === 'element') + const linesToRemove: (Element | Text)[] = [] + + code.data ??= {} + const data = code.data as { + _shiki_notation?: ParsedComments + } + + data._shiki_notation ??= parseComments(lines, ['jsx', 'tsx'].includes(this.options.lang)) + const parsed = data._shiki_notation + + for (const comment of parsed) { + if (comment.info[1].length === 0) + continue + + const isLineCommentOnly = comment.line.children.length === (comment.isJsxStyle ? 3 : 1) + let lineIdx = lines.indexOf(comment.line) + if (isLineCommentOnly) + lineIdx++ + + comment.info[1] = comment.info[1].replace(regex, (...match) => { + if (onMatch.call(this, match, comment.line, comment.token, lines, lineIdx)) { + return '' + } + + return match[0] + }) + + const isEmpty = comment.info[1].trim().length === 0 + // ignore comment node + if (isEmpty) + comment.info[1] = '' + + if (isEmpty && isLineCommentOnly) { + linesToRemove.push(comment.line) + } + else if (isEmpty && comment.isJsxStyle) { + comment.line.children.splice(comment.line.children.indexOf(comment.token) - 1, 3) + } + else if (isEmpty) { + comment.line.children.splice(comment.line.children.indexOf(comment.token), 1) + } + else { + const head = comment.token.children[0] + + if (head.type === 'text') { + head.value = comment.info.join('') + } + } + } + + for (const line of linesToRemove) + code.children.splice(code.children.indexOf(line), 1) + }, + } +} diff --git a/packages/transformers/src/shared/parse-comments.ts b/packages/transformers/src/shared/parse-comments.ts new file mode 100644 index 000000000..f366e7e03 --- /dev/null +++ b/packages/transformers/src/shared/parse-comments.ts @@ -0,0 +1,109 @@ +import type { Element, ElementContent } from 'hast' + +export type ParsedComments = { + line: Element + token: Element + info: [prefix: string, content: string, suffix?: string] + isJsxStyle: boolean +}[] + +/** + * some comment formats have to be located at the end of line + * hence we can skip matching them for other tokens + */ +const matchers: [re: RegExp, endOfLine: boolean][] = [ + [/^()$/, false], + [/^(\/\*)(.+)(\*\/)$/, false], + [/^(\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/, true], + /** + * for multi-line comments like this + */ + [/^(\*)(.+)$/, true], +] + +export function parseComments(lines: Element[], jsx: boolean): ParsedComments { + const out: ParsedComments = [] + + for (const line of lines) { + const elements = line.children + // one step further for JSX as it's inside `{}` + const start = jsx ? elements.length - 2 : elements.length - 1 + + for (let i = Math.max(start, 0); i < elements.length; i++) { + const token = elements[i] + + if (token.type !== 'element') + continue + + const isLast = i === elements.length - 1 + const match = matchToken(token, isLast) + if (!match) + continue + + if (jsx && !isLast && i !== 0) { + const left = elements[i - 1] + const right = elements[i + 1] + + out.push({ + info: match, + line, + token, + isJsxStyle: isValue(left, '{') && isValue(right, '}'), + }) + } + else { + out.push({ + info: match, + line, + token, + isJsxStyle: false, + }) + } + } + } + + return out +} + +function isValue(element: ElementContent, value: string): boolean { + if (element.type !== 'element') + return false + const text = element.children[0] + if (text.type !== 'text') + return false + + return text.value.trim() === value +} + +/** + * @param token the token node (children of line) + * @param isLast whether the token is located at the end of line + */ +function matchToken(token: Element, isLast: boolean): [prefix: string, content: string, suffix?: string] | undefined { + const text = token.children[0] + if (text.type !== 'text') + return + + for (const [matcher, endOfLine] of matchers) { + if (endOfLine && !isLast) + continue + + // no leading and trailing spaces allowed for matchers + // we extract the spaces + let trimmed = text.value.trimStart() + const spaceFront = text.value.length - trimmed.length + + trimmed = trimmed.trimEnd() + const spaceEnd = text.value.length - trimmed.length - spaceFront + + const result = matcher.exec(trimmed) + if (!result) + continue + + return [ + ' '.repeat(spaceFront) + result[1], + result[2], + result[3] ? result[3] + ' '.repeat(spaceEnd) : undefined, + ] + } +} diff --git a/packages/transformers/src/transformers/notation-highlight-word.ts b/packages/transformers/src/transformers/notation-highlight-word.ts index 1e5cc5cbc..499ca361e 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 { createCommentNotationTransformer } from '../shared/notation-transformer' export interface TransformerNotationWordHighlightOptions { /** @@ -23,23 +23,19 @@ export function transformerNotationWordHighlight( return createCommentNotationTransformer( '@shikijs/transformers:notation-highlight-word', - // comment-start | marker | word | range | comment-end - /^\s*(?:\/\/|\/\*|)?/, + /\s*\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/, 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)) + for (let i = index; i < Math.min(index + lineNum, lines.length); i++) { + highlightWordInLine.call(this, lines[i], comment, word, classActiveWord) + } if (classActivePre) this.addClassToHast(this.pre, classActivePre) return true }, - true, // remove empty lines ) } diff --git a/packages/transformers/src/transformers/notation-map.ts b/packages/transformers/src/transformers/notation-map.ts index 728f7e763..06ea8dcab 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 { createCommentNotationTransformer } from '../shared/notation-transformer' export interface TransformerNotationMapOptions { classMap?: Record @@ -24,14 +24,14 @@ export function transformerNotationMap( return createCommentNotationTransformer( name, - new RegExp(`\\s*(?://|/\\*|)?\\s*$`), + new RegExp(`\\s*\\[!code (${Object.keys(classMap).map(escapeRegExp).join('|')})(:\\d+)?\\]`), 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]) - }) + + for (let i = index; i < Math.min(index + lineNum, lines.length); i++) { + this.addClassToHast(lines[i], classMap[match]) + } + if (classActivePre) this.addClassToHast(this.pre, classActivePre) return true diff --git a/packages/transformers/src/transformers/meta-highlight-word.ts b/packages/transformers/src/transformers/transformer-meta-highlight-word.ts similarity index 100% rename from packages/transformers/src/transformers/meta-highlight-word.ts rename to packages/transformers/src/transformers/transformer-meta-highlight-word.ts diff --git a/packages/transformers/src/transformers/meta-highlight.ts b/packages/transformers/src/transformers/transformer-meta-highlight.ts similarity index 80% rename from packages/transformers/src/transformers/meta-highlight.ts rename to packages/transformers/src/transformers/transformer-meta-highlight.ts index cc5c7ae08..ac7b8ce3d 100644 --- a/packages/transformers/src/transformers/meta-highlight.ts +++ b/packages/transformers/src/transformers/transformer-meta-highlight.ts @@ -12,8 +12,8 @@ export function parseMetaHighlightString(meta: string): number[] | null { const num = v.split('-').map(v => Number.parseInt(v, 10)) if (num.length === 1) return [num[0]] - else - return Array.from({ length: num[1] - num[0] + 1 }, (_, i) => i + num[0]) + + return Array.from({ length: num[1] - num[0] + 1 }, (_, i) => i + num[0]) }) return lines } @@ -45,8 +45,13 @@ export function transformerMetaHighlight( if (!this.options.meta?.__raw) { return } - ;(this.meta as any)[symbol] ||= parseMetaHighlightString(this.options.meta.__raw) - const lines: number[] = (this.meta as any)[symbol] || [] + const meta = this.meta as { + [symbol]: number[] | null + } + + meta[symbol] ??= parseMetaHighlightString(this.options.meta.__raw) + const lines: number[] = meta[symbol] ?? [] + if (lines.includes(line)) this.addClassToHast(node, className) return node From 2456510e0fb6a12bcd75d7f976165020b0586e80 Mon Sep 17 00:00:00 2001 From: Fuma Nama Date: Thu, 14 Nov 2024 15:19:19 +0800 Subject: [PATCH 2/5] apply rename --- ...{transformer-meta-highlight-word.ts => meta-highlight-word.ts} | 0 .../{transformer-meta-highlight.ts => meta-highlight.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/transformers/src/transformers/{transformer-meta-highlight-word.ts => meta-highlight-word.ts} (100%) rename packages/transformers/src/transformers/{transformer-meta-highlight.ts => meta-highlight.ts} (100%) diff --git a/packages/transformers/src/transformers/transformer-meta-highlight-word.ts b/packages/transformers/src/transformers/meta-highlight-word.ts similarity index 100% rename from packages/transformers/src/transformers/transformer-meta-highlight-word.ts rename to packages/transformers/src/transformers/meta-highlight-word.ts diff --git a/packages/transformers/src/transformers/transformer-meta-highlight.ts b/packages/transformers/src/transformers/meta-highlight.ts similarity index 100% rename from packages/transformers/src/transformers/transformer-meta-highlight.ts rename to packages/transformers/src/transformers/meta-highlight.ts From 693a60c424f69002684cc7630a57aee545f45816 Mon Sep 17 00:00:00 2001 From: Fuma Nama Date: Thu, 14 Nov 2024 15:37:17 +0800 Subject: [PATCH 3/5] remove unused files --- .../src/shared/notation-transformer.ts | 2 +- packages/transformers/src/utils.ts | 63 ------------------- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 packages/transformers/src/utils.ts diff --git a/packages/transformers/src/shared/notation-transformer.ts b/packages/transformers/src/shared/notation-transformer.ts index acbcc4ae4..ee4806f8a 100644 --- a/packages/transformers/src/shared/notation-transformer.ts +++ b/packages/transformers/src/shared/notation-transformer.ts @@ -20,7 +20,7 @@ export function createCommentNotationTransformer( const lines = code.children.filter(i => i.type === 'element') const linesToRemove: (Element | Text)[] = [] - code.data ??= {} + code.data ??= {} as any const data = code.data as { _shiki_notation?: ParsedComments } diff --git a/packages/transformers/src/utils.ts b/packages/transformers/src/utils.ts deleted file mode 100644 index 2095b9eb8..000000000 --- a/packages/transformers/src/utils.ts +++ /dev/null @@ -1,63 +0,0 @@ -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) - }, - } -} From d703eea8d93fe92f7da08290373dbd155fb7804d Mon Sep 17 00:00:00 2001 From: Fuma Nama Date: Thu, 14 Nov 2024 16:15:20 +0800 Subject: [PATCH 4/5] remove export --- packages/transformers/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts index a7f5291a7..b4118a202 100644 --- a/packages/transformers/src/index.ts +++ b/packages/transformers/src/index.ts @@ -10,4 +10,3 @@ export * from './transformers/remove-line-breaks' export * from './transformers/remove-notation-escape' export * from './transformers/render-whitespace' export * from './transformers/style-to-class' -export * from './utils' From d5696e0c1844a9f5adc329cb32aa18e2e0677ad3 Mon Sep 17 00:00:00 2001 From: Fuma Nama Date: Mon, 18 Nov 2024 22:12:35 +0800 Subject: [PATCH 5/5] feat(transformers): add `legacy` option for notation transformers --- .../src/shared/notation-transformer.ts | 16 ++++- .../transformers/src/shared/parse-comments.ts | 62 ++++++++++++------- .../src/transformers/notation-diff.ts | 4 ++ .../src/transformers/notation-error-level.ts | 4 ++ .../src/transformers/notation-focus.ts | 4 ++ .../transformers/notation-highlight-word.ts | 4 ++ .../src/transformers/notation-highlight.ts | 4 ++ .../src/transformers/notation-map.ts | 4 ++ packages/transformers/test/fixtures.test.ts | 24 +++++++ packages/transformers/test/fixtures/all/a.js | 2 +- .../test/fixtures/focus/empty-line-comment.js | 2 +- .../test/fixtures/highlight-word/complex.js | 2 +- .../fixtures/highlight/empty-line-comment.js | 2 +- .../transformers/test/fixtures/legacy/a.js | 19 ++++++ .../test/fixtures/legacy/a.js.output.html | 12 ++++ 15 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 packages/transformers/test/fixtures/legacy/a.js create mode 100644 packages/transformers/test/fixtures/legacy/a.js.output.html diff --git a/packages/transformers/src/shared/notation-transformer.ts b/packages/transformers/src/shared/notation-transformer.ts index ee4806f8a..673ee66c0 100644 --- a/packages/transformers/src/shared/notation-transformer.ts +++ b/packages/transformers/src/shared/notation-transformer.ts @@ -1,6 +1,6 @@ import type { Element, Text } from 'hast' import type { ShikiTransformer, ShikiTransformerContext } from 'shiki' -import { parseComments, type ParsedComments } from './parse-comments' +import { legacyClearEndCommentPrefix, parseComments, type ParsedComments } from './parse-comments' export function createCommentNotationTransformer( name: string, @@ -13,6 +13,7 @@ export function createCommentNotationTransformer( lines: Element[], index: number ) => boolean, + legacy = false, ): ShikiTransformer { return { name, @@ -25,7 +26,7 @@ export function createCommentNotationTransformer( _shiki_notation?: ParsedComments } - data._shiki_notation ??= parseComments(lines, ['jsx', 'tsx'].includes(this.options.lang)) + data._shiki_notation ??= parseComments(lines, ['jsx', 'tsx'].includes(this.options.lang), legacy) const parsed = data._shiki_notation for (const comment of parsed) { @@ -34,17 +35,26 @@ export function createCommentNotationTransformer( const isLineCommentOnly = comment.line.children.length === (comment.isJsxStyle ? 3 : 1) let lineIdx = lines.indexOf(comment.line) - if (isLineCommentOnly) + if (isLineCommentOnly && !legacy) lineIdx++ + let replaced = false comment.info[1] = comment.info[1].replace(regex, (...match) => { if (onMatch.call(this, match, comment.line, comment.token, lines, lineIdx)) { + replaced = true return '' } return match[0] }) + if (!replaced) + continue + + if (legacy) { + comment.info[1] = legacyClearEndCommentPrefix(comment.info[1]) + } + const isEmpty = comment.info[1].trim().length === 0 // ignore comment node if (isEmpty) diff --git a/packages/transformers/src/shared/parse-comments.ts b/packages/transformers/src/shared/parse-comments.ts index f366e7e03..969edd589 100644 --- a/packages/transformers/src/shared/parse-comments.ts +++ b/packages/transformers/src/shared/parse-comments.ts @@ -21,34 +21,42 @@ const matchers: [re: RegExp, endOfLine: boolean][] = [ [/^(\*)(.+)$/, true], ] -export function parseComments(lines: Element[], jsx: boolean): ParsedComments { +/** + * @param lines line tokens + * @param jsx enable JSX parsing + * @param legacy support legacy behaviours, force to parse all tokens. + */ +export function parseComments(lines: Element[], jsx: boolean, legacy = false): ParsedComments { const out: ParsedComments = [] for (const line of lines) { const elements = line.children - // one step further for JSX as it's inside `{}` - const start = jsx ? elements.length - 2 : elements.length - 1 + let start = elements.length - 1 + if (legacy) + start = 0 + else if (jsx) + // one step further for JSX as comment is inside curly brackets + start = elements.length - 2 for (let i = Math.max(start, 0); i < elements.length; i++) { const token = elements[i] - if (token.type !== 'element') continue + const head = token.children.at(0) + if (head?.type !== 'text') + continue const isLast = i === elements.length - 1 - const match = matchToken(token, isLast) + const match = matchToken(head.value, isLast) if (!match) continue if (jsx && !isLast && i !== 0) { - const left = elements[i - 1] - const right = elements[i + 1] - out.push({ info: match, line, token, - isJsxStyle: isValue(left, '{') && isValue(right, '}'), + isJsxStyle: isValue(elements[i - 1], '{') && isValue(elements[i + 1], '}'), }) } else { @@ -76,26 +84,22 @@ function isValue(element: ElementContent, value: string): boolean { } /** - * @param token the token node (children of line) + * @param text text value of comment node * @param isLast whether the token is located at the end of line */ -function matchToken(token: Element, isLast: boolean): [prefix: string, content: string, suffix?: string] | undefined { - const text = token.children[0] - if (text.type !== 'text') - return +function matchToken(text: string, isLast: boolean): [prefix: string, content: string, suffix?: string] | undefined { + // no leading and trailing spaces allowed for matchers + // we extract the spaces + let trimmed = text.trimStart() + const spaceFront = text.length - trimmed.length + + trimmed = trimmed.trimEnd() + const spaceEnd = text.length - trimmed.length - spaceFront for (const [matcher, endOfLine] of matchers) { if (endOfLine && !isLast) continue - // no leading and trailing spaces allowed for matchers - // we extract the spaces - let trimmed = text.value.trimStart() - const spaceFront = text.value.length - trimmed.length - - trimmed = trimmed.trimEnd() - const spaceEnd = text.value.length - trimmed.length - spaceFront - const result = matcher.exec(trimmed) if (!result) continue @@ -107,3 +111,17 @@ function matchToken(token: Element, isLast: boolean): [prefix: string, content: ] } } + +/** + * Remove empty comment prefixes at line end, e.g. `// ` + */ +export function legacyClearEndCommentPrefix(text: string): string { + const regex = /(?:\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/ + const result = regex.exec(text) + + if (result && result[1].trim().length === 0) { + return text.slice(0, result.index) + } + + return text +} diff --git a/packages/transformers/src/transformers/notation-diff.ts b/packages/transformers/src/transformers/notation-diff.ts index b21f9b22f..31b0bcb3f 100644 --- a/packages/transformers/src/transformers/notation-diff.ts +++ b/packages/transformers/src/transformers/notation-diff.ts @@ -14,6 +14,8 @@ export interface TransformerNotationDiffOptions { * Class added to the
 element when the current code has diff
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 /**
@@ -26,6 +28,7 @@ export function transformerNotationDiff(
     classLineAdd = 'diff add',
     classLineRemove = 'diff remove',
     classActivePre = 'has-diff',
+    legacy,
   } = options
 
   return transformerNotationMap(
@@ -34,6 +37,7 @@ export function transformerNotationDiff(
         '++': classLineAdd,
         '--': classLineRemove,
       },
+      legacy,
       classActivePre,
     },
     '@shikijs/transformers:notation-diff',
diff --git a/packages/transformers/src/transformers/notation-error-level.ts b/packages/transformers/src/transformers/notation-error-level.ts
index 03a514f83..dac18567c 100644
--- a/packages/transformers/src/transformers/notation-error-level.ts
+++ b/packages/transformers/src/transformers/notation-error-level.ts
@@ -7,6 +7,8 @@ export interface TransformerNotationErrorLevelOptions {
    * Class added to the 
 element when the current code has diff
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 /**
@@ -21,12 +23,14 @@ export function transformerNotationErrorLevel(
       warning: ['highlighted', 'warning'],
     },
     classActivePre = 'has-highlighted',
+    legacy,
   } = options
 
   return transformerNotationMap(
     {
       classMap,
       classActivePre,
+      legacy,
     },
     '@shikijs/transformers:notation-error-level',
   )
diff --git a/packages/transformers/src/transformers/notation-focus.ts b/packages/transformers/src/transformers/notation-focus.ts
index 1af4dce16..a1d7b6c81 100644
--- a/packages/transformers/src/transformers/notation-focus.ts
+++ b/packages/transformers/src/transformers/notation-focus.ts
@@ -10,6 +10,8 @@ export interface TransformerNotationFocusOptions {
    * Class added to the root element when the code has focused lines
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 /**
@@ -21,6 +23,7 @@ export function transformerNotationFocus(
   const {
     classActiveLine = 'focused',
     classActivePre = 'has-focused',
+    legacy,
   } = options
 
   return transformerNotationMap(
@@ -29,6 +32,7 @@ export function transformerNotationFocus(
         focus: classActiveLine,
       },
       classActivePre,
+      legacy,
     },
     '@shikijs/transformers:notation-focus',
   )
diff --git a/packages/transformers/src/transformers/notation-highlight-word.ts b/packages/transformers/src/transformers/notation-highlight-word.ts
index 499ca361e..f00bd05a1 100644
--- a/packages/transformers/src/transformers/notation-highlight-word.ts
+++ b/packages/transformers/src/transformers/notation-highlight-word.ts
@@ -11,6 +11,8 @@ export interface TransformerNotationWordHighlightOptions {
    * Class added to the root element when the code has highlighted words
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 export function transformerNotationWordHighlight(
@@ -19,6 +21,7 @@ export function transformerNotationWordHighlight(
   const {
     classActiveWord = 'highlighted-word',
     classActivePre = undefined,
+    legacy,
   } = options
 
   return createCommentNotationTransformer(
@@ -37,5 +40,6 @@ export function transformerNotationWordHighlight(
         this.addClassToHast(this.pre, classActivePre)
       return true
     },
+    legacy,
   )
 }
diff --git a/packages/transformers/src/transformers/notation-highlight.ts b/packages/transformers/src/transformers/notation-highlight.ts
index f0287971f..ea56ec280 100644
--- a/packages/transformers/src/transformers/notation-highlight.ts
+++ b/packages/transformers/src/transformers/notation-highlight.ts
@@ -10,6 +10,8 @@ export interface TransformerNotationHighlightOptions {
    * Class added to the root element when the code has highlighted lines
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 /**
@@ -21,6 +23,7 @@ export function transformerNotationHighlight(
   const {
     classActiveLine = 'highlighted',
     classActivePre = 'has-highlighted',
+    legacy,
   } = options
 
   return transformerNotationMap(
@@ -29,6 +32,7 @@ export function transformerNotationHighlight(
         highlight: classActiveLine,
         hl: classActiveLine,
       },
+      legacy,
       classActivePre,
     },
     '@shikijs/transformers:notation-highlight',
diff --git a/packages/transformers/src/transformers/notation-map.ts b/packages/transformers/src/transformers/notation-map.ts
index 06ea8dcab..5ef445f06 100644
--- a/packages/transformers/src/transformers/notation-map.ts
+++ b/packages/transformers/src/transformers/notation-map.ts
@@ -7,6 +7,8 @@ export interface TransformerNotationMapOptions {
    * Class added to the 
 element when the current code has diff
    */
   classActivePre?: string
+
+  legacy?: boolean
 }
 
 function escapeRegExp(str: string): string {
@@ -20,6 +22,7 @@ export function transformerNotationMap(
   const {
     classMap = {},
     classActivePre = undefined,
+    legacy,
   } = options
 
   return createCommentNotationTransformer(
@@ -36,5 +39,6 @@ export function transformerNotationMap(
         this.addClassToHast(this.pre, classActivePre)
       return true
     },
+    legacy,
   )
 }
diff --git a/packages/transformers/test/fixtures.test.ts b/packages/transformers/test/fixtures.test.ts
index f92b6371b..7840adecc 100644
--- a/packages/transformers/test/fixtures.test.ts
+++ b/packages/transformers/test/fixtures.test.ts
@@ -185,3 +185,27 @@ body { margin: 0; }
 .space::before { content: "\\B7"; position: absolute; opacity: 0.3; }
 `,
 )
+
+suite(
+  'all',
+  import.meta.glob('./fixtures/legacy/*.*', { as: 'raw', eager: true }),
+  [
+    transformerNotationFocus({ legacy: true }),
+    transformerNotationHighlight({ legacy: true }),
+    transformerNotationErrorLevel({ legacy: true }),
+    transformerNotationWordHighlight({ legacy: true }),
+    transformerRemoveLineBreak(),
+  ],
+  code => `${code}
+`,
+)
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 b/packages/transformers/test/fixtures/focus/empty-line-comment.js
index c9547605e..923d2ef59 100644
--- a/packages/transformers/test/fixtures/focus/empty-line-comment.js
+++ b/packages/transformers/test/fixtures/focus/empty-line-comment.js
@@ -1,5 +1,5 @@
 export function transformerNotationFocus(
-  // [!code focus:5]
+  // [!code focus:4]
   options = {},
 ) {
   const {
diff --git a/packages/transformers/test/fixtures/highlight-word/complex.js b/packages/transformers/test/fixtures/highlight-word/complex.js
index 56e804c05..7cc93ca8f 100644
--- a/packages/transformers/test/fixtures/highlight-word/complex.js
+++ b/packages/transformers/test/fixtures/highlight-word/complex.js
@@ -1,6 +1,6 @@
 export function transformerNotationFocus(
   // [!code word:options.a]
-  options = {}, // [!code word:console.log:3]
+  options = {}, // [!code word:console.log:4]
 ) {
   const options = 'options'
   console.log(options)
diff --git a/packages/transformers/test/fixtures/highlight/empty-line-comment.js b/packages/transformers/test/fixtures/highlight/empty-line-comment.js
index 6ca3a5240..3c71214e8 100644
--- a/packages/transformers/test/fixtures/highlight/empty-line-comment.js
+++ b/packages/transformers/test/fixtures/highlight/empty-line-comment.js
@@ -1,5 +1,5 @@
 export function transformerNotationFocus(
-  // [!code highlight:5]
+  // [!code highlight:4]
   options = {},
 ) {
   const {
diff --git a/packages/transformers/test/fixtures/legacy/a.js b/packages/transformers/test/fixtures/legacy/a.js
new file mode 100644
index 000000000..de2d43864
--- /dev/null
+++ b/packages/transformers/test/fixtures/legacy/a.js
@@ -0,0 +1,19 @@
+function hello(indentSize, type) {
+  console.log('error and focus'); // [!code error] // [!code focus]
+}
+
+// [!code focus:5]
+console.log('focus')
+console.log('focus')
+console.log('focus')
+console.log('focus')
+
+// [!code highlight:3]
+console.log('highlighted')
+console.log('highlighted')
+
+// [!code word:options:4]
+let options = 'options'
+options = {}, // [!code word:log]
+console.log(options)
+options.a = "HELLO" // should not be highlighted
diff --git a/packages/transformers/test/fixtures/legacy/a.js.output.html b/packages/transformers/test/fixtures/legacy/a.js.output.html
new file mode 100644
index 000000000..f541a691d
--- /dev/null
+++ b/packages/transformers/test/fixtures/legacy/a.js.output.html
@@ -0,0 +1,12 @@
+
function hello(indentSize, type) {  console.log('error and focus'); }console.log('focus')console.log('focus')console.log('focus')console.log('focus')console.log('highlighted')console.log('highlighted')let options = 'options'options = {}, console.log(options)options.a = "HELLO" // should not be highlighted
+ \ No newline at end of file