refactor: extract css-i18n package

This commit is contained in:
YunYouJun 2023-07-20 15:18:36 +08:00
parent de94291af7
commit 60fe76b22f
21 changed files with 246 additions and 156 deletions

View File

@ -273,7 +273,7 @@ const { headers, handleClick } = useOutline()
</span>
<PressOutlineItem
class="va-toc relative z-1"
class="va-toc relative z-1 css-i18n-toc"
:headers="headers"
:on-click="handleClick"
root

View File

@ -47,6 +47,9 @@
"@antfu/eslint-config": "^0.39.8",
"@microsoft/api-extractor": "^7.36.3",
"@types/debug": "^4.1.8",
"@types/markdown-it-attrs": "^4.1.0",
"@types/markdown-it-container": "^2.0.5",
"@types/markdown-it-emoji": "^2.0.2",
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4",
"@types/resolve": "^1.20.2",

View File

@ -0,0 +1,24 @@
# CSS i18n
> To Be Published.
- Based on `markdown-it` & `markdown-it-container`
## Usage
### Use markdown-it plugin
```ts
md.use(cssI18nPlugin, {
languages: ['zh-CN', 'en']
})
```
### CSS
```ts
import 'css-i18n/dist/index.css'
// source scss
// import 'css-i18n/src/index.scss'
```

View File

@ -0,0 +1,24 @@
{
"name": "css-i18n",
"version": "0.0.1",
"keywords": [
"markdown-it-plugin",
"markdown-it",
"markdown"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "npm run build:lib && npm run build:css",
"build:lib": "tsup src/index.ts --dts",
"build:css": "sass src/styles/index.scss dist/index.css"
},
"devDependencies": {
"@types/markdown-it-container": "^2.0.5",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0"
}
}

View File

