= {},
): VNode {
- content = content.trim().replace(/:([\w-]+?):/g, (_, name) => {
- const emoji = customEmojis[name]
- if (emoji)
- return ``
- return `:${name}:`
- })
+ content = content
+ .trim()
+ // handle custom emojis
+ .replace(/:([\w-]+?):/g, (_, name) => {
+ const emoji = customEmojis[name]
+ if (emoji)
+ return ``
+ return `:${name}:`
+ })
+ // handle codeblocks
+ .replace(/(```|~~~)([\s\S]+?)\1(\s|
)*<\/p>/g, (_1, _2, raw) => {
+ const plain = htmlToText(`
${raw}
`).trim()
+ const [lang, ...rest] = plain.split(/\n/)
+ return ``
+ })
+
const tree = parseFragment(content)
- return h(Fragment, tree.childNodes.map(n => treeToVNode(n, handle)))
+ return h(Fragment, tree.childNodes.map(n => treeToVNode(n)))
}
export function treeToVNode(
input: Node,
- handle: (node: Element) => Element | undefined | null | void = defaultHandle,
): VNode | string | null {
if (input.nodeName === '#text')
// @ts-expect-error casing
return input.value
if ('childNodes' in input) {
- const node = handle(input)
+ const node = handleNode(input)
if (node == null)
return null
+ if (isVNode(node))
+ return node
const attrs = Object.fromEntries(node.attrs.map(i => [i.name, i.value]))
if (node.nodeName === 'a' && (attrs.href?.startsWith('/') || attrs.href?.startsWith('.'))) {
@@ -65,14 +92,38 @@ export function treeToVNode(
return h(
RouterLink as any,
attrs,
- () => node.childNodes.map(n => treeToVNode(n, handle)),
+ () => node.childNodes.map(treeToVNode),
)
}
return h(
node.nodeName,
attrs,
- node.childNodes.map(n => treeToVNode(n, handle)),
+ node.childNodes.map(treeToVNode),
)
}
return null
}
+
+function htmlToText(html: string) {
+ const tree = parseFragment(html)
+ return tree.childNodes.map(n => treeToText(n)).join('')
+}
+
+function treeToText(input: Node): string {
+ let pre = ''
+
+ if (input.nodeName === '#text')
+ // @ts-expect-error casing
+ return input.value
+
+ if (input.nodeName === 'br')
+ return '\n'
+
+ if (input.nodeName === 'p')
+ pre = '\n'
+
+ if ('childNodes' in input)
+ return pre + input.childNodes.map(n => treeToText(n)).join('')
+
+ return pre
+}
diff --git a/composables/shiki.ts b/composables/shiki.ts
new file mode 100644
index 00000000..975f84ca
--- /dev/null
+++ b/composables/shiki.ts
@@ -0,0 +1,37 @@
+import type { Highlighter, Lang } from 'shiki'
+
+export const shiki = ref()
+
+const registeredLang = ref(new Map())
+let shikiImport: Promise | undefined
+
+export function highlightCode(code: string, lang: Lang) {
+ if (!shikiImport) {
+ shikiImport = import('shiki')
+ .then(async (r) => {
+ r.setCDN('/shiki/')
+ shiki.value = await r.getHighlighter({
+ themes: [
+ 'vitesse-dark',
+ 'vitesse-light',
+ ],
+ })
+ })
+ }
+
+ if (!shiki.value)
+ return code
+
+ if (!registeredLang.value.get(lang)) {
+ shiki.value.loadLanguage(lang)
+ .then(() => {
+ registeredLang.value.set(lang, true)
+ })
+ return code
+ }
+
+ return shiki.value.codeToHtml(code, {
+ lang,
+ theme: isDark.value ? 'vitesse-dark' : 'vitesse-light',
+ })
+}
diff --git a/composables/utils.ts b/composables/utils.ts
index a2eaf2a0..f29426ec 100644
--- a/composables/utils.ts
+++ b/composables/utils.ts
@@ -23,3 +23,4 @@ export function getDataUrlFromArr(arr: Uint8ClampedArray, w: number, h: number)
export function emojisArrayToObject(emojis: Emoji[]) {
return Object.fromEntries(emojis.map(i => [i.shortcode, i]))
}
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index d999c021..6ca8185a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -20,12 +20,12 @@ export default defineNuxtConfig({
},
vite: {
define: {
- __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
+ '__BUILD_TIME__': JSON.stringify(new Date().toISOString()),
+ 'process.env.VSCODE_TEXTMATE_DEBUG': 'false',
},
build: {
target: 'esnext',
},
-
},
postcss: {
plugins: {
diff --git a/package.json b/package.json
index c8093823..032d0317 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"start": "node .output/server/index.mjs",
"lint": "eslint .",
"postinstall": "nuxi prepare",
+ "prepare": "esno scripts/prepare.ts",
"generate": "nuxi generate"
},
"devDependencies": {
@@ -18,6 +19,7 @@
"@iconify-json/twemoji": "^1.1.5",
"@pinia/nuxt": "^0.4.3",
"@types/fs-extra": "^9.0.13",
+ "@types/js-yaml": "^4.0.5",
"@types/sanitize-html": "^2.6.2",
"@types/wicg-file-system-access": "^2020.9.5",
"@unocss/nuxt": "^0.46.5",
@@ -28,6 +30,7 @@
"esno": "^0.16.3",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
+ "js-yaml": "^4.1.0",
"masto": "^4.6.6",
"nuxt": "^3.0.0",
"parse5": "^7.1.1",
@@ -35,6 +38,8 @@
"postcss-nested": "^6.0.0",
"rollup-plugin-node-polyfills": "^0.2.1",
"sanitize-html": "^2.7.3",
+ "shiki": "^0.11.1",
+ "theme-vitesse": "^0.6.0",
"typescript": "^4.9.3",
"ufo": "^1.0.0"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9083aff2..2851da24 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,5 +1,8 @@
lockfileVersion: 5.4
+overrides:
+ debug: 4.3.4
+
specifiers:
'@antfu/eslint-config': ^0.30.1
'@iconify-json/carbon': ^1.1.10
@@ -8,6 +11,7 @@ specifiers:
'@iconify-json/twemoji': ^1.1.5
'@pinia/nuxt': ^0.4.3
'@types/fs-extra': ^9.0.13
+ '@types/js-yaml': ^4.0.5
'@types/sanitize-html': ^2.6.2
'@types/wicg-file-system-access': ^2020.9.5
'@unocss/nuxt': ^0.46.5
@@ -18,6 +22,7 @@ specifiers:
esno: ^0.16.3
form-data: ^4.0.0
fs-extra: ^10.1.0
+ js-yaml: ^4.1.0
masto: ^4.6.6
nuxt: ^3.0.0
parse5: ^7.1.1
@@ -25,6 +30,8 @@ specifiers:
postcss-nested: ^6.0.0
rollup-plugin-node-polyfills: ^0.2.1
sanitize-html: ^2.7.3
+ shiki: ^0.11.1
+ theme-vitesse: ^0.6.0
typescript: ^4.9.3
ufo: ^1.0.0
@@ -36,6 +43,7 @@ devDependencies:
'@iconify-json/twemoji': 1.1.5
'@pinia/nuxt': 0.4.3_typescript@4.9.3
'@types/fs-extra': 9.0.13
+ '@types/js-yaml': 4.0.5
'@types/sanitize-html': 2.6.2
'@types/wicg-file-system-access': 2020.9.5
'@unocss/nuxt': 0.46.5
@@ -46,6 +54,7 @@ devDependencies:
esno: 0.16.3
form-data: 4.0.0
fs-extra: 10.1.0
+ js-yaml: 4.1.0
masto: 4.6.6
nuxt: 3.0.0_e3uo4sehh4zr4i6m57mkkxxv7y
parse5: 7.1.1
@@ -53,6 +62,8 @@ devDependencies:
postcss-nested: 6.0.0
rollup-plugin-node-polyfills: 0.2.1
sanitize-html: 2.7.3
+ shiki: 0.11.1
+ theme-vitesse: 0.6.0
typescript: 4.9.3
ufo: 1.0.0
@@ -1255,6 +1266,10 @@ packages:
'@types/node': 18.7.23
dev: true
+ /@types/js-yaml/4.0.5:
+ resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==}
+ dev: true
+
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
@@ -2883,28 +2898,6 @@ packages:
engines: {node: '>= 12'}
dev: true
- /debug/2.6.9:
- resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
- dependencies:
- ms: 2.0.0
- dev: true
-
- /debug/3.2.7:
- resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
- peerDependencies:
- supports-color: '*'
- peerDependenciesMeta:
- supports-color:
- optional: true
- dependencies:
- ms: 2.1.3
- dev: true
-
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@@ -3432,7 +3425,7 @@ packages:
/eslint-import-resolver-node/0.3.6:
resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==}
dependencies:
- debug: 3.2.7
+ debug: 4.3.4
resolve: 1.22.1
transitivePeerDependencies:
- supports-color
@@ -3460,7 +3453,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 5.42.1_e3uo4sehh4zr4i6m57mkkxxv7y
- debug: 3.2.7
+ debug: 4.3.4
eslint: 8.27.0
eslint-import-resolver-node: 0.3.6
transitivePeerDependencies:
@@ -3518,7 +3511,7 @@ packages:
'@typescript-eslint/parser': 5.42.1_e3uo4sehh4zr4i6m57mkkxxv7y
array-includes: 3.1.5
array.prototype.flat: 1.3.0
- debug: 2.6.9
+ debug: 4.3.4
doctrine: 2.1.0
eslint: 8.27.0
eslint-import-resolver-node: 0.3.6
@@ -5160,10 +5153,6 @@ packages:
engines: {node: '>=10'}
dev: true
- /ms/2.0.0:
- resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
- dev: true
-
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -6497,7 +6486,7 @@ packages:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
engines: {node: '>= 0.8.0'}
dependencies:
- debug: 2.6.9
+ debug: 4.3.4
depd: 2.0.0
destroy: 1.2.0
encodeurl: 1.0.2
@@ -6566,6 +6555,14 @@ packages:
engines: {node: '>=8'}
dev: true
+ /shiki/0.11.1:
+ resolution: {integrity: sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA==}
+ dependencies:
+ jsonc-parser: 3.2.0
+ vscode-oniguruma: 1.6.2
+ vscode-textmate: 6.0.0
+ dev: true
+
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@@ -6882,6 +6879,11 @@ packages:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
dev: true
+ /theme-vitesse/0.6.0:
+ resolution: {integrity: sha512-/XEZFGXLTK/AlWSe9t+NIXB1tP3yqdzugcSJJ2Fg0KYM1PcoL/zWs5AuaEcCFt1pfi/9Og++tzOdiU2aKf/+Xw==}
+ engines: {vscode: ^1.43.0}
+ dev: true
+
/through/2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
@@ -7502,6 +7504,14 @@ packages:
vscode-languageserver-protocol: 3.16.0
dev: true
+ /vscode-oniguruma/1.6.2:
+ resolution: {integrity: sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==}
+ dev: true
+
+ /vscode-textmate/6.0.0:
+ resolution: {integrity: sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ==}
+ dev: true
+
/vscode-uri/3.0.6:
resolution: {integrity: sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ==}
dev: true
diff --git a/scripts/prepare.ts b/scripts/prepare.ts
new file mode 100644
index 00000000..f7a2fe7d
--- /dev/null
+++ b/scripts/prepare.ts
@@ -0,0 +1,10 @@
+import { copy } from 'fs-extra'
+
+const dereference = process.platform === 'win32' ? true : undefined
+
+await copy('node_modules/shiki/', 'public/shiki/', {
+ dereference,
+ filter: src => src === 'node_modules/shiki/' || src.includes('languages') || src.includes('dist'),
+})
+await copy('node_modules/theme-vitesse/themes', 'public/shiki/themes', { dereference })
+await copy('node_modules/theme-vitesse/themes', 'node_modules/shiki/themes', { overwrite: true, dereference })
diff --git a/styles/global.css b/styles/global.css
index 7441eb18..79dba27d 100644
--- a/styles/global.css
+++ b/styles/global.css
@@ -1,5 +1,5 @@
* {
- scrollbar-color: #5555 var(--c-border);
+ scrollbar-color: #8885 var(--c-border);
}
::-webkit-scrollbar {
@@ -16,12 +16,12 @@
}
::-webkit-scrollbar-thumb {
- background: #555;
+ background: #8885;
border-radius: 1px;
}
::-webkit-scrollbar-thumb:hover {
- background: #666;
+ background: #8886;
}
/* Force vertical scrollbar to be always visible to avoid layout shift while loading the content */
@@ -52,4 +52,12 @@ html {
p {
--at-apply: my-2;
}
+
+ .code-block {
+ --at-apply: bg-code text-0.9rem p3 rounded overflow-auto leading-1.6em;
+
+ .shiki {
+ background: transparent !important;
+ }
+ }
}
diff --git a/styles/vars.css b/styles/vars.css
index e38e7a1a..57644227 100644
--- a/styles/vars.css
+++ b/styles/vars.css
@@ -4,6 +4,7 @@
--c-border: #88888820;
--c-bg-base: #fff;
--c-bg-active: #f6f6f6;
+ --c-bg-code: #00000006;
--c-text-base: #222;
--c-text-secondary: #888;
}
@@ -11,5 +12,6 @@
.dark {
--c-bg-base: #111;
--c-bg-active: #151515;
+ --c-bg-code: #ffffff06;
--c-text-base: #fff;
}
diff --git a/unocss.config.ts b/unocss.config.ts
index 6faa57b0..5a3b742e 100644
--- a/unocss.config.ts
+++ b/unocss.config.ts
@@ -15,6 +15,7 @@ export default defineConfig({
'border-base': 'border-$c-border',
'bg-base': 'bg-$c-bg-base',
'bg-active': 'bg-$c-bg-active',
+ 'bg-code': 'bg-$c-bg-code',
'text-base': 'text-$c-text-base',
'text-secondary': 'text-$c-text-secondary',
'interact-disabled': 'disabled:opacity-50 disabled:pointer-events-none disabled:saturate-0',