Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit c176b23

Browse files
authoredDec 16, 2023
feat: support grammar injection (#48)
1 parent 0294ad4 commit c176b23

File tree

12 files changed

+344
-198
lines changed

12 files changed

+344
-198
lines changed
 

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"lint-staged": "^15.2.0",
3939
"markdown-it": "^14.0.0",
4040
"markdown-it-shikiji": "workspace:*",
41+
"ofetch": "^1.3.3",
4142
"pnpm": "^8.12.1",
4243
"prettier": "^3.1.1",
4344
"rimraf": "^5.0.5",

‎packages/shikiji-core/src/resolver.ts

+25-8
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { IOnigLib, RegistryOptions } from './textmate'
22
import type { LanguageRegistration } from './types'
33

44
export class Resolver implements RegistryOptions {
5-
private readonly languageMap: { [langIdOrAlias: string]: LanguageRegistration } = {}
6-
private readonly scopeToLangMap: { [scope: string]: LanguageRegistration } = {}
5+
private readonly _langs = new Map<string, LanguageRegistration>()
6+
private readonly _scopeToLang = new Map<string, LanguageRegistration>()
7+
private readonly _injections = new Map<string, string[]>()
78

89
private readonly _onigLibPromise: Promise<IOnigLib>
910

1011
constructor(onigLibPromise: Promise<IOnigLib>, langs: LanguageRegistration[]) {
1112
this._onigLibPromise = onigLibPromise
12-
1313
langs.forEach(i => this.addLanguage(i))
1414
}
1515

@@ -18,20 +18,37 @@ export class Resolver implements RegistryOptions {
1818
}
1919

2020
public getLangRegistration(langIdOrAlias: string): LanguageRegistration {
21-
return this.languageMap[langIdOrAlias]
21+
return this._langs.get(langIdOrAlias)!
2222
}
2323

2424
public async loadGrammar(scopeName: string): Promise<any> {
25-
return this.scopeToLangMap[scopeName]
25+
return this._scopeToLang.get(scopeName)!
2626
}
2727

2828
public addLanguage(l: LanguageRegistration) {
29-
this.languageMap[l.name] = l
29+
this._langs.set(l.name, l)
3030
if (l.aliases) {
3131
l.aliases.forEach((a) => {
32-
this.languageMap[a] = l
32+
this._langs.set(a, l)
33+
})
34+
}
35+
this._scopeToLang.set(l.scopeName, l)
36+
if (l.injectTo) {
37+
l.injectTo.forEach((i) => {
38+
if (!this._injections.get(i))
39+
this._injections.set(i, [])
40+
this._injections.get(i)!.push(l.scopeName)
3341
})
3442
}
35-
this.scopeToLangMap[l.scopeName] = l
43+
}
44+
45+
public getInjections(scopeName: string): string[] | undefined {
46+
const scopeParts = scopeName.split('.')
47+
let injections: string[] = []
48+
for (let i = 1; i <= scopeParts.length; i++) {
49+
const subScopeName = scopeParts.slice(0, i).join('.')
50+
injections = [...injections, ...(this._injections.get(subScopeName) || [])]
51+
}
52+
return injections
3653
}
3754
}

‎packages/shikiji-core/src/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@ export interface LanguageRegistration extends RawGrammar {
197197
embeddedLangs?: string[]
198198
balancedBracketSelectors?: string[]
199199
unbalancedBracketSelectors?: string[]
200+
201+
/**
202+
* Inject this language to other scopes.
203+
* Same as `injectTo` in VSCode's `contributes.grammars`.
204+
*
205+
* @see https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#injection-grammars
206+
*/
207+
injectTo?: string[]
200208
}
201209

202210
export interface CodeToThemedTokensOptions<Languages = string, Themes = string> {

‎packages/shikiji/scripts/prepare.ts

+4-190
Original file line numberDiff line numberDiff line change
@@ -1,194 +1,8 @@
11
import fs from 'fs-extra'
2-
import { BUNDLED_LANGUAGES, BUNDLED_THEMES } from 'shiki'
3-
import fg from 'fast-glob'
4-
5-
const allLangFiles = await fg('*.json', {
6-
cwd: './node_modules/shiki/languages',
7-
absolute: true,
8-
onlyFiles: true,
9-
})
10-
11-
const comments = `
12-
/**
13-
* Generated by scripts/prepare.ts
14-
*/
15-
`.trim()
2+
import { prepareLangs } from './prepare/langs'
3+
import { prepareTheme } from './prepare/themes'
164

175
await fs.ensureDir('./src/assets/langs')
186
await fs.emptyDir('./src/assets/langs')
19-
20-
allLangFiles.sort()
21-
for (const file of allLangFiles) {
22-
const content = await fs.readJSON(file)
23-
const lang = BUNDLED_LANGUAGES.find(i => i.id === content.name)
24-
if (!lang) {
25-
console.warn(`unknown ${content.name}`)
26-
continue
27-
}
28-
29-
const json = {
30-
...content,
31-
name: content.name || lang.id,
32-
scopeName: content.scopeName || lang.scopeName,
33-
displayName: lang.displayName,
34-
aliases: lang.aliases,
35-
embeddedLangs: lang.embeddedLangs,
36-
balancedBracketSelectors: lang.balancedBracketSelectors,
37-
unbalancedBracketSelectors: lang.unbalancedBracketSelectors,
38-
}
39-
40-
// F# and Markdown has circular dependency
41-
if (lang.id === 'fsharp')
42-
json.embeddedLangs = json.embeddedLangs.filter((i: string) => i !== 'markdown')
43-
44-
const embedded = (json.embeddedLangs || []) as string[]
45-
46-
await fs.writeFile(`./src/assets/langs/${lang.id}.ts`, `${comments}
47-
import type { LanguageRegistration } from 'shikiji-core'
48-
49-
${embedded.map(i => `import ${i.replace(/[^\w]/g, '_')} from './${i}'`).join('\n')}
50-
51-
const lang = Object.freeze(${JSON.stringify(json)}) as unknown as LanguageRegistration
52-
53-
export default [
54-
${[
55-
...embedded.map(i => ` ...${i.replace(/[^\w]/g, '_')}`),
56-
' lang',
57-
].join(',\n') || ''}
58-
]
59-
`, 'utf-8')
60-
}
61-
62-
async function writeLanguageBundleIndex(fileName: string, ids: string[]) {
63-
const bundled = ids.map(id => BUNDLED_LANGUAGES.find(i => i.id === id)!)
64-
65-
const info = bundled.map(i => ({
66-
id: i.id,
67-
name: i.displayName,
68-
aliases: i.aliases,
69-
import: `__(() => import('./langs/${i.id}')) as DynamicLangReg__`,
70-
}) as const)
71-
.sort((a, b) => a.id.localeCompare(b.id))
72-
73-
const type = info.flatMap(i => [...i.aliases || [], i.id]).sort().map(i => `'${i}'`).join(' | ')
74-
75-
await fs.writeFile(
76-
`src/assets/${fileName}.ts`,
77-
`${comments}
78-
import type { LanguageRegistration } from 'shikiji-core'
79-
80-
type DynamicLangReg = () => Promise<{ default: LanguageRegistration[] }>
81-
82-
export interface BundledLanguageInfo {
83-
id: string
84-
name: string
85-
import: DynamicLangReg
86-
aliases?: string[]
87-
}
88-
89-
export const bundledLanguagesInfo: BundledLanguageInfo[] = ${JSON.stringify(info, null, 2).replace(/"__|__"/g, '').replace(/"/g, '\'')}
90-
91-
export const bundledLanguagesBase = Object.fromEntries(bundledLanguagesInfo.map(i => [i.id, i.import]))
92-
93-
export const bundledLanguagesAlias = Object.fromEntries(bundledLanguagesInfo.flatMap(i => i.aliases?.map(a => [a, i.import]) || []))
94-
95-
export type BuiltinLanguage = ${type}
96-
97-
export const bundledLanguages = {
98-
...bundledLanguagesBase,
99-
...bundledLanguagesAlias,
100-
} as Record<BuiltinLanguage, DynamicLangReg>
101-
`,
102-
'utf-8',
103-
)
104-
105-
await fs.writeJSON(
106-
`src/assets/${fileName}.json`,
107-
BUNDLED_LANGUAGES.map(i => ({
108-
id: i.id,
109-
name: i.displayName,
110-
aliases: i.aliases,
111-
})),
112-
{ spaces: 2 },
113-
)
114-
}
115-
116-
await writeLanguageBundleIndex('langs', BUNDLED_LANGUAGES.map(i => i.id))
117-
// await writeLanguageBundleIndex('langs-common', BundleCommonLangs)
118-
119-
const themes = BUNDLED_THEMES.sort()
120-
.filter(i => i !== 'css-variables')
121-
.map((id) => {
122-
const theme = fs.readJSONSync(`./node_modules/shiki/themes/${id}.json`)
123-
124-
return {
125-
id,
126-
name: guessThemeName(id, theme),
127-
type: guessThemeType(id, theme),
128-
import: `__(() => import('shiki/themes/${id}.json')) as unknown as DynamicThemeReg__`,
129-
}
130-
})
131-
132-
await fs.writeFile(
133-
'src/assets/themes.ts',
134-
`${comments}
135-
import type { ThemeRegistrationRaw } from 'shikiji-core'
136-
137-
type DynamicThemeReg = () => Promise<{ default: ThemeRegistrationRaw }>
138-
139-
export interface BundledThemeInfo {
140-
id: string
141-
name: string
142-
type: 'light' | 'dark'
143-
import: DynamicThemeReg
144-
}
145-
146-
export const bundledThemesInfo: BundledThemeInfo[] = ${JSON.stringify(themes, null, 2).replace(/"__|__"/g, '')}
147-
148-
export type BuiltinTheme = ${themes.map(i => `'${i.id}'`).join(' | ')}
149-
150-
export const bundledThemes = Object.fromEntries(bundledThemesInfo.map(i => [i.id, i.import])) as Record<BuiltinTheme, DynamicThemeReg>
151-
`,
152-
'utf-8',
153-
)
154-
155-
await fs.writeJSON(
156-
'src/assets/themes.json',
157-
BUNDLED_THEMES
158-
.filter(i => i !== 'css-variables')
159-
.map(i => ({
160-
id: i,
161-
})),
162-
{ spaces: 2 },
163-
)
164-
165-
function isLightColor(hex: string) {
166-
const [r, g, b] = hex.slice(1).match(/.{2}/g)!.map(i => Number.parseInt(i, 16))
167-
return (r * 299 + g * 587 + b * 114) / 1000 > 128
168-
}
169-
170-
function guessThemeType(id: string, theme: any) {
171-
let color
172-
if (id.includes('dark') || id.includes('dimmed') || id.includes('black'))
173-
color = 'dark'
174-
else if (id.includes('light') || id.includes('white') || id === 'slack-ochin')
175-
color = 'light'
176-
else if (theme.colors.background)
177-
color = isLightColor(theme.colors.background) ? 'light' : 'dark'
178-
else if (theme.colors['editor.background'])
179-
color = isLightColor(theme.colors['editor.background']) ? 'light' : 'dark'
180-
else if (theme.colors.foreground)
181-
color = isLightColor(theme.colors.foreground) ? 'dark' : 'light'
182-
else
183-
color = 'light'
184-
return color
185-
}
186-
187-
function guessThemeName(id: string, theme: any) {
188-
if (theme.displayName)
189-
return theme.displayName
190-
let name: string = theme.name || id
191-
name = name.split(/[_-]/).map(i => i[0].toUpperCase() + i.slice(1)).join(' ')
192-
name = name.replace(/github/ig, 'GitHub')
193-
return name
194-
}
7+
await prepareLangs()
8+
await prepareTheme()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const COMMENT_HEAD = `
2+
/**
3+
* Generated by scripts/prepare.ts
4+
*/
5+
`.trim()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Download and prepare grammar injections
2+
3+
import fs from 'fs-extra'
4+
import { fetch } from 'ofetch'
5+
import { COMMENT_HEAD } from './constants'
6+
7+
interface Injection {
8+
name: string
9+
contents: any[]
10+
/**
11+
* Bundle into a language
12+
*/
13+
toLang?: string
14+
}
15+
16+
export async function prepareInjections() {
17+
const injections = (await Promise.all([
18+
prepareVueInjections(),
19+
])).flat()
20+
21+
for (const injection of injections) {
22+
await fs.writeFile(
23+
`src/assets/langs/${injection.name}.ts`,
24+
`${COMMENT_HEAD}
25+
import type { LanguageRegistration } from 'shikiji-core'
26+
27+
export default ${JSON.stringify(injection.contents, null, 2)} as unknown as LanguageRegistration[]
28+
`,
29+
'utf-8',
30+
)
31+
}
32+
33+
return injections
34+
}
35+
36+
export async function prepareVueInjections(): Promise<Injection> {
37+
const base = 'https://github.com/vuejs/language-tools/blob/master/extensions/vscode/'
38+
const pkgJson = await fetchJson(`${base}package.json?raw=true`)
39+
const grammars = pkgJson.contributes.grammars as any[]
40+
const injections = await Promise.all(grammars
41+
.filter(i => i.injectTo)
42+
.map(async (i) => {
43+
const content = await fetchJson(`${new URL(i.path, base).href}?raw=true`)
44+
return {
45+
...content,
46+
name: i.language,
47+
injectTo: i.injectTo,
48+
}
49+
}),
50+
)
51+
52+
return {
53+
name: 'vue-injections',
54+
toLang: 'vue',
55+
contents: injections,
56+
}
57+
}
58+
59+
export function fetchJson(url: string) {
60+
return fetch(url).then(res => res.json())
61+
}
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import fs from 'fs-extra'
2+
import { BUNDLED_LANGUAGES } from 'shiki'
3+
import fg from 'fast-glob'
4+
import type { LanguageRegistration } from 'shikiji-core'
5+
import { COMMENT_HEAD } from './constants'
6+
import { prepareInjections } from './injections'
7+
8+
export async function prepareLangs() {
9+
const allLangFiles = await fg('*.json', {
10+
cwd: './node_modules/shiki/languages',
11+
absolute: true,
12+
onlyFiles: true,
13+
})
14+
15+
allLangFiles.sort()
16+
17+
const injections = await prepareInjections()
18+
19+
for (const file of allLangFiles) {
20+
const content = await fs.readJSON(file)
21+
const lang = BUNDLED_LANGUAGES.find(i => i.id === content.name)
22+
if (!lang) {
23+
console.warn(`unknown ${content.name}`)
24+
continue
25+
}
26+
27+
const json: LanguageRegistration = {
28+
...content,
29+
name: content.name || lang.id,
30+
scopeName: content.scopeName || lang.scopeName,
31+
displayName: lang.displayName,
32+
aliases: lang.aliases,
33+
embeddedLangs: lang.embeddedLangs,
34+
balancedBracketSelectors: lang.balancedBracketSelectors,
35+
unbalancedBracketSelectors: lang.unbalancedBracketSelectors,
36+
}
37+
38+
// F# and Markdown has circular dependency
39+
if (lang.id === 'fsharp' && json.embeddedLangs)
40+
json.embeddedLangs = json.embeddedLangs.filter((i: string) => i !== 'markdown')
41+
42+
const deps: string[] = [
43+
...(json.embeddedLangs || []),
44+
...injections.filter(i => i.toLang === lang.id).map(i => i.name),
45+
]
46+
47+
await fs.writeFile(`./src/assets/langs/${lang.id}.ts`, `${COMMENT_HEAD}
48+
import type { LanguageRegistration } from 'shikiji-core'
49+
50+
${deps.map(i => `import ${i.replace(/[^\w]/g, '_')} from './${i}'`).join('\n')}
51+
52+
const lang = Object.freeze(${JSON.stringify(json)}) as unknown as LanguageRegistration
53+
54+
export default [
55+
${[
56+
...deps.map(i => ` ...${i.replace(/[^\w]/g, '_')}`),
57+
' lang',
58+
].join(',\n') || ''}
59+
]
60+
`, 'utf-8')
61+
}
62+
63+
async function writeLanguageBundleIndex(fileName: string, ids: string[]) {
64+
const bundled = ids.map(id => BUNDLED_LANGUAGES.find(i => i.id === id)!)
65+
66+
const info = bundled.map(i => ({
67+
id: i.id,
68+
name: i.displayName,
69+
aliases: i.aliases,
70+
import: `__(() => import('./langs/${i.id}')) as DynamicLangReg__`,
71+
}) as const)
72+
.sort((a, b) => a.id.localeCompare(b.id))
73+
74+
const type = info.flatMap(i => [...i.aliases || [], i.id]).sort().map(i => `'${i}'`).join(' | ')
75+
76+
await fs.writeFile(
77+
`src/assets/${fileName}.ts`,
78+
`${COMMENT_HEAD}
79+
import type { LanguageRegistration } from 'shikiji-core'
80+
81+
type DynamicLangReg = () => Promise<{ default: LanguageRegistration[] }>
82+
83+
export interface BundledLanguageInfo {
84+
id: string
85+
name: string
86+
import: DynamicLangReg
87+
aliases?: string[]
88+
}
89+
90+
export const bundledLanguagesInfo: BundledLanguageInfo[] = ${JSON.stringify(info, null, 2).replace(/"__|__"/g, '').replace(/"/g, '\'')}
91+
92+
export const bundledLanguagesBase = Object.fromEntries(bundledLanguagesInfo.map(i => [i.id, i.import]))
93+
94+
export const bundledLanguagesAlias = Object.fromEntries(bundledLanguagesInfo.flatMap(i => i.aliases?.map(a => [a, i.import]) || []))
95+
96+
export type BuiltinLanguage = ${type}
97+
98+
export const bundledLanguages = {
99+
...bundledLanguagesBase,
100+
...bundledLanguagesAlias,
101+
} as Record<BuiltinLanguage, DynamicLangReg>
102+
`,
103+
'utf-8',
104+
)
105+
106+
await fs.writeJSON(
107+
`src/assets/${fileName}.json`,
108+
BUNDLED_LANGUAGES.map(i => ({
109+
id: i.id,
110+
name: i.displayName,
111+
aliases: i.aliases,
112+
})),
113+
{ spaces: 2 },
114+
)
115+
}
116+
117+
await writeLanguageBundleIndex('langs', BUNDLED_LANGUAGES.map(i => i.id))
118+
}
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import fs from 'fs-extra'
2+
import { BUNDLED_THEMES } from 'shiki'
3+
import { COMMENT_HEAD } from './constants'
4+
5+
export async function prepareTheme() {
6+
const themes = BUNDLED_THEMES.sort()
7+
.filter(i => i !== 'css-variables')
8+
.map((id) => {
9+
const theme = fs.readJSONSync(`./node_modules/shiki/themes/${id}.json`)
10+
11+
return {
12+
id,
13+
name: guessThemeName(id, theme),
14+
type: guessThemeType(id, theme),
15+
import: `__(() => import('shiki/themes/${id}.json')) as unknown as DynamicThemeReg__`,
16+
}
17+
})
18+
await fs.writeFile(
19+
'src/assets/themes.ts',
20+
`${COMMENT_HEAD}
21+
import type { ThemeRegistrationRaw } from 'shikiji-core'
22+
23+
type DynamicThemeReg = () => Promise<{ default: ThemeRegistrationRaw }>
24+
25+
export interface BundledThemeInfo {
26+
id: string
27+
name: string
28+
type: 'light' | 'dark'
29+
import: DynamicThemeReg
30+
}
31+
32+
export const bundledThemesInfo: BundledThemeInfo[] = ${JSON.stringify(themes, null, 2).replace(/"__|__"/g, '')}
33+
34+
export type BuiltinTheme = ${themes.map(i => `'${i.id}'`).join(' | ')}
35+
36+
export const bundledThemes = Object.fromEntries(bundledThemesInfo.map(i => [i.id, i.import])) as Record<BuiltinTheme, DynamicThemeReg>
37+
`,
38+
'utf-8',
39+
)
40+
await fs.writeJSON(
41+
'src/assets/themes.json',
42+
BUNDLED_THEMES
43+
.filter(i => i !== 'css-variables')
44+
.map(i => ({
45+
id: i,
46+
})),
47+
{ spaces: 2 },
48+
)
49+
}
50+
51+
function isLightColor(hex: string) {
52+
const [r, g, b] = hex.slice(1).match(/.{2}/g)!.map(i => Number.parseInt(i, 16))
53+
return (r * 299 + g * 587 + b * 114) / 1000 > 128
54+
}
55+
56+
function guessThemeType(id: string, theme: any) {
57+
let color
58+
if (id.includes('dark') || id.includes('dimmed') || id.includes('black'))
59+
color = 'dark'
60+
else if (id.includes('light') || id.includes('white') || id === 'slack-ochin')
61+
color = 'light'
62+
else if (theme.colors.background)
63+
color = isLightColor(theme.colors.background) ? 'light' : 'dark'
64+
else if (theme.colors['editor.background'])
65+
color = isLightColor(theme.colors['editor.background']) ? 'light' : 'dark'
66+
else if (theme.colors.foreground)
67+
color = isLightColor(theme.colors.foreground) ? 'dark' : 'light'
68+
69+
else
70+
color = 'light'
71+
return color
72+
}
73+
74+
function guessThemeName(id: string, theme: any) {
75+
if (theme.displayName)
76+
return theme.displayName
77+
let name: string = theme.name || id
78+
name = name.split(/[_-]/).map(i => i[0].toUpperCase() + i.slice(1)).join(' ')
79+
name = name.replace(/github/ig, 'GitHub')
80+
return name
81+
}

‎packages/shikiji/test/index.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ describe('should', () => {
145145
"typescript",
146146
"vb",
147147
"vue",
148+
"vue-directives",
149+
"vue-injection-markdown",
150+
"vue-interpolations",
151+
"vue-sfc-style-variable-injection",
148152
"xml",
149153
"xsl",
150154
"yaml",
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { codeToHtml } from 'shikiji'
2+
import { expect, it } from 'vitest'
3+
4+
// Basically we need to make sure that the syntax inside `v-if` and `{{}}` is highlighted correctly.
5+
// This is done by a `vue-injections` patch that injects extra grammar into HTML.
6+
it('vue-injections', async () => {
7+
const code = `
8+
<script setup lang="ts">
9+
import { ref } from 'vue'
10+
const count = ref(0)
11+
</script>
12+
13+
<template>
14+
<div>
15+
<h1 v-if="count == 1 ? true : 'str'.toUpperCase()">{{ count * 2 }}</h1>
16+
</div>
17+
</template>
18+
`
19+
20+
expect(`${await codeToHtml(code, { lang: 'vue', theme: 'vitesse-dark' })}<style>html{color-scheme:dark}</style>`)
21+
.toMatchFileSnapshot('./out/vue-injections.html')
22+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<pre class="shiki vitesse-dark" style="background-color:#121212;color:#dbd7caee" tabindex="0"><code><span class="line"></span>
2+
<span class="line"><span style="color:#666666">&#x3C;</span><span style="color:#4D9375">script</span><span style="color:#BD976A"> setup</span><span style="color:#BD976A"> lang</span><span style="color:#666666">=</span><span style="color:#C98A7D99">"</span><span style="color:#C98A7D">ts</span><span style="color:#C98A7D99">"</span><span style="color:#666666">></span></span>
3+
<span class="line"><span style="color:#4D9375">import</span><span style="color:#666666"> {</span><span style="color:#BD976A"> ref</span><span style="color:#666666"> }</span><span style="color:#4D9375"> from</span><span style="color:#C98A7D99"> '</span><span style="color:#C98A7D">vue</span><span style="color:#C98A7D99">'</span></span>
4+
<span class="line"><span style="color:#CB7676">const </span><span style="color:#BD976A">count</span><span style="color:#666666"> =</span><span style="color:#80A665"> ref</span><span style="color:#666666">(</span><span style="color:#4C9A91">0</span><span style="color:#666666">)</span></span>
5+
<span class="line"><span style="color:#666666">&#x3C;/</span><span style="color:#4D9375">script</span><span style="color:#666666">></span></span>
6+
<span class="line"></span>
7+
<span class="line"><span style="color:#666666">&#x3C;</span><span style="color:#4D9375">template</span><span style="color:#666666">></span></span>
8+
<span class="line"><span style="color:#666666"> &#x3C;</span><span style="color:#4D9375">div</span><span style="color:#666666">></span></span>
9+
<span class="line"><span style="color:#666666"> &#x3C;</span><span style="color:#4D9375">h1</span><span style="color:#4D9375"> v-if</span><span style="color:#666666">=</span><span style="color:#C98A7D99">"</span><span style="color:#BD976A">count</span><span style="color:#CB7676"> ==</span><span style="color:#4C9A91"> 1</span><span style="color:#CB7676"> ?</span><span style="color:#4D9375"> true</span><span style="color:#CB7676"> :</span><span style="color:#C98A7D99"> '</span><span style="color:#C98A7D">str</span><span style="color:#C98A7D99">'</span><span style="color:#666666">.</span><span style="color:#80A665">toUpperCase</span><span style="color:#666666">()</span><span style="color:#C98A7D99">"</span><span style="color:#666666">>{{</span><span style="color:#BD976A"> count</span><span style="color:#CB7676"> *</span><span style="color:#4C9A91"> 2</span><span style="color:#666666"> }}&#x3C;/</span><span style="color:#4D9375">h1</span><span style="color:#666666">></span></span>
10+
<span class="line"><span style="color:#666666"> &#x3C;/</span><span style="color:#4D9375">div</span><span style="color:#666666">></span></span>
11+
<span class="line"><span style="color:#666666">&#x3C;/</span><span style="color:#4D9375">template</span><span style="color:#666666">></span></span>
12+
<span class="line"></span></code></pre><style>html{color-scheme:dark}</style>

‎pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
This repository has been archived.