@ -0,0 +1,18 @@
import type MarkdownIt from 'markdown-it'
import type Token from 'markdown-it/lib/token'
import container from 'markdown-it-container'
export interface CSSI18nOptions {
languages?: string[]
}
export function cssI18nContainer(md: MarkdownIt, options: CSSI18nOptions = {}) {
const languages = options.languages || ['zh-CN', 'en']
languages.forEach((lang) => {
md.use(container, lang, {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div lang="${lang}">\n` : '</div>\n',
})
})
}

View File

@ -14,7 +14,7 @@ html[lang] {
}
}
.va-toc {
.css-i18n-toc {
li[lang] {
a {
display: none;
@ -39,7 +39,7 @@ html[lang] {
}
}
.va-toc {
.css-i18n-toc {
li[lang="#{$lang}"] {
> a {
display: block;

View File

@ -33,7 +33,7 @@ const { headers, handleClick } = useOutline()
</span>
<PressOutlineItem
class="va-toc relative z-1"
class="va-toc relative z-1 css-i18n-toc"
:headers="headers"
:on-click="handleClick"
root

View File

@ -31,7 +31,7 @@ const { headers, handleClick } = useOutline()
</span>
<YunOutlineItem
class="va-toc relative z-1"
class="va-toc relative z-1 css-i18n-toc"
:headers="headers"
:on-click="handleClick"
root

View File

@ -5,9 +5,11 @@ $c-primary: #0078e7 !default;
// global css
@use "./global/reset.scss" as *;
@use "./global/index.scss" as *;
@use "./global/i18n.scss" as *;
@use "./global/nprogress.scss" as *;
// css-i18n
@use "../../../css-i18n/src/styles/index.scss" as *;
// common
@use "./common/code.scss" as *;
@use "./common/custom-blocks.scss" as *;

View File

@ -1,14 +1,10 @@
import MarkdownIt from 'markdown-it'
import type { IThemeRegistration } from 'shiki'
import anchorPlugin from 'markdown-it-anchor'
import attrsPlugin from 'markdown-it-attrs'
import emojiPlugin from 'markdown-it-emoji'
import TaskLists from 'markdown-it-task-lists'
import type { KatexOptions } from 'katex'
import { componentPlugin } from '@mdit-vue/plugin-component'
import {
type FrontmatterPluginOptions,
@ -22,21 +18,20 @@ import { type SfcPluginOptions, sfcPlugin } from '@mdit-vue/plugin-sfc'
import { titlePlugin } from '@mdit-vue/plugin-title'
import { type TocPluginOptions, tocPlugin } from '@mdit-vue/plugin-toc'
import { slugify } from '@mdit-vue/shared'
import { cssI18nContainer } from '../../../css-i18n/src'
import type { Header } from '../../types'
import Katex from './markdown-it/katex'
import { type Blocks, containerPlugin } from './markdown-it/container'
import { slugify } from './slugify'
import { highlight } from './highlight'
import { highlightLinePlugin, preWrapperPlugin } from './markdown-it/highlightLines'
import type { MarkdownOptions } from './types'
import Katex from './plugins/markdown-it/katex'
import { containerPlugin } from './plugins/markdown-it/container'
import { highlight } from './plugins/highlight'
import { highlightLinePlugin, preWrapperPlugin } from './plugins/markdown-it/highlightLines'
import { linkPlugin } from './plugins/link'
// import { lineNumberPlugin } from "./plugins/lineNumbers";
export * from './env'
export type ThemeOptions =
| IThemeRegistration
| { light: IThemeRegistration; dark: IThemeRegistration }
export interface MarkdownParsedData {
hoistedTags?: string[]
@ -46,41 +41,6 @@ export interface MarkdownParsedData {
export type MarkdownRenderer = MarkdownIt
export interface MarkdownOptions {
/**
* markdown-it options
*/
options?: MarkdownIt.Options
/**
* config markdown-it
*/
config?: (md: MarkdownIt) => void
anchor?: anchorPlugin.AnchorOptions
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: string[]
disable?: boolean
}
// mdit-vue plugins
frontmatter?: FrontmatterPluginOptions
headers?: HeadersPluginOptions
sfc?: SfcPluginOptions
toc?: TocPluginOptions
katex?: KatexOptions
/**
* shiki
*/
theme?: ThemeOptions
/**
* Custom block configurations
*/
blocks?: Blocks
externalLinks?: Record<string, string>
}
export async function setupMarkdownPlugins(
md: MarkdownIt,
mdOptions: MarkdownOptions = {},
@ -91,6 +51,9 @@ export async function setupMarkdownPlugins(
md.use(highlightLinePlugin)
.use(preWrapperPlugin)
.use(containerPlugin, mdOptions.blocks)
.use(cssI18nContainer, {
languages: ['zh-CN', 'en'],
})
.use(
linkPlugin,
{
@ -149,10 +112,11 @@ export async function setupMarkdownPlugins(
}
export async function createMarkdownRenderer(mdOptions: MarkdownOptions = {}): Promise<MarkdownRenderer> {
const theme = mdOptions.theme ?? 'material-theme-palenight'
const md = MarkdownIt({
html: true,
linkify: true,
highlight: await highlight(mdOptions.theme),
highlight: await highlight(theme, mdOptions.languages, mdOptions.defaultHighlightLang),
...mdOptions.options,
}) as MarkdownRenderer
await setupMarkdownPlugins(md, mdOptions)

View File

@ -8,8 +8,9 @@ import { resolveTitleFromToken } from '@mdit-vue/shared'
import { EXTERNAL_URL_RE } from '../constants'
import { getGitTimestamp, slash, transformObject } from '../utils'
import type { CleanUrlsMode, HeadConfig, PageData } from '../../types'
import type { MarkdownOptions } from './types'
import { createMarkdownRenderer } from '.'
import type { MarkdownEnv, MarkdownOptions, MarkdownRenderer } from '.'
import type { MarkdownEnv, MarkdownRenderer } from '.'
const jsStringBreaker = '\u200B'
const vueTemplateBreaker = '<wbr>'

View File

@ -1,7 +1,12 @@
// ref vitepress
import { customAlphabet } from 'nanoid'
import c from 'picocolors'
import type { HtmlRendererOptions, IThemeRegistration } from 'shiki'
import {
BUNDLED_LANGUAGES,
type HtmlRendererOptions,
type ILanguageRegistration,
type IThemeRegistration,
} from 'shiki'
import {
type Processor,
addClass,
@ -12,7 +17,8 @@ import {
defineProcessor,
getHighlighter,
} from 'shiki-processor'
import type { ThemeOptions } from '../markdown'
import type { Logger } from 'vite'
import type { ThemeOptions } from '..'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -58,8 +64,10 @@ const errorLevelProcessor = defineProcessor({
})
export async function highlight(
theme: ThemeOptions = 'material-theme-palenight',
defaultLang = 'txt',
theme: ThemeOptions,
languages: ILanguageRegistration[] = [],
defaultLang: string = '',
logger: Pick<Logger, 'warn'> = console,
): Promise<(str: string, lang: string, attrs: string) => string> {
const hasSingleTheme = typeof theme === 'string' || 'name' in theme
const getThemeName = (themeValue: IThemeRegistration) =>
@ -74,6 +82,7 @@ export async function highlight(
const highlighter = await getHighlighter({
themes: hasSingleTheme ? [theme] : [theme.dark, theme.light],
langs: [...BUNDLED_LANGUAGES, ...languages],
processors,
})
@ -90,25 +99,28 @@ export async function highlight(
if (lang) {
const langLoaded = highlighter.getLoadedLanguages().includes(lang as any)
if (!langLoaded) {
if (lang !== defaultLang) {
console.warn(
c.yellow(
`The language '${lang}' is not loaded, falling back to '${
defaultLang
}' for syntax highlighting.`,
),
)
}
if (!langLoaded && lang !== 'ansi' && lang !== 'txt') {
logger.warn(
c.yellow(
`\nThe language '${lang}' is not loaded, falling back to '${
defaultLang || 'txt'
}' for syntax highlighting.`,
),
)
lang = defaultLang
}
}
const lineOptions = attrsToLines(attrs)
const cleanup = (str: string) =>
str
.replace(preRE, (_, attributes) => `<pre ${vPre}${attributes}>`)
const cleanup = (str: string) => {
return str
.replace(
preRE,
(_, attributes) =>
`<pre ${vPre}${attributes.replace(' tabindex="0"', '')}>`,
)
.replace(styleRE, (_, style) => _.replace(style, ''))
}
const mustaches = new Map<string, string>()
@ -132,42 +144,34 @@ export async function highlight(
return s
}
if (hasSingleTheme) {
return cleanup(
restoreMustache(
highlighter.codeToHtml(removeMustache(str), {
lang,
lineOptions,
theme: getThemeName(theme),
}),
),
const fillEmptyHighlightedLine = (s: string) => {
return s.replace(
/(<span class="line highlighted">)(<\/span>)/g,
'$1<wbr>$2',
)
}
const dark = addClass(
cleanup(
highlighter.codeToHtml(str, {
lang,
lineOptions,
theme: getThemeName(theme.dark),
}),
),
'va-code-dark',
'pre',
)
str = removeMustache(str).trim()
const light = addClass(
cleanup(
highlighter.codeToHtml(str, {
lang,
lineOptions,
theme: getThemeName(theme.light),
}),
),
'va-code-light',
'pre',
)
const codeToHtml = (theme: IThemeRegistration) => {
const res
= lang === 'ansi'
? highlighter.ansiToHtml(str, {
lineOptions,
theme: getThemeName(theme),
})
: highlighter.codeToHtml(str, {
lang,
lineOptions,
theme: getThemeName(theme),
})
return fillEmptyHighlightedLine(cleanup(restoreMustache(res)))
}
if (hasSingleTheme)
return codeToHtml(theme)
const dark = addClass(codeToHtml(theme.dark), 'vp-code-dark', 'pre')
const light = addClass(codeToHtml(theme.light), 'vp-code-light', 'pre')
return dark + light
}
}

