Merge pull request #42 from pengzhanbo/RC-23

feat: (试验性)代码高亮支持 twoslash
This commit is contained in:
pengzhanbo 2024-01-12 02:38:03 +08:00 committed by GitHub
commit a83e665cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 666 additions and 1003 deletions

View File

@ -72,6 +72,7 @@
"taze",
"Tongji",
"tsbuildinfo",
"twoslash",
"vite",
"vuepress",
"vueuse",

View File

@ -124,6 +124,56 @@ const obj = {
}
```
**Code Blocks TwoSlash**
```ts twoslash
// @errors: 2339
const welcome = "Tudo bem gente?"
const words = welcome.contains(" ")
```
```ts twoslash
import express from "express"
const app = express()
app.get("/", function (req, res) {
res.send
})
app.listen(3000)
```
```ts twoslash
import { getHighlighterCore } from 'shikiji/core'
const highlighter = await getHighlighterCore({})
// @log: Custom log message
const a = 1
// @error: Custom error message
const b = 1
// @warn: Custom warning message
const c = 1
// @annotate: Custom annotation message
```
```ts twoslash
// @errors: 2540
interface Todo {
title: string
}
const todo: Readonly<Todo> = {
title: 'Delete inactive users'.toUpperCase(),
// ^?
}
todo.title = 'Hello'
Number.parseInt('123', 10)
// ^|
//
//
```
**代码分组**
::: code-tabs

View File

@ -17,7 +17,7 @@
"anywhere": "^1.6.0",
"sass": "^1.69.7",
"sass-loader": "^13.3.3",
"vue": "^3.4.7",
"vue": "^3.4.10",
"vuepress-theme-plume": "workspace:*"
}
}

View File

@ -60,6 +60,11 @@
"typescript": "^5.3.3",
"vite": "^5.0.11"
},
"pnpm": {
"patchedDependencies": {
"@vuepress/markdown@2.0.0-rc.0": "patches/@vuepress__markdown@2.0.0-rc.0.patch"
}
},
"lint-staged": {
"*": "eslint --fix"
},

View File

@ -0,0 +1,13 @@
diff --git a/dist/index.js b/dist/index.js
index 996b0d16dac39667cc25496e52adcc9dd2b2befa..a4c9f5ba3a20967d9a561fcc73178d9e84f48279 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -245,7 +245,7 @@ var codePlugin = (md, {
const info = token.info ? md.utils.unescapeAll(token.info).trim() : "";
const language = resolveLanguage(info);
const languageClass = `${options.langPrefix}${language.name}`;
- const code = options.highlight?.(token.content, language.name, "") || md.utils.escapeHtml(token.content);
+ const code = options.highlight?.(token.content, language.name, info || "") || md.utils.escapeHtml(token.content);
token.attrJoin("class", languageClass);
let result = code.startsWith("<pre") ? code : `<pre${slf.renderAttrs(token)}><code>${code}</code></pre>`;
const useVPre = resolveVPre(info) ?? vPreBlock;

View File

@ -42,7 +42,7 @@
"@vuepress/utils": "2.0.0-rc.0",
"chokidar": "^3.5.3",
"create-filter": "^1.0.1",
"vue": "^3.4.7"
"vue": "^3.4.10"
},
"publishConfig": {
"access": "public"

View File

@ -39,7 +39,7 @@
"@vuepress/client": "2.0.0-rc.0",
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.7",
"vue": "^3.4.10",
"vue-router": "4.2.5"
},
"publishConfig": {

View File

@ -41,7 +41,7 @@
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.7"
"vue": "^3.4.10"
},
"publishConfig": {
"access": "public"

View File

@ -8,7 +8,7 @@ const options = __COPY_CODE_OPTIONS__
const RE_LANGUAGE = /language-([\w]+)/
const RE_START_CODE = /^ *(\$|>)/gm
const shells = ['shellscript', 'shell', 'bash', 'sh', 'zsh']
const ignoredNodes = ['.diff.remove']
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
function isMobile(): boolean {
return navigator

View File

@ -41,7 +41,7 @@
"@vuepress/core": "2.0.0-rc.0",
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"vue": "^3.4.7"
"vue": "^3.4.10"
},
"publishConfig": {
"access": "public"

View File

@ -51,11 +51,11 @@
"dotenv": "^16.3.1",
"esbuild": "^0.19.11",
"execa": "^8.0.1",
"netlify-cli": "^17.13.0",
"netlify-cli": "^17.13.1",
"portfinder": "^1.0.32"
},
"devDependencies": {
"@types/node": "^20.10.8"
"@types/node": "^20.11.0"
},
"publishConfig": {
"access": "public"

View File

@ -43,7 +43,7 @@
"@vuepress/utils": "2.0.0-rc.0",
"chokidar": "^3.5.3",
"create-filter": "^1.0.1",
"vue": "^3.4.7"
"vue": "^3.4.10"
},
"publishConfig": {
"access": "public"

View File

@ -37,7 +37,7 @@
"@vuepress/shared": "2.0.0-rc.0",
"@vuepress/utils": "2.0.0-rc.0",
"leancloud-storage": "^4.15.2",
"vue": "^3.4.7",
"vue": "^3.4.10",
"vue-router": "4.2.5",
"vuepress-plugin-netlify-functions": "workspace:*"
},

View File

@ -37,7 +37,8 @@
"nanoid": "^5.0.4",
"picocolors": "^1.0.0",
"shikiji": "^0.9.18",
"shikiji-transformers": "^0.9.18"
"shikiji-transformers": "^0.9.18",
"shikiji-twoslash": "^0.9.18"
},
"publishConfig": {
"access": "public"

View File

@ -15,7 +15,9 @@ import {
transformerNotationFocus,
transformerNotationHighlight,
} from 'shikiji-transformers'
import { rendererRich, transformerTwoSlash } from 'shikiji-twoslash'
import type { HighlighterOptions, ThemeOptions } from './types.js'
import { resolveAttrs } from './resolveAttrs.js'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -88,9 +90,7 @@ export async function highlight(
lang = defaultLang
}
}
// const lineOptions = attrsToLines(attrs)
const { attrs: attributes, rawAttrs } = resolveAttrs(attrs || '')
const mustaches = new Map<string, string>()
const removeMustache = (s: string) => {
@ -110,32 +110,75 @@ export async function highlight(
mustaches.forEach((marker, match) => {
s = s.replaceAll(marker, match)
})
return s
}
const fillEmptyHighlightedLine = (s: string) => {
return `${s.replace(
/(<span class="line highlighted">)(<\/span>)/g,
'$1<wbr>$2',
).replace(/(\/\/\s*?\[)\\(!code.*?\])/g, '$1$2')}\n`
return `${s}\n`
}
str = removeMustache(str).trimEnd()
const highlighted = highlighter.codeToHtml(str, {
lang,
transformers: [
...transformers,
...userTransformers,
],
meta: {
__raw: attrs,
const inlineTransformers: ShikijiTransformer[] = [
{
name: 'vuepress-shikiji:empty-line',
pre(hast) {
hast.children.forEach((code) => {
if (code.type === 'element' && code.tagName === 'code') {
code.children.forEach((span) => {
if (
span.type === 'element'
&& span.tagName === 'span'
&& Array.isArray(span.properties.class)
&& span.properties.class.includes('line')
&& span.children.length === 0
) {
span.children.push({
type: 'element',
tagName: 'wbr',
properties: {},
children: [],
})
}
})
}
})
},
},
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
? { themes: theme, defaultColor: false }
: { theme }),
})
{
name: 'vuepress-shikiji:remove-escape',
postprocess(code) {
return code.replace(/\[\\\!code/g, '[!code')
},
},
]
return fillEmptyHighlightedLine(restoreMustache(highlighted))
if (attributes.twoslash) {
inlineTransformers.push(transformerTwoSlash({
renderer: rendererRich({
classExtra: 'vp-copy-ignore',
}),
}))
}
try {
const highlighted = highlighter.codeToHtml(str, {
lang,
transformers: [
...transformers,
...inlineTransformers,
...userTransformers,
],
meta: {
__raw: rawAttrs,
},
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
? { themes: theme, defaultColor: false }
: { theme }),
})
return restoreMustache(highlighted)
}
catch (e) {
logger.error(e)
return str
}
}
}

View File

@ -0,0 +1,43 @@
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w\d-]+)(?:=\s*(?<quote>['"])(?<value>.+?)\k<quote>)?(?:\s+|$)/
const RE_CODE_BLOCKS = /^[\w\d-]*(\s*:[\w\d-]*)?(\s*\{[\d\w-,\s]+\})?\s*/
export function resolveAttrs(info: string): {
attrs: Record<string, string | boolean>
rawAttrs: string
} {
if (!info)
return { rawAttrs: '', attrs: {} }
info = info.replace(RE_CODE_BLOCKS, '').trim()
if (!info)
return { rawAttrs: '', attrs: {} }
const attrs: Record<string, string | boolean> = {}
const rawAttrs = info
let matched: RegExpMatchArray | null
// eslint-disable-next-line no-cond-assign
while (matched = info.match(RE_ATTR_VALUE)) {
const { attr, value } = matched.groups || {}
attrs[attr] = value ?? true
info = info.slice(matched[0].length)
}
Object.keys(attrs).forEach((key) => {
let value = attrs[key]
value = typeof value === 'string' ? value.trim() : value
if (value === 'true')
value = true
else if (value === 'false')
value = false
attrs[key] = value
if (key.includes('-')) {
const _key = key.replace(/-(\w)/g, (_, c) => c.toUpperCase())
attrs[_key] = value
}
})
return { attrs, rawAttrs }
}

View File

@ -2,9 +2,6 @@ import type { Plugin } from '@vuepress/core'
import { highlight } from './highlight.js'
import type { HighlighterOptions } from './types'
/**
* Options of @vuepress/plugin-shiki
*/
export type ShikijiPluginOptions = HighlighterOptions
export function shikijiPlugin(options: ShikijiPluginOptions = {}): Plugin {

1213
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -82,7 +82,7 @@
"katex": "^0.16.9",
"lodash.merge": "^4.6.2",
"nanoid": "^5.0.4",
"vue": "^3.4.7",
"vue": "^3.4.10",
"vue-router": "4.2.5",
"vuepress-plugin-comment2": "2.0.0-rc.10",
"vuepress-plugin-md-enhance": "2.0.0-rc.10",

View File

@ -6,6 +6,8 @@
@import "utils";
@import "content";
@import "code";
@import "twoslash";
@import "md-enhance";
@import "search";
@import "@vuepress/plugin-palette/style";

View File

@ -0,0 +1,225 @@
/* stylelint-disable no-descending-specificity */
/* ===== Basic ===== */
:root {
--twoslash-border-color: var(--vp-c-divider);
--twoslash-jsdoc-color: #888;
--twoslash-underline-color: currentcolor;
--twoslash-popup-bg: var(--vp-c-neutral-inverse);
--twoslash-popup-shadow: var(--vp-shadow-2);
--twoslash-matched-color: inherit;
--twoslash-unmatched-color: #888;
--twoslash-cursor-color: #8888;
--twoslash-error-color: var(--vp-c-danger-1);
--twoslash-error-bg: var(--vp-c-danger-soft);
--twoslash-tag-color: var(--vp-c-tip-1);
--twoslash-tag-bg: var(--vp-c-tip-soft);
--twoslash-tag-warn-color: var(--vp-c-warning-1);
--twoslash-tag-warn-bg: var(--vp-c-warning-soft);
--twoslash-tag-annotate-color: var(--vp-c-green-1);
--twoslash-tag-annotate-bg: var(--vp-c-green-soft);
}
div[class*="language-"].line-numbers-mode:has(> .twoslash) {
.line-numbers {
display: none;
}
pre {
padding-left: 1.5rem;
margin-left: 0;
}
}
/* Respect people's wishes to not have animations */
@media (prefers-reduced-motion: reduce) {
.twoslash * {
transition: none !important;
}
}
/* ===== Hover Info ===== */
.twoslash:hover .twoslash-hover {
border-color: var(--twoslash-underline-color);
}
.twoslash .twoslash-hover {
position: relative;
border-bottom: 1px dotted transparent;
transition: border-color 0.3s;
transition-timing-function: ease;
}
.twoslash .twoslash-popup-info {
position: absolute;
z-index: 10;
display: inline-block;
padding: 4px 6px;
text-align: left;
pointer-events: none;
user-select: none;
background: var(--twoslash-popup-bg);
border: 1px solid var(--twoslash-border-color);
border-radius: 4px;
box-shadow: var(--twoslash-popup-shadow);
opacity: 0;
transition: opacity 0.3s;
transform: translateY(1.5em);
}
.twoslash .twoslash-query-presisted .twoslash-popup-info {
left: 50%;
z-index: 9;
transform: translate(-1.3em, 1.8em);
}
.twoslash .twoslash-hover:hover .twoslash-popup-info,
.twoslash .twoslash-query-presisted .twoslash-popup-info {
pointer-events: auto;
opacity: 1;
}
.twoslash .twoslash-popup-info:hover {
user-select: auto;
}
.twoslash .twoslash-popup-arrow {
position: absolute;
top: -4px;
left: 1em;
width: 6px;
height: 6px;
pointer-events: none;
background: var(--twoslash-popup-bg);
border-top: 1px solid var(--twoslash-border-color);
border-right: 1px solid var(--twoslash-border-color);
transform: rotate(-45deg);
}
.twoslash .twoslash-popup-jsdoc {
padding-top: 6px;
padding-bottom: 2px;
font-family: sans-serif;
font-size: 0.8em;
color: var(--twoslash-jsdoc-color);
}
/* ===== Error Line ===== */
.twoslash .twoslash-error-line {
position: relative;
padding: 6px;
margin: 0.2em 0;
color: var(--twoslash-error-color);
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
}
.twoslash .twoslash-error {
padding-bottom: 2px;
background:
url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
repeat-x bottom left;
}
/* ===== Completeions ===== */
.twoslash .twoslash-completions-list {
position: relative;
}
.twoslash .twoslash-completions-list ul {
position: absolute;
top: 0;
left: 0;
z-index: 8;
display: inline-block;
display: flex;
flex-direction: column;
gap: 4px;
width: 240px;
padding: 4px;
margin: 3px 0 0 -1px;
font-size: 0.8rem;
user-select: none;
background: var(--twoslash-popup-bg);
border: 1px solid var(--twoslash-border-color);
box-shadow: var(--twoslash-popup-shadow);
transform: translate(0, 1.2em);
}
.twoslash .twoslash-completions-list ul:hover {
user-select: auto;
}
.twoslash .twoslash-completions-list ul::before {
position: absolute;
top: -1.6em;
left: -1px;
width: 2px;
height: 1.4em;
content: " ";
background-color: var(--twoslash-cursor-color);
}
.twoslash .twoslash-completions-list ul li {
display: flex;
gap: 0.25em;
align-items: center;
overflow: hidden;
line-height: 1em;
}
.twoslash .twoslash-completions-list ul li span.twoslash-completions-unmatched {
color: var(--twoslash-unmatched-color);
}
.twoslash .twoslash-completions-list ul .deprecated {
text-decoration: line-through;
opacity: 0.5;
}
.twoslash .twoslash-completions-list ul li span.twoslash-completions-matched {
color: var(--twoslash-matched-color);
}
/* Icons */
.twoslash .twoslash-completions-list .twoslash-completions-icon {
flex: none;
width: 1em;
color: var(--twoslash-unmatched-color);
}
/* Custom Tags */
.twoslash .twoslash-tag-line {
position: relative;
display: flex;
gap: 0.3em;
align-items: center;
padding: 6px;
margin: 0.2em 0;
color: var(--twoslash-tag-color);
background-color: var(--twoslash-tag-bg);
border-left: 3px solid var(--twoslash-tag-color);
}
.twoslash .twoslash-tag-line .twoslash-tag-icon {
width: 1.1em;
color: inherit;
}
.twoslash .twoslash-tag-line.twoslash-tag-error-line {
color: var(--twoslash-error-color);
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
}
.twoslash .twoslash-tag-line.twoslash-tag-warn-line {
color: var(--twoslash-tag-warn-color);
background-color: var(--twoslash-tag-warn-bg);
border-left: 3px solid var(--twoslash-tag-warn-color);
}
.twoslash .twoslash-tag-line.twoslash-tag-annotate-line {
color: var(--twoslash-tag-annotate-color);
background-color: var(--twoslash-tag-annotate-bg);
border-left: 3px solid var(--twoslash-tag-annotate-color);
}