Skip to content

Commit 8e2b543

Browse files
committed
feat: code block components
1 parent 0b07583 commit 8e2b543

File tree

9 files changed

+138
-6
lines changed

9 files changed

+138
-6
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"@vue-email/cli": "latest",
7878
"@vue-email/tailwind": "^0.0.6",
7979
"isomorphic-dompurify": "^1.12.0",
80+
"shikiji": "0.10.0-beta.9",
8081
"ufo": "^1.3.2",
8182
"vue": "^3.3.8",
8283
"vue-i18n": "^9.8.0"

playground/src/App.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { onMounted, ref } from 'vue'
3-
import { useRender } from 'vue-email'
3+
import { ECodeBlock, useRender } from 'vue-email'
44
import Test from './components/Test.vue'
55
66
const email = ref('')
@@ -11,9 +11,20 @@ onMounted(async () => {
1111
email.value = res.html
1212
})
1313
})
14+
15+
const code = `import { codeToThemedTokens } from 'shikiji'
16+
17+
const tokens = await codeToThemedTokens('<div class="foo">bar</div>', {
18+
lang: 'html',
19+
theme: 'min-dark'
20+
})
21+
`
1422
</script>
1523

1624
<template>
25+
<Suspense>
26+
<ECodeBlock style="padding: 20px;" :code="code" lang="typescript" theme="dracula" />
27+
</Suspense>
1728
<iframe :srcdoc="email" />
1829
</template>
1930

playground/src/components/Test.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { EBody, EButton, EColumn, EContainer, EHead, EHeading, EHr, EHtml, EImg, ELink, EPreview, ERow, ESection, EStyle, ETailwind, EText } from 'vue-email'
2+
import { EBody, EButton, ECodeBlock, EColumn, EContainer, EHead, EHeading, EHr, EHtml, EImg, ELink, EPreview, ERow, ESection, EStyle, ETailwind, EText } from 'vue-email'
33
44
interface Props {
55
invitedByUsername?: string
@@ -21,6 +21,13 @@ const props = withDefaults(defineProps<Props>(), {
2121
})
2222
2323
const previewText = `Join ${props.invitedByUsername} on Vercel`
24+
25+
const code = `import { codeToThemedTokens } from 'shikiji'
26+
const tokens = await codeToThemedTokens('<div class="foo">bar</div>', {
27+
lang: 'html',
28+
theme: 'min-dark'
29+
})
30+
`
2431
</script>
2532

2633
<template>
@@ -54,6 +61,7 @@ const previewText = `Join ${props.invitedByUsername} on Vercel`
5461
</ELink>
5562
) has invited you to the <strong>{{ teamName }}</strong> team on <strong>Vercel</strong>.
5663
</EText>
64+
<ECodeBlock class="p-5" :code="code" lang="typescript" theme="min-dark" />
5765
<ESection>
5866
<ERow>
5967
<EColumn align="right">

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ECodeBlock.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { PropType } from 'vue'
2+
import { defineComponent, h } from 'vue'
3+
import type { BundledLanguage, BundledTheme, SpecialLanguage, ThemeRegistrationAny } from 'shikiji'
4+
import { getHighlighter } from 'shikiji'
5+
6+
export default defineComponent({
7+
name: 'ECodeBlock',
8+
props: {
9+
code: {
10+
type: String,
11+
required: true,
12+
},
13+
lang: {
14+
type: String as PropType<BundledLanguage | SpecialLanguage>,
15+
required: true,
16+
},
17+
theme: {
18+
type: String as PropType<BundledTheme | ThemeRegistrationAny>,
19+
required: true,
20+
},
21+
class: {
22+
type: String,
23+
default: '',
24+
},
25+
},
26+
async setup({ code, lang, theme }) {
27+
const shiki = await getHighlighter({
28+
langs: [lang],
29+
themes: [theme],
30+
})
31+
32+
const themeColorBg = shiki.getTheme(theme).bg
33+
const htmlCode = shiki.codeToThemedTokens(code, {
34+
lang,
35+
theme,
36+
})
37+
38+
return () =>
39+
h('pre', {
40+
class: ['shiki', theme],
41+
style: {
42+
backgroundColor: themeColorBg,
43+
display: 'block',
44+
whiteSpace: 'pre',
45+
fontFamily: 'monospace',
46+
},
47+
tabindex: 0,
48+
}, [
49+
h('code', null, [
50+
...htmlCode.map((line) => {
51+
return h('span', { class: ['line'], style: {
52+
display: 'table-row',
53+
lineHeight: '1.5',
54+
height: '1.5em',
55+
} }, [
56+
...line.map((token) => {
57+
return h('span', { style: { color: token.color } }, token.content)
58+
}),
59+
])
60+
}),
61+
]),
62+
])
63+
64+
// const htmlCode = await codeToHtml(code, {
65+
// lang,
66+
// theme,
67+
// })
68+
69+
// return () => h('clean-component', {
70+
// innerHTML: htmlCode,
71+
// })
72+
},
73+
})