View File

@ -1,25 +0,0 @@
// ref vitepress
import { remove as removeDiacritics } from 'diacritics'
// eslint-disable-next-line no-control-regex
const rControl = /[\u0000-\u001F]/g
// add '…'
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.…?/]+/g
export function slugify(str: string): string {
return (
removeDiacritics(str)
// Remove control characters
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continuous separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separators
.replace(/^\-+|\-+$/g, '')
// ensure it doesn't start with a number (#121)
.replace(/^(\d)/, '_$1')
// lowercase
.toLowerCase()
)
}

View File

@ -0,0 +1,58 @@
import type MarkdownIt from 'markdown-it'
import type { ILanguageRegistration, IThemeRegistration } from 'shiki'
import type anchorPlugin from 'markdown-it-anchor'
import type { KatexOptions } from 'katex'
import {
type FrontmatterPluginOptions,
} from '@mdit-vue/plugin-frontmatter'
import {
type HeadersPluginOptions,
} from '@mdit-vue/plugin-headers'
import { type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
import { type TocPluginOptions } from '@mdit-vue/plugin-toc'
import { type Blocks } from './plugins/markdown-it/container'
export type ThemeOptions =
| IThemeRegistration
| { light: IThemeRegistration; dark: IThemeRegistration }
export interface MarkdownOptions {
/**
* markdown-it options
*/
options?: MarkdownIt.Options
/**
* config markdown-it
*/
config?: (md: MarkdownIt) => void
anchor?: anchorPlugin.AnchorOptions
attrs?: {
leftDelimiter?: string
rightDelimiter?: string
allowedAttributes?: string[]
disable?: boolean
}
defaultHighlightLang?: string
// mdit-vue plugins
frontmatter?: FrontmatterPluginOptions
headers?: HeadersPluginOptions
sfc?: SfcPluginOptions
toc?: TocPluginOptions
katex?: KatexOptions
/**
* shiki
*/
theme?: ThemeOptions
languages?: ILanguageRegistration[]
/**
* Custom block configurations
*/
blocks?: Blocks
externalLinks?: Record<string, string>
}

View File

@ -3,26 +3,11 @@ declare module 'escape-html' {
export default def
}
declare module 'markdown-it-attrs' {
const def: any
export default def
}
declare module 'markdown-it-emoji' {
const def: any
export default def
}
declare module 'markdown-it-table-of-contents' {
const def: any
export default def
}
declare module 'markdown-it-container' {
const def: any
export default def
}
declare module 'markdown-it-task-lists' {
const def: any
export default def

View File

@ -6,7 +6,7 @@ import type { UserConfig as ViteUserConfig } from 'vite'
import type { presetAttributify, presetIcons, presetTypography, presetUno } from 'unocss'
import type { DefaultThemeConfig, PartialDeep, ValaxyAddon, ValaxyConfig } from '../types'
import type { ResolvedValaxyOptions } from './options'
import type { MarkdownOptions } from './markdown'
import type { MarkdownOptions } from './markdown/types'
export type ValaxyNodeConfig<ThemeConfig = DefaultThemeConfig> = ValaxyConfig<ThemeConfig> & ValaxyExtendConfig
export type UserValaxyNodeConfig<ThemeConfig = DefaultThemeConfig> = PartialDeep<ValaxyNodeConfig<ThemeConfig>>

View File

@ -54,7 +54,7 @@
"scripts": {
"build": "rimraf dist && tsup --splitting",
"dev": "tsup --splitting --watch",
"lint": "eslint \"**/*.{vue,ts,js}\"",
"lint": "eslint .",
"preview": "vite preview",
"preview-https": "serve dist"
},

View File

@ -17,6 +17,15 @@ importers:
'@types/debug':
specifier: ^4.1.8
version: 4.1.8
'@types/markdown-it-attrs':
specifier: ^4.1.0
version: 4.1.0
'@types/markdown-it-container':
specifier: ^2.0.5
version: 2.0.5
'@types/markdown-it-emoji':
specifier: ^2.0.2
version: 2.0.2
'@types/node':
specifier: ^20.4.2
version: 20.4.2
@ -185,6 +194,18 @@ importers:
specifier: ^2.4.2
version: 2.4.2
packages/css-i18n:
devDependencies:
'@types/markdown-it-container':
specifier: ^2.0.5
version: 2.0.5
markdown-it:
specifier: ^13.0.1
version: 13.0.1
markdown-it-container:
specifier: ^3.0.0
version: 3.0.0
packages/valaxy:
dependencies:
'@antfu/utils':
@ -2998,6 +3019,24 @@ packages:
/@types/linkify-it@3.0.2:
resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==}
/@types/markdown-it-attrs@4.1.0:
resolution: {integrity: sha512-ILGUUJf7gydzxY3FrN2XwFT/f6rfxtkXZal478Jf4vqFn2AkQCwGCTx3TI+IPT+5ipOf+hUplem8wfVuCyK/Pw==}
dependencies:
'@types/markdown-it': 12.2.3
dev: true
/@types/markdown-it-container@2.0.5:
resolution: {integrity: sha512-8v5jIC5gcCUv+JcD0DExwNBkoKC0kLB4acensF0NoNlTIcXmQxF3RDjzAdIW82sXSoR+n772ePguxIWlq2ELvA==}
dependencies:
'@types/markdown-it': 12.2.3
dev: true
/@types/markdown-it-emoji@2.0.2:
resolution: {integrity: sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==}
dependencies:
'@types/markdown-it': 12.2.3
dev: true
/@types/markdown-it@12.2.3:
resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
dependencies:
@ -5338,7 +5377,7 @@ packages:
/eslint-import-resolver-node@0.3.7:
resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
is-core-module: 2.12.1
resolve: 1.22.2
transitivePeerDependencies:
@ -5367,7 +5406,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/parser': 6.0.0(eslint@8.45.0)(typescript@5.1.6)
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
eslint: 8.45.0
eslint-import-resolver-node: 0.3.7
transitivePeerDependencies:
@ -5427,7 +5466,7 @@ packages:
peerDependencies:
eslint: ^7.2.0 || ^8
dependencies:
debug: 3.2.7(supports-color@8.1.1)
debug: 3.2.7(supports-color@5.5.0)
doctrine: 2.1.0
eslint: 8.45.0
eslint-import-resolver-node: 0.3.7
@ -7025,7 +7064,7 @@ packages:
eslint-utils: 2.1.0
eslint-visitor-keys: 1.3.0
espree: 6.2.1
semver: 6.3.0
semver: 6.3.1
dev: false
/jsonc-eslint-parser@2.3.0:
@ -7341,7 +7380,6 @@ packages:
/markdown-it-container@3.0.0:
resolution: {integrity: sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==}
dev: false
/markdown-it-emoji@2.0.2:
resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==}
@ -8510,15 +8548,9 @@ packages:
hasBin: true
dev: true
/semver@6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true
dev: false
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
dev: true
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}