Skip to content

Commit

Permalink
feat: add web component renderer (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aziteee authored Dec 8, 2024
1 parent b6c7218 commit b9f9d7c
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 1 deletion.
1 change: 1 addition & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default defineBuildConfig({
'src/core',
'src/types',
'src/solid',
'src/web-component',
'src/renderer',
{
builder: 'mkdist',
Expand Down
7 changes: 7 additions & 0 deletions playground/src/Playground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createRendererReact } from './renderer/react'
import { createRendererSolid } from './renderer/solid'
import { createRendererSvelte } from './renderer/svelte.svelte'
import { createRendererVue } from './renderer/vue'
import { createRendererWebComponent } from './renderer/web-component'
const defaultOptions = {
theme: 'vitesse-dark',
Expand Down Expand Up @@ -108,6 +109,9 @@ function rendererUpdate() {
case 'svelte':
renderer = createRendererSvelte(rendererOptions)
break
case 'web-component':
renderer = createRendererWebComponent(rendererOptions)
break
}
renderer.mount(rendererContainer.value, payload)
Expand Down Expand Up @@ -324,6 +328,9 @@ watch(
<option value="svelte">
Svelte Renderer
</option>
<option value="web-component">
Web Component Renderer
</option>
</select>
</div>
<div ref="rendererContainer" class="of-auto" />
Expand Down
2 changes: 1 addition & 1 deletion playground/src/renderer/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { HighlighterCore } from 'shiki/core'
import type { MagicMoveDifferOptions, MagicMoveRenderOptions } from '../../../src/core'

export type RendererType = 'vue' | 'react' | 'svelte' | 'solid'
export type RendererType = 'vue' | 'react' | 'svelte' | 'solid' | 'web-component'

export interface RendererUpdatePayload {
highlighter: HighlighterCore
Expand Down
43 changes: 43 additions & 0 deletions playground/src/renderer/web-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ShikiMagicMove } from '../../../src/web-component/ShikiMagicMove'
import type { RendererFactory, RendererFactoryResult, RendererUpdatePayload } from './types'

import '../../../src/web-component/ShikiMagicMove'

export const createRendererWebComponent: RendererFactory = (options): RendererFactoryResult => {
let app: ShikiMagicMove | undefined

return {
mount: async (element, payload) => {
app = document.createElement('shiki-magic-move')

app.addEventListener('onStart', options.onStart ?? (() => {}))
app.addEventListener('onEnd', options.onEnd ?? (() => {}))

Object.keys(payload).forEach((prop) => {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore
app[prop as keyof RendererUpdatePayload] = payload[prop as keyof RendererUpdatePayload]
})

element.appendChild(app)

// eslint-disable-next-line no-console
console.log('Web Component renderer mounted')
},

update: (payload) => {
Object.keys(payload).forEach((prop) => {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore
app[prop as keyof RendererUpdatePayload] = payload[prop as keyof RendererUpdatePayload]
})
},

dispose: () => {
if (!app)
return
app.remove()
app = undefined
},
}
}
1 change: 1 addition & 0 deletions src/web-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './web-component/index'
147 changes: 147 additions & 0 deletions src/web-component/ShikiMagicMove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { HighlighterCore } from 'shiki/core'
import type { KeyedTokensInfo, MagicMoveDifferOptions, MagicMoveRenderOptions } from '../types'
import type { ShikiMagicMoveRenderer } from './ShikiMagicMoveRenderer'
import { codeToKeyedTokens, createMagicMoveMachine } from '../core'

import './ShikiMagicMoveRenderer'

export class ShikiMagicMove extends HTMLElement {
private _highlighter!: HighlighterCore
private _lang!: string
private _theme!: string
private _code!: string
private _class?: string
private _options?: MagicMoveRenderOptions & MagicMoveDifferOptions

get highlighter() {
return this._highlighter
}

set highlighter(value: HighlighterCore) {
this._highlighter = value
this.propertyChangedCallback()
}

get lang() {
return this._lang
}

set lang(value: string) {
this._lang = value
this.propertyChangedCallback()
}

get theme() {
return this._theme
}

set theme(value: string) {
this._theme = value
this.propertyChangedCallback()
}

get code() {
return this._code
}

set code(value: string) {
this._code = value
this.propertyChangedCallback()
}

get class() {
return this._class
}

set class(value: string | undefined) {
this._class = value
this.propertyChangedCallback()
}

get options() {
return this._options
}

set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions | undefined) {
this._options = value
this.propertyChangedCallback()
}

private machine?: {
commit: (code: string, override?: MagicMoveDifferOptions) => {
current: KeyedTokensInfo
previous: KeyedTokensInfo
}
}

private renderer?: ShikiMagicMoveRenderer

private result?: { current: KeyedTokensInfo, previous: KeyedTokensInfo }

private batchUpdate = false

private hasUpdated = false

constructor() {
super()
}

connectedCallback() {
this.renderer = document.createElement('shiki-magic-move-renderer') as ShikiMagicMoveRenderer
this.renderer.addEventListener('onStart', () => this.dispatchEvent(new CustomEvent('onStart')))
this.renderer.addEventListener('onEnd', () => this.dispatchEvent(new CustomEvent('onEnd')))

this.machine = createMagicMoveMachine(
code => codeToKeyedTokens(
this.highlighter,
code,
{
lang: this.lang,
theme: this.theme,
},
this.options?.lineNumbers,
),
this.options,
)

this.updateRenderer()

this.appendChild(this.renderer)
}

propertyChangedCallback() {
if (!this.batchUpdate) {
this.batchUpdate = true

setTimeout(() => {
this.batchUpdate = false

if (!this.hasUpdated) {
this.hasUpdated = true
return
}

this.updateRenderer()
}, 0)
}
}

updateRenderer() {
if (this.machine && this.renderer) {
this.result = this.machine.commit(this.code, this.options)

this.renderer.tokens = this.result!.current
this.renderer.previous = this.result!.previous
this.renderer.options = this.options
this.renderer.class = this.class ?? ''
}
}
}

customElements.define('shiki-magic-move', ShikiMagicMove)

declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move': ShikiMagicMove
}
}
108 changes: 108 additions & 0 deletions src/web-component/ShikiMagicMovePrecompiled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import type { KeyedTokensInfo, MagicMoveDifferOptions, MagicMoveRenderOptions } from '../types'
import type { ShikiMagicMoveRenderer } from './ShikiMagicMoveRenderer'

import { syncTokenKeys, toKeyedTokens } from '../core'

export class ShikiMagicMovePrecompiled extends HTMLElement {
private _steps: KeyedTokensInfo[] = []
private _step: number = 0
private _animated: boolean = true
private _options?: MagicMoveRenderOptions & MagicMoveDifferOptions

get steps() {
return this._steps
}

set steps(value: KeyedTokensInfo[]) {
this._steps = value
this.propertyChangedCallback()
}

get step() {
return this._step
}

set step(value: number) {
this._step = value
this.propertyChangedCallback()
}

get animated() {
return this._animated
}

set animated(value: boolean) {
this._animated = value
this.propertyChangedCallback()
}

get options(): MagicMoveRenderOptions & MagicMoveDifferOptions | undefined {
return this._options
}

set options(value: MagicMoveRenderOptions & MagicMoveDifferOptions) {
this._options = value
this.propertyChangedCallback()
}

private renderer?: ShikiMagicMoveRenderer

private previous: KeyedTokensInfo = toKeyedTokens('', [])

private batchUpdate = false

private hasUpdated = false

constructor() {
super()
}

connectedCallback() {
this.renderer = document.createElement('shiki-magic-move-renderer') as ShikiMagicMoveRenderer
this.renderer.addEventListener('onStart', () => this.dispatchEvent(new CustomEvent('onStart')))
this.renderer.addEventListener('onEnd', () => this.dispatchEvent(new CustomEvent('onEnd')))

this.updateRenderer()

this.appendChild(this.renderer)
}

propertyChangedCallback() {
if (!this.batchUpdate) {
this.batchUpdate = true

setTimeout(() => {
this.batchUpdate = false

if (!this.hasUpdated) {
this.hasUpdated = true
return
}

this.updateRenderer()
}, 0)
}
}

updateRenderer() {
const result = syncTokenKeys(
this.previous,
this.steps[Math.min(this.step, this.steps.length - 1)],
this.options,
)
this.previous = result.to

this.renderer!.tokens = result.to
this.renderer!.previous = result.from
this.renderer!.options = this.options
this.renderer!.animated = this.animated
}
}

customElements.define('shiki-magic-move-precompiled', ShikiMagicMovePrecompiled)

declare global {
interface HTMLElementTagNameMap {
'shiki-magic-move-precompiled': ShikiMagicMovePrecompiled
}
}
Loading

0 comments on commit b9f9d7c

Please sign in to comment.