src/components/ETailwind.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export default defineComponent({
120120
})
121121

122122
return () => {
123-
return h('tailwind-clean', { innerHTML: html })
123+
return h('clean-component', { innerHTML: html })
124124
}
125125
},
126126
})

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export { default as ETailwind } from './ETailwind'
1616
export { default as EText } from './EText'
1717
export { default as EMarkdown } from './EMarkdown'
1818
export { default as EStyle } from './EStyle'
19+
export { default as ECodeBlock } from './ECodeBlock'

src/utils/cleanup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default function cleanup(str: string) {
99
.replace(/<template>/g, '')
1010
.replace(/<template[^>]*>/g, '')
1111
.replace(/<\/template>/g, '')
12-
.replace(/<tailwind-clean>/g, '')
13-
.replace(/<tailwind-clean[^>]*>/g, '')
14-
.replace(/<\/tailwind-clean>/g, '')
12+
.replace(/<clean-component>/g, '')
13+
.replace(/<clean-component[^>]*>/g, '')
14+
.replace(/<\/clean-component>/g, '')
1515
}

tests/CodeBlock.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it } from 'vitest'
2+
3+
describe('render', () => {
4+
it('renders the <Column> component', async () => {
5+
// const component = h(ECodeBlock, {
6+
// code: `import { codeToThemedTokens } from 'shikiji'
7+
8+
// const tokens = await codeToThemedTokens('<div class="foo">bar</div>', {
9+
// lang: 'html',
10+
// theme: 'min-dark'
11+
// })`,
12+
// lang: 'typescript',
13+
// theme: 'dracula',
14+
// })
15+
16+
// const actualOutput = await useRender(component)
17+
18+
// expect(actualOutput.html).toMatchInlineSnapshot(
19+
// `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><pre class="shiki dracula" style="background-color:#282A36;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#FF79C6">export</span><span style="color:#FF79C6"> default</span><span style="color:#FF79C6"> async</span><span style="color:#F8F8F2"> (</span><span style="color:#FFB86C;font-style:italic">req</span><span style="color:#F8F8F2">,
20+
// </span><span style="color:#FFB86C;font-style:italic">res</span><span style="color:#F8F8F2">) </span><span style="color:#FF79C6">=></span><span style="color:#F8F8F2"> {</span></span>
21+
// <span class="line"><span style="color:#FF79C6"> try</span><span style="color:#F8F8F2"> {</span></span>
22+
// <span class="line"><span style="color:#FF79C6"> const</span><span style="color:#F8F8F2"> html </span><span style="color:#FF79C6">=</span><span style="color:#FF79C6"> await</span><span style="color:#50FA7B"> renderAsync</span><span style="color:#F8F8F2">(</span></span><span class="line"><span style="color:#50FA7B"> EmailTemplate</span><span style="color:#F8F8F2">({ firstName</span><span style="color:#FF79C6">:</span><span style="color:#E9F284"> '</span><span style="color:#F1FA8C">John</span><span style="color:#E9F284">'</span><span style="color:#F8F8F2"> })</span></span><span class="line"><span style="color:#F8F8F2"> );</span></span><span class="line"><span style="color:#FF79C6"> return</span><span style="color:#F8F8F2"> NextResponse.</span><span style="color:#50FA7B">json</span><span style="color:#F8F8F2">({ html });</span></span><span class="line"><span style="color:#F8F8F2"> } </span><span style="color:#FF79C6">catch</span><span style="color:#F8F8F2"> (error) {</span></span><span class="line"><span style="color:#FF79C6"> return</span><span style="color:#F8F8F2"> NextResponse.</span><span style="color:#50FA7B">json</span><span style="color:#F8F8F2">({ error });</span></span><span class="line"><span style="color:#F8F8F2"> }</span></span><span class="line"><span style="color:#F8F8F2"> }</span></span></code></pre>`,
23+
// )
24+
})
25+
})

0 commit comments

Comments
 (0)