Skip to content
This repository has been archived by the owner on Feb 7, 2024. It is now read-only.

feat(transformers): Support word highlighting across multiple tokens #116

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ee6e48b
Support word highlighting
fuma-nama Jan 13, 2024
6772afa
Update tests & docs
fuma-nama Jan 13, 2024
243c29d
Ignore whitespaces in span elements
fuma-nama Jan 13, 2024
d214d8c
Split whitespace & highlighted span
fuma-nama Jan 13, 2024
0968a52
feat: Support filtering meta string before processing highlights
fuma-nama Jan 14, 2024
4ec5d16
feat: Support word highlighting for rehype & markdown-it
fuma-nama Jan 14, 2024
4fc7ed9
Merge branch 'main' into main
fuma-nama Jan 14, 2024
b80c531
feat(transformers): Support `transformerNotationWordHighlight`
fuma-nama Jan 14, 2024
69ba597
Merge branch 'main' of https://github.com/fuma-nama/shikiji
fuma-nama Jan 14, 2024
b4ff342
Merge branch 'main' of https://github.com/antfu/shikiji into antfu-main
fuma-nama Jan 14, 2024
2fe47d7
Merge branch 'antfu-main'
fuma-nama Jan 14, 2024
86c20b8
Remove conflicts
fuma-nama Jan 14, 2024
245aad9
Remove legacy docs
fuma-nama Jan 15, 2024
ebd2f7b
Merge branch 'main' into main
fuma-nama Jan 15, 2024
3785cbf
Discard changes to docs/packages/rehype.md
antfu Jan 15, 2024
1fbae06
Discard changes to docs/.vitepress/config.ts
antfu Jan 15, 2024
4c611d3
Merge branch 'antfu:main' into main
fuma-nama Jan 16, 2024
8ac236d
Update syntax
fuma-nama Jan 16, 2024
dd56021
Implement `transformerMetaWordHighlight` transformer
fuma-nama Jan 17, 2024
2fc4ad6
Update docs
fuma-nama Jan 17, 2024
7d915ad
Merge branch 'main' into pr/fuma-nama/92
antfu Jan 17, 2024
4ad0755
chore: refactor
antfu Jan 17, 2024
ff9619f
chore: refactor
antfu Jan 17, 2024
e7f3216
chore: docs update
antfu Jan 17, 2024
4d3e5b3
feat: remove empty line
antfu Jan 17, 2024
a48b3cb
chore: avoid using structure clone
antfu Jan 17, 2024
54bea1b
Merge branch 'main' of https://github.com/fuma-nama/shikiji
fuma-nama Jan 26, 2024
0cac5a7
Support highlighting across multiple tokens
fuma-nama Jan 26, 2024
22d89a8
Remove empty lines
fuma-nama Jan 26, 2024
01080d9
Fix implicit external error
fuma-nama Jan 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/shikiji-transformers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"prepublishOnly": "nr build"
},
"dependencies": {
"hast-util-to-string": "^3.0.0",
"shikiji": "workspace:*"
}
}
97 changes: 65 additions & 32 deletions packages/shikiji-transformers/src/shared/highlight-word.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,66 @@
import type { Element } from 'hast'
import type { Element, ElementContent, Text } from 'hast'
import { addClassToHast } from 'shikiji/core'
import { toString as hastToString } from 'hast-util-to-string'

