Skip to content

Commit

Permalink
feat(transformers): two new transformers for word highlighting (#92)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
fuma-nama and antfu authored Jan 17, 2024
1 parent ac6298b commit ecf60cd
Show file tree
Hide file tree
Showing 24 changed files with 361 additions and 25 deletions.
3 changes: 3 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DefaultTheme } from 'vitepress'
import { defineConfig } from 'vitepress'
import { bundledThemes } from 'shikiji'
import { transformerMetaWordHighlight, transformerNotationWordHighlight } from '../../packages/shikiji-transformers/src'
import { defaultHoverInfoProcessor, transformerTwoslash } from '../../packages/vitepress-plugin-twoslash/src/index'
import { version } from '../../package.json'
import vite from './vite.config'
Expand Down Expand Up @@ -54,6 +55,8 @@ export default defineConfig({
}))
},
codeTransformers: [
transformerMetaWordHighlight(),
transformerNotationWordHighlight(),
{
// Render custom themes with codeblocks
name: 'shikiji:inline-theme',
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'floating-vue/dist/style.css'
import '../../../packages/vitepress-plugin-twoslash/src/style.css'
import 'uno.css'
import './style.css'
import './transformers.css'

export default {
extends: Theme,
Expand Down
17 changes: 0 additions & 17 deletions docs/.vitepress/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -181,20 +181,3 @@
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}

.vp-code .tab,
.vp-code .space {
position: relative;
}

.vp-code .tab::before {
content: '⇥';
position: absolute;
opacity: 0.3;
}

.vp-code .space::before {
content: '·';
position: absolute;
opacity: 0.3;
}
24 changes: 24 additions & 0 deletions docs/.vitepress/theme/transformers.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.vp-code .tab,
.vp-code .space {
position: relative;
}

.vp-code .tab::before {
content: '⇥';
position: absolute;
opacity: 0.3;
}

.vp-code .space::before {
content: '·';
position: absolute;
opacity: 0.3;
}

.vp-code .highlighted-word {
background-color: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-border);
padding: 1px 3px;
margin: -1px -3px;
border-radius: 4px;
}
92 changes: 92 additions & 0 deletions docs/packages/transformers.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ export function foo() {

Use `[!code highlight]` to highlight a line (adding `highlighted` class).

````md
```ts
export function foo() {
console.log('Highlighted') // [\!code highlight]
}
```
````

Results in

```ts
export function foo() {
console.log('Highlighted') // [!code highlight]
Expand All @@ -98,10 +108,62 @@ Alternatively, you can use the [`transformerMetaHighlight`](#transformermetahigh

---

### `transformerNotationWordHighlight`

Use `[!code word:xxx]` to highlight a word (adding `highlighted-word` class).

````md
```ts
export function foo() { // [\!code word:Hello]
const msg = 'Hello World'
console.log(msg) // prints Hello World
}
```
````

Results in

```ts
export function foo() { // [!code word:Hello]
const msg = 'Hello World'
console.log(msg) // prints Hello World
}
```

You can also specify the number of occurrences to highlight, e.g. `[!code word:options:2]` will highlight the next 2 occurrences of `options`.

````md
```ts
// [\!code word:options:2]
const options = { foo: 'bar' }
options.foo = 'baz'
console.log(options.foo) // this one will not be highlighted
```
````

```ts
// [!code word:options:2]
const options = { foo: 'bar' }
options.foo = 'baz'
console.log(options.foo) // this one will not be highlighted
```

---

### `transformerNotationFocus`

Use `[!code focus]` to focus a line (adding `focused` class).

````md
```ts
export function foo() {
console.log('Focused') // [\!code focus]
}
```
````

Results in

```ts
export function foo() {
console.log('Focused') // [!code focus]
Expand All @@ -114,6 +176,17 @@ export function foo() {

Use `[!code error]`, `[!code warning]`, to mark a line with an error level (adding `highlighted error`, `highlighted warning` class).

````md
```ts
export function foo() {
console.error('Error') // [\!code error]
console.warn('Warning') // [\!code warning]
}
```
````

Results in

```ts
export function foo() {
console.error('Error') // [!code error]
Expand Down Expand Up @@ -181,6 +254,25 @@ console.log('3')
console.log('4')
```

### `transformerMetaWordHighlight`

Highlight words based on the meta string provided on the code snippet. Requires integrations supports.

````md
```js /Hello/
const msg = 'Hello World'
console.log(msg)
console.log(msg) // prints Hello World
```
````

Results in

```js /Hello/
const msg = 'Hello World'
console.log(msg) // prints Hello World
```

---

### `transformerCompactLineOptions`
Expand Down
2 changes: 1 addition & 1 deletion packages/rehype-shikiji/test/fixtures/a.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

…world!

```js {3-4} fileName=test
```js {3-4} fileName=test /a/
console.log('it works!')

const a = 1
Expand Down
4 changes: 2 additions & 2 deletions packages/rehype-shikiji/test/fixtures/a.out.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ <h1>Hello</h1>
<p>…world!</p>
<pre class="shiki vitesse-light" style="background-color:#ffffff;color:#393a34" tabindex="0" fileName="test"><code><span class="line"><span style="color:#B07D48">console</span><span style="color:#999999">.</span><span style="color:#59873A">log</span><span style="color:#999999">(</span><span style="color:#B5695999">'</span><span style="color:#B56959">it works!</span><span style="color:#B5695999">'</span><span style="color:#999999">)</span></span>
<span class="line"></span>
<span class="line highlighted"><span style="color:#AB5959">const</span><span style="color:#B07D48"> a</span><span style="color:#999999"> =</span><span style="color:#2F798A"> 1</span></span>
<span class="line highlighted"><span style="color:#B07D48">console</span><span style="color:#999999">.</span><span style="color:#59873A">log</span><span style="color:#999999">(</span><span style="color:#B07D48">a</span><span style="color:#999999">)</span></span>
<span class="line highlighted"><span style="color:#AB5959">const</span><span style="color:#B07D48"> </span><span style="color:#B07D48" class="highlighted-word">a</span><span style="color:#999999"> =</span><span style="color:#2F798A"> 1</span></span>
<span class="line highlighted"><span style="color:#B07D48">console</span><span style="color:#999999">.</span><span style="color:#59873A">log</span><span style="color:#999999">(</span><span style="color:#B07D48" class="highlighted-word">a</span><span style="color:#999999">)</span></span>
<span class="line"></span></code></pre>
6 changes: 5 additions & 1 deletion packages/rehype-shikiji/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import { expect, it } from 'vitest'
import { transformerMetaHighlight, transformerMetaWordHighlight } from 'shikiji-transformers'
import rehypeShikiji from '../src'

it('run', async () => {
Expand All @@ -12,7 +13,10 @@ it('run', async () => {
.use(remarkRehype)
.use(rehypeShikiji, {
theme: 'vitesse-light',
highlightLines: true,
transformers: [
transformerMetaWordHighlight(),
transformerMetaHighlight(),
],
parseMetaString: (str) => {
return Object.fromEntries(str.split(' ').reduce((prev: [string, boolean | string][], curr: string) => {
const [key, value] = curr.split('=')
Expand Down
2 changes: 2 additions & 0 deletions packages/shikiji-transformers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from './transformers/remove-line-breaks'
export * from './transformers/compact-line-options'
export * from './transformers/notation-focus'
export * from './transformers/notation-highlight'
export * from './transformers/notation-highlight-word'
export * from './transformers/transformer-meta-highlight-word'
export * from './transformers/notation-diff'
export * from './transformers/notation-error-level'
export * from './transformers/transformer-meta-highlight'
Expand Down
56 changes: 56 additions & 0 deletions packages/shikiji-transformers/src/shared/highlight-word.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Element } from 'hast'
import { addClassToHast } from 'shikiji/core'

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 textNode = span.children[0]

if (textNode.type !== 'text')
return span

return replaceSpan(span, textNode.value, word, className) ?? span
})
}

function inheritElement(original: Element, overrides: Partial<Element>): Element {
return {
...original,
properties: {
...original.properties,
},
...overrides,
}
}

function replaceSpan(span: Element, text: string, word: string, className: string): Element[] | undefined {
const index = text.indexOf(word)

if (index === -1)
return

const createNode = (value: string) => inheritElement(span, {
children: [
{
type: 'text',
value,
},
],
})

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 nodes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type ShikijiTransformer, addClassToHast } from 'shikiji'
import { createCommentNotationTransformer } from '../utils'
import { highlightWordInLine } from '../shared/highlight-word'

export interface TransformerNotationWordHighlightOptions {
/**
* Class for highlighted words
*/
classActiveWord?: string
/**
* Class added to the root element when the code has highlighted words
*/
classActivePre?: string
}

export function transformerNotationWordHighlight(
options: TransformerNotationWordHighlightOptions = {},
): ShikijiTransformer {
const {
classActiveWord = 'highlighted-word',
classActivePre = undefined,
} = options

return createCommentNotationTransformer(
'shikiji-transformers:notation-highlight-word',
/^\s*(?:\/\/|\/\*|<!--|#)\s+\[!code word:(\w+)(:\d+)?\]\s*(?:\*\/|-->)?/,
function ([_, word, range], _line, comment, lines, index) {
const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length

lines
// Don't include the comment itself
.slice(index + 1, index + 1 + lineNum)
.forEach(line => highlightWordInLine(line, comment, word, classActiveWord))

if (classActivePre)
addClassToHast(this.pre, classActivePre)
return true
},
true, // remove empty lines
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ShikijiTransformer } from 'shikiji'
import { addClassToHast } from 'shikiji'
import type { Element } from 'hast'
import { splitSpaces } from './_utils'
import { splitSpaces } from '../shared/utils'

export interface TransformerRenderWhitespaceOptions {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ShikijiTransformer } from 'shikiji'
import { highlightWordInLine } from '../shared/highlight-word'

export function parseMetaHighlightWords(meta: string): string[] {
if (!meta)
return []

const match = Array.from(meta.matchAll(/\/(\w+)\//g))

return match.map(v => v[1])
}

export interface TransformerMetaWordHighlightOptions {
/**
* Class for highlighted words
*
* @default 'highlighted-word'
*/
className?: string
}

/**
* Allow using `/word/` in the code snippet meta to mark highlighted words.
*/
export function transformerMetaWordHighlight(
options: TransformerMetaWordHighlightOptions = {},
): ShikijiTransformer {
const {
className = 'highlighted-word',
} = options

return {
name: 'shikiji-transformers:meta-word-highlight',
line(node) {
if (!this.options.meta?.__raw)
return

const words = parseMetaHighlightWords(this.options.meta.__raw)

for (const word of words)
highlightWordInLine(node, null, word, className)

return node
},
}
}
Loading

0 comments on commit ecf60cd

Please sign in to comment.