export function highlightWordInLine(line: Element, ignoredElement: Element | null, word: string, className: string): void {
line.children = line.children.flatMap((span) => {
if (span.type !== 'element' || span.tagName !== 'span' || span === ignoredElement)
return span
const content = hastToString(line)
let index = content.indexOf(word)

const textNode = span.children[0]
while (index !== -1) {
highlightRange(line.children, ignoredElement, index, word.length, className)

index = content.indexOf(word, index + 1)
}
}

/**
* @param elements
* @param ignoredElement
* @param index highlight beginning index
* @param len highlight length
*/
function highlightRange(elements: ElementContent[], ignoredElement: Element | null, index: number, len: number, className: string) {
let currentIdx = 0

for (let i = 0; i < elements.length; i++) {
const element = elements[i]
if (element.type !== 'element' || element.tagName !== 'span' || element === ignoredElement)
continue
const textNode = element.children[0]
if (textNode.type !== 'text')
return span
continue

return replaceSpan(span, textNode.value, word, className) ?? span
})
}
// check if it is overlapped with highlight range
if (hasOverlap([currentIdx, currentIdx + textNode.value.length - 1], [index, index + len])) {
const start = Math.max(0, index - currentIdx)
const length = len - Math.max(0, currentIdx - index)

function inheritElement(original: Element, overrides: Partial<Element>): Element {
return {
...original,
properties: {
...original.properties,
},
...overrides,
if (length === 0)
continue

const separated = separateToken(element, textNode, start, length)
addClassToHast(separated[1], className)

// insert
const output = separated.filter(Boolean) as Element[]
elements.splice(i, 1, ...output)
i += output.length - 1
}

currentIdx += textNode.value.length
}
}

function replaceSpan(span: Element, text: string, word: string, className: string): Element[] | undefined {
const index = text.indexOf(word)
function hasOverlap(range1: [number, number], range2: [ number, number]): boolean {
return (range1[0] <= range2[1]) && (range1[1]) >= range2[0]
}

if (index === -1)
return
function separateToken(span: Element, textNode: Text, index: number, len: number): [
before: Element | undefined,
med: Element,
after: Element | undefined,
] {
const text = textNode.value

const createNode = (value: string) => inheritElement(span, {
children: [
Expand All @@ -40,17 +71,19 @@ function replaceSpan(span: Element, text: string, word: string, className: strin
],
})

const nodes: Element[] = []

if (index > 0)
nodes.push(createNode(text.slice(0, index)))

const highlightedNode = createNode(word)
addClassToHast(highlightedNode, className)
nodes.push(highlightedNode)

if (index + word.length < text.length)
nodes.push(createNode(text.slice(index + word.length)))
return [
index > 0 ? createNode(text.slice(0, index)) : undefined,
createNode(text.slice(index, index + len)),
index + len < text.length ? createNode(text.slice(index + len)) : undefined,
]
}

return nodes
function inheritElement(original: Element, overrides: Partial<Element>): Element {
return {
...original,
properties: {
...original.properties,
},
...overrides,
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export function transformerNotationFocus(
// [!code word:options.a]
options = {}, // [!code word:console.log:2]
options = {}, // [!code word:console.log:3]
) {
const options = 'options'
console.log(options) // TODO: cross-token highlighting should be supported
console.log(options)
options.a = "HELLO"
console.log('// [!code word:options.a]')
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70"> options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'options'</span></span><span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(options) </span><span style="color:#6A737D">// TODO: cross-token highlighting should be supported</span></span><span class="line"><span style="color:#E1E4E8"> </span><span style="color:#E1E4E8" class="highlighted-word">options.a</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span></span><span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'// [!code word:</span><span style="color:#9ECBFF" class="highlighted-word">options.a</span><span style="color:#9ECBFF">]'</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70"> options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'options'</span></span><span class="line"><span style="color:#E1E4E8"> </span><span style="color:#E1E4E8" class="highlighted-word">console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8"> </span><span style="color:#E1E4E8" class="highlighted-word">options.a</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span></span><span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'// [!code word:</span><span style="color:#9ECBFF" class="highlighted-word">options.a</span><span style="color:#9ECBFF">]'</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
<style>
body { margin: 0; }
.shiki { padding: 1em; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export function transformerNotationFocus(
// [!code word:options:4]
// [!code word:'options':4]
options = {},
) {
const options = 'options'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70"> </span><span style="color:#FFAB70" class="highlighted-word">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {},</span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> </span><span style="color:#79B8FF" class="highlighted-word">options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> '</span><span style="color:#9ECBFF" class="highlighted-word">options</span><span style="color:#9ECBFF">'</span></span><span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8"> options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70"> options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {},</span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> </span><span style="color:#9ECBFF" class="highlighted-word">'options'</span></span><span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8"> options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
<style>
body { margin: 0; }
.shiki { padding: 1em; }
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading