const c = 3
-npm i
+npm i
pnpm i
yarn
-const a = 1
+const a = 1
const a = 1
@@ -67,13 +67,13 @@ exports[`codeTabsPlugin > should work with options: { named: [npm,pnpm,yarn], ex
const c = 3
-npm i
+npm i
pnpm i
yarn
-const a = 1
+const a = 1
const a = 1
diff --git a/plugins/plugin-md-power/__test__/__snapshots__/fileTreePlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/fileTreePlugin.spec.ts.snap
index fe41c4d8..aea16acd 100644
--- a/plugins/plugin-md-power/__test__/__snapshots__/fileTreePlugin.spec.ts.snap
+++ b/plugins/plugin-md-power/__test__/__snapshots__/fileTreePlugin.spec.ts.snap
@@ -72,91 +72,91 @@ exports[`fileTree > parseFileTreeRawContent > should work 1`] = `
exports[`fileTreePlugin > should work with default options 1`] = `
"files
:: mdi:11 ::
" `; -exports[`iconsPlugin > should not work with invalid icon 2`] = ` +exports[`iconPlugin > should not work with invalid icon 2`] = ` "::::
" `; -exports[`iconsPlugin > should not work with invalid icon 3`] = ` +exports[`iconPlugin > should not work with invalid icon 3`] = ` "::]&
" `; -exports[`iconsPlugin > should not work with invalid icon 4`] = ` -"::::
-" -`; - -exports[`iconsPlugin > should not work with invalid icon 5`] = ` +exports[`iconPlugin > should not work with invalid icon 4`] = ` "::mdi:11
" `; -exports[`iconsPlugin > should work 1`] = ` -"strong
strong
-
strong
+" npm yarn pnpm
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -16,7 +16,7 @@ exports[`npmToPlugin > should work width options: [npm, yarn, pnpm] 1`] = `
pnpm install
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -25,7 +25,7 @@ exports[`npmToPlugin > should work width options: [npm, yarn, pnpm] 1`] = `
pnpm install
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -34,7 +34,7 @@ exports[`npmToPlugin > should work width options: [npm, yarn, pnpm] 1`] = `
pnpm install
- npm yarn pnpmcross-env NODE_ENV=production npm run docs
+
npm yarn pnpmcross-env NODE_ENV=production npm run docs
cross-env NODE_ENV=production yarn docs
@@ -43,7 +43,7 @@ exports[`npmToPlugin > should work width options: [npm, yarn, pnpm] 1`] = `
cross-env NODE_ENV=production pnpm docs
- npm yarn pnpmnpm i -D package1 package2
+
npm yarn pnpmnpm i -D package1 package2
npm i --save-peer package3
npm run docs
@@ -58,7 +58,7 @@ pnpm add --save-peer package3
pnpm docs
- npm yarn pnpmnpm install && npm run docs
+
npm yarn pnpmnpm install && npm run docs
mkdir foo
@@ -70,7 +70,7 @@ mkdir foo
mkdir foo
- npm yarn pnpmnpm run docs -- --clean-cache --clean-temp
+
npm yarn pnpmnpm run docs -- --clean-cache --clean-temp
@@ -82,7 +82,7 @@ mkdir foo
- npm pnpm yarnnpm create vuepress-theme-plume@latest
+
npm pnpm yarnnpm create vuepress-theme-plume@latest
pnpm create vuepress-theme-plume@latest
@@ -91,7 +91,7 @@ mkdir foo
yarn create vuepress-theme-plume@latest
- npm pnpm yarn bun denonpx vp-update
+
npm pnpm yarn bun denonpx vp-update
pnpm dlx vp-update
@@ -106,7 +106,7 @@ mkdir foo
deno run -A vp-update
- npm yarn pnpmmkdir foo
+
npm yarn pnpmmkdir foo
mkdir foo
@@ -121,13 +121,13 @@ mkdir foo
`;
exports[`npmToPlugin > should work width options: { tabs: [npm, yarn, pnpm] } 1`] = `
-" npm yarn pnpm
+" npm yarn pnpm
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -136,7 +136,7 @@ exports[`npmToPlugin > should work width options: { tabs: [npm, yarn, pnpm] } 1`
pnpm install
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -145,7 +145,7 @@ exports[`npmToPlugin > should work width options: { tabs: [npm, yarn, pnpm] } 1`
pnpm install
- npm yarn pnpmnpm install
+
npm yarn pnpmnpm install
yarn
@@ -154,7 +154,7 @@ exports[`npmToPlugin > should work width options: { tabs: [npm, yarn, pnpm] } 1`
pnpm install
- npm yarn pnpmcross-env NODE_ENV=production npm run docs
+
npm yarn pnpmcross-env NODE_ENV=production npm run docs
cross-env NODE_ENV=production yarn docs
@@ -163,7 +163,7 @@ exports[`npmToPlugin > should work width options: { tabs: [npm, yarn, pnpm] } 1`
cross-env NODE_ENV=production pnpm docs
- npm yarn pnpmnpm i -D package1 package2
+
npm yarn pnpmnpm i -D package1 package2
npm i --save-peer package3
npm run docs
@@ -178,7 +178,7 @@ pnpm add --save-peer package3
pnpm docs
- npm yarn pnpmnpm install && npm run docs
+
npm yarn pnpmnpm install && npm run docs
mkdir foo
@@ -190,7 +190,7 @@ mkdir foo
mkdir foo
- npm yarn pnpmnpm run docs -- --clean-cache --clean-temp
+
npm yarn pnpmnpm run docs -- --clean-cache --clean-temp
@@ -202,7 +202,7 @@ mkdir foo
- npm pnpm yarnnpm create vuepress-theme-plume@latest
+
npm pnpm yarnnpm create vuepress-theme-plume@latest
pnpm create vuepress-theme-plume@latest
@@ -211,7 +211,7 @@ mkdir foo
yarn create vuepress-theme-plume@latest
- npm pnpm yarn bun denonpx vp-update
+
npm pnpm yarn bun denonpx vp-update
pnpm dlx vp-update
@@ -226,7 +226,7 @@ mkdir foo
deno run -A vp-update
- npm yarn pnpmmkdir foo
+
npm yarn pnpmmkdir foo
mkdir foo
@@ -241,13 +241,13 @@ mkdir foo
`;
exports[`npmToPlugin > should work with default options 1`] = `
-" npm pnpm yarn
+" npm pnpm yarn
- npm pnpm yarnnpm install
+
npm pnpm yarnnpm install
pnpm install
@@ -256,7 +256,7 @@ exports[`npmToPlugin > should work with default options 1`] = `
yarn
- npm pnpm yarnnpm install
+
npm pnpm yarnnpm install
pnpm install
@@ -265,7 +265,7 @@ exports[`npmToPlugin > should work with default options 1`] = `
yarn
- npm pnpm yarnnpm install
+
npm pnpm yarnnpm install
pnpm install
@@ -274,7 +274,7 @@ exports[`npmToPlugin > should work with default options 1`] = `
yarn
- npm pnpm yarncross-env NODE_ENV=production npm run docs
+
npm pnpm yarncross-env NODE_ENV=production npm run docs
cross-env NODE_ENV=production pnpm docs
@@ -283,7 +283,7 @@ exports[`npmToPlugin > should work with default options 1`] = `
cross-env NODE_ENV=production yarn docs
- npm pnpm yarnnpm i -D package1 package2
+
npm pnpm yarnnpm i -D package1 package2
npm i --save-peer package3
npm run docs
@@ -298,7 +298,7 @@ yarn add --peer package3
yarn docs
- npm pnpm yarnnpm install && npm run docs
+
npm pnpm yarnnpm install && npm run docs
mkdir foo
@@ -310,7 +310,7 @@ mkdir foo
mkdir foo
- npm pnpm yarnnpm run docs -- --clean-cache --clean-temp
+
npm pnpm yarnnpm run docs -- --clean-cache --clean-temp
@@ -322,7 +322,7 @@ mkdir foo
- npm pnpm yarnnpm create vuepress-theme-plume@latest
+
npm pnpm yarnnpm create vuepress-theme-plume@latest
pnpm create vuepress-theme-plume@latest
@@ -331,7 +331,7 @@ mkdir foo
yarn create vuepress-theme-plume@latest
- npm pnpm yarn bun denonpx vp-update
+
npm pnpm yarn bun denonpx vp-update
pnpm dlx vp-update
@@ -346,7 +346,7 @@ mkdir foo
deno run -A vp-update
- npm pnpm yarnmkdir foo
+
npm pnpm yarnmkdir foo
mkdir foo
diff --git a/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
index 1fc02a58..b476b3b8 100644
--- a/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
+++ b/plugins/plugin-md-power/__test__/__snapshots__/timelinePlugin.spec.ts.snap
@@ -16,7 +16,7 @@ exports[`timeline > timelinePlugin() > should work 1`] = `
这是标题这是内容
- 这是标题这是内容
+ 这是标题这是内容
这是标题这是内容
这是标题这是内容
这是标题这是内容
diff --git a/plugins/plugin-md-power/__test__/iconsPlugin.spec.ts b/plugins/plugin-md-power/__test__/iconsPlugin.spec.ts
index b2ee7234..ea80a23c 100644
--- a/plugins/plugin-md-power/__test__/iconsPlugin.spec.ts
+++ b/plugins/plugin-md-power/__test__/iconsPlugin.spec.ts
@@ -1,39 +1,58 @@
import MarkdownIt from 'markdown-it'
import { describe, expect, it } from 'vitest'
-import { iconPlugin } from '../src/node/inline/icons.js'
+import { iconPlugin } from '../src/node/icon/icon.js'
-describe('iconsPlugin', () => {
- it('should work', () => {
+describe('iconPlugin', () => {
+ it('should work with default', () => {
const md = MarkdownIt().use(iconPlugin)
- expect(md.render('::mdi:11::')).toMatchSnapshot()
- expect(md.render('**strong** ::mdi:11:: ::mdi:11::')).toMatchSnapshot()
- expect(md.render('**strong**\n::mdi:11::\n ::mdi:11::')).toMatchSnapshot()
+ expect(md.render('::mdi:11::')).toContain(' ')
+ expect(md.render('::mdi:11 =32px::')).toContain(' ')
+ expect(md.render('::mdi:11 /#fff::')).toContain(' ')
+ expect(md.render('::mdi:11 =32px /#fff::')).toContain(' ')
+ expect(md.render('::mdi:11 =32px /#fff fa data-fa-transform="shrink-8::"'))
+ .toContain(' ')
+
+ expect(md.render('::iconify mdi:11::')).toContain(' ')
})
- it('should work with options', () => {
+ it('should work with options -> { size, color }', () => {
const md = MarkdownIt().use(iconPlugin, { size: '1.25em', color: '#ccc' })
- expect(md.render('::mdi:11::')).toMatchSnapshot()
- expect(md.render('**strong** ::mdi:11:: ::mdi:11::')).toMatchSnapshot()
+ expect(md.render('::mdi:11::')).toContain(' ')
+ expect(md.render('::mdi:11 =32px::')).toContain(' ')
+ expect(md.render('::mdi:11 /#fff::')).toContain(' ')
+ expect(md.render('::mdi:11 =32px /#fff::')).toContain(' ')
})
- it('should work with single icon options', () => {
- const md = MarkdownIt().use(iconPlugin)
+ it('should work with options -> { provider: "iconify", prefix } ', () => {
+ const md = MarkdownIt().use(iconPlugin, { prefix: 'mdi' })
- expect(md.render('::mdi:11 =36px::')).toMatchSnapshot()
- expect(md.render('::mdi:11 =32px /#eee::')).toMatchSnapshot()
+ expect(md.render('::11::')).toMatchSnapshot()
+ expect(md.render('::11 =32px /#eee::')).toMatchSnapshot()
expect(md.render('::mdi:11 /#eee::')).toMatchSnapshot()
- expect(md.render('::mdi:11 =32px/::')).toMatchSnapshot()
- expect(md.render('::mdi:11 /::')).toMatchSnapshot()
+ expect(md.render('::fas:11 =32px/::')).toMatchSnapshot()
+ })
- const md2 = MarkdownIt().use(iconPlugin, { size: '1.25em', color: '#ccc' })
- expect(md2.render('::mdi:11::')).toMatchSnapshot()
- expect(md2.render('::mdi:11 =36px::')).toMatchSnapshot()
- expect(md2.render('::mdi:11 =32px/#eee::')).toMatchSnapshot()
- expect(md2.render('::mdi:11 /#eee::')).toMatchSnapshot()
- expect(md2.render('::mdi:11 =32px/::')).toMatchSnapshot()
- expect(md2.render('::mdi:11 /::')).toMatchSnapshot()
+ it('should work with options -> { provider: "iconfont", prefix } ', () => {
+ const md = MarkdownIt().use(iconPlugin, { provider: 'iconfont', prefix: 'iconfont icon-' })
+
+ expect(md.render('::home::')).toMatchSnapshot()
+ expect(md.render('::home =32px::')).toMatchSnapshot()
+ expect(md.render('::home /#eee::')).toMatchSnapshot()
+ expect(md.render('::home =32px /#eee::')).toMatchSnapshot()
+ })
+
+ it('should work with options -> { provider: "fontawesome", prefix } ', () => {
+ const md = MarkdownIt().use(iconPlugin, { provider: 'fontawesome', prefix: 'iconfont icon-' })
+
+ expect(md.render('::home::')).toMatchSnapshot()
+ expect(md.render('::home =32px::')).toMatchSnapshot()
+ expect(md.render('::home /#eee::')).toMatchSnapshot()
+ expect(md.render('::home =32px /#eee::')).toMatchSnapshot()
+
+ expect(md.render('::fas:home::')).toMatchSnapshot()
+ expect(md.render('::fas:home 2xl data-fa-transform="shrink-8"::')).toMatchSnapshot()
})
it('should not work with invalid icon', () => {
@@ -42,7 +61,6 @@ describe('iconsPlugin', () => {
expect(md.render(':: mdi:11 ::')).toMatchSnapshot()
expect(md.render('::::')).toMatchSnapshot()
expect(md.render('::]&')).toMatchSnapshot()
- expect(md.render('::::')).toMatchSnapshot()
expect(md.render('::mdi:11')).toMatchSnapshot()
})
})
diff --git a/plugins/plugin-md-power/src/client/options.ts b/plugins/plugin-md-power/src/client/options.ts
index dd8bcb3e..cf59fcdf 100644
--- a/plugins/plugin-md-power/src/client/options.ts
+++ b/plugins/plugin-md-power/src/client/options.ts
@@ -1,10 +1,5 @@
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
-declare const __MD_POWER_INJECT_OPTIONS__: MarkdownPowerPluginOptions
-declare const __MD_POWER_DASHJS_INSTALLED__: boolean
-declare const __MD_POWER_HLSJS_INSTALLED__: boolean
-declare const __MD_POWER_MPEGTSJS_INSTALLED__: boolean
-
export const pluginOptions: MarkdownPowerPluginOptions = __MD_POWER_INJECT_OPTIONS__
export const installed: {
diff --git a/plugins/plugin-md-power/src/client/shim.d.ts b/plugins/plugin-md-power/src/client/shim.d.ts
index 336bb25d..ce27319b 100644
--- a/plugins/plugin-md-power/src/client/shim.d.ts
+++ b/plugins/plugin-md-power/src/client/shim.d.ts
@@ -12,3 +12,12 @@ declare module '@internal/md-power/replEditorData' {
const res: ReplEditorData
export default res
}
+
+declare global {
+
+ const __MD_POWER_INJECT_OPTIONS__: MarkdownPowerPluginOptions
+ const __MD_POWER_DASHJS_INSTALLED__: boolean
+ const __MD_POWER_HLSJS_INSTALLED__: boolean
+ const __MD_POWER_MPEGTSJS_INSTALLED__: boolean
+
+}
diff --git a/plugins/plugin-md-power/src/node/container/codeTabs.ts b/plugins/plugin-md-power/src/node/container/codeTabs.ts
index 7352a03b..0c69469e 100644
--- a/plugins/plugin-md-power/src/node/container/codeTabs.ts
+++ b/plugins/plugin-md-power/src/node/container/codeTabs.ts
@@ -52,7 +52,7 @@ export const codeTabs: PluginWithOptions = (md, options: CodeTa
const titlesContent = titles.map((title, index) => {
const icon = getIcon(title)
- return `${icon ? ` ` : ''}${title}`
+ return `${icon ? ` ` : ''}${title}`
}).join('')
return `${titlesContent}`
diff --git a/plugins/plugin-md-power/src/node/container/codeTree.ts b/plugins/plugin-md-power/src/node/container/codeTree.ts
index 9d1a86dd..008f33a0 100644
--- a/plugins/plugin-md-power/src/node/container/codeTree.ts
+++ b/plugins/plugin-md-power/src/node/container/codeTree.ts
@@ -129,7 +129,7 @@ export function codeTreePlugin(md: Markdown, app: App, options: CodeTreeOptions
filepath: node.filepath,
}
return `
-
+
${node.children?.length ? renderFileTree(node.children, mode) : ''}
`
})
diff --git a/plugins/plugin-md-power/src/node/container/fileTree.ts b/plugins/plugin-md-power/src/node/container/fileTree.ts
index b3ad34ee..ab6cd24c 100644
--- a/plugins/plugin-md-power/src/node/container/fileTree.ts
+++ b/plugins/plugin-md-power/src/node/container/fileTree.ts
@@ -117,7 +117,7 @@ export function fileTreePlugin(md: Markdown, options: FileTreeOptions = {}): voi
? `${md.renderInline(comment.replaceAll('#', '\#'))}`
: ''
const renderedIcon = !isOmit
- ? ` `
+ ? ` `
: ''
const props: FileTreeNodeProps = {
expanded: nodeType === 'folder' ? expanded : false,
diff --git a/plugins/plugin-md-power/src/node/container/timeline.ts b/plugins/plugin-md-power/src/node/container/timeline.ts
index cc2c8c85..5ec03819 100644
--- a/plugins/plugin-md-power/src/node/container/timeline.ts
+++ b/plugins/plugin-md-power/src/node/container/timeline.ts
@@ -57,7 +57,7 @@ export function timelinePlugin(md: Markdown): void {
const attrs = token.meta as TimelineItemMeta
attrs.card ??= undefined
const icon = attrs.icon
- return `${icon ? ` ` : ''}`
+ return `${icon ? ` ` : ''}`
}
md.renderer.rules.timeline_item_close = () => ' '
diff --git a/plugins/plugin-md-power/src/node/icon/README.md b/plugins/plugin-md-power/src/node/icon/README.md
new file mode 100644
index 00000000..d2a29b88
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/README.md
@@ -0,0 +1,4 @@
+# 图标插件
+
+- 语法支持
+- 其他跟图标关联的功能增强
diff --git a/plugins/plugin-md-power/src/node/icon/createIconRule.ts b/plugins/plugin-md-power/src/node/icon/createIconRule.ts
new file mode 100644
index 00000000..a06d2d07
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/createIconRule.ts
@@ -0,0 +1,81 @@
+import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
+
+export function createIconRule(
+ [l1, l2, r1, r2]: readonly [number, number, number, number],
+ deprecated?: boolean,
+): RuleInline {
+ return (state, silent) => {
+ let found = false
+ const max = state.posMax
+ const start = state.pos
+
+ // ::xxx
+ // ^^
+ if (
+ state.src.charCodeAt(start) !== l1
+ || state.src.charCodeAt(start + 1) !== l2
+ ) {
+ return false
+ }
+
+ const next = state.src.charCodeAt(start + 2)
+
+ // :: xxx | :::xxx
+ // ^ | ^
+ if (next === 0x20 || next === 0x3A)
+ return false
+
+ /* istanbul ignore if -- @preserve */
+ if (silent)
+ return false
+
+ // ::::
+ if (max - start < 5)
+ return false
+
+ state.pos = start + 2
+
+ while (state.pos < max) {
+ // ::xxx::
+ // ^^
+ if (
+ state.src.charCodeAt(state.pos) === r1
+ && state.src.charCodeAt(state.pos + 1) === r2
+ ) {
+ found = true
+ break
+ }
+
+ state.md.inline.skipToken(state)
+ }
+
+ if (
+ !found
+ || start + 2 === state.pos
+ // ::xxx ::
+ // ^
+ || state.src.charCodeAt(state.pos - 1) === 0x20
+ ) {
+ state.pos = start
+
+ return false
+ }
+
+ const info = state.src.slice(start + 2, state.pos)
+
+ // found
+ state.posMax = state.pos
+ state.pos = start + 2
+
+ const icon = state.push('icon', 'i', 0)
+
+ icon.markup = '::'
+ icon.content = info
+ icon.meta = { deprecated }
+
+ state.pos = state.posMax + 2
+ state.posMax = max
+
+ return true
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/icon/icon.ts b/plugins/plugin-md-power/src/node/icon/icon.ts
new file mode 100644
index 00000000..a32fb7b1
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/icon.ts
@@ -0,0 +1,50 @@
+import type { PluginWithOptions } from 'markdown-it'
+import type { MarkdownEnv } from 'vuepress/markdown'
+import type { IconOptions } from '../../shared/index.js'
+import { colors } from 'vuepress/utils'
+import { stringifyAttrs } from '../utils/stringifyAttrs.js'
+import { createIconRule } from './createIconRule.js'
+import { resolveIcon } from './resolveIcon.js'
+
+function iconRender(content: string, options: IconOptions): string {
+ const icon = resolveIcon(content, options)
+ return ` `
+}
+
+export const iconPlugin: PluginWithOptions = (md, options = {}) => {
+ /**
+ * ::collect:icon_name =size /color::
+ */
+ md.inline.ruler.before(
+ 'link',
+ 'icon',
+ // : : : :
+ createIconRule([0x3A, 0x3A, 0x3A, 0x3A]),
+ )
+ /**
+ * :[collect:icon_name size/color]:
+ * @deprecated
+ */
+ md.inline.ruler.before(
+ 'link',
+ 'icon_deprecated',
+ // : [ ] :
+ createIconRule([0x3A, 0x5B, 0x5D, 0x3A], true),
+ )
+
+ md.renderer.rules.icon = (tokens, idx, _, env: MarkdownEnv) => {
+ const { content, meta } = tokens[idx]
+ let icon = content
+
+ /* istanbul ignore if -- @preserve */
+ if (meta.deprecated) {
+ const [name, opt = ''] = content.split(' ')
+ const [size, color] = opt.trim().split('/')
+ icon = `${name}${size ? ` =${size}` : ''}${color ? ` /${color}` : ''}`
+
+ console.warn(`The icon syntax of \`${colors.yellow(`:[${content}]:`)}\` is deprecated, please use \`${colors.green(`::${icon}::`)}\` instead. (${colors.gray(env.filePathRelative || env.filePath)})`)
+ }
+
+ return iconRender(icon, options)
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/icon/index.ts b/plugins/plugin-md-power/src/node/icon/index.ts
new file mode 100644
index 00000000..53ebb870
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/index.ts
@@ -0,0 +1,46 @@
+/**
+ * # Icon
+ *
+ * ## syntax
+ *
+ * ::name::
+ * ::provide name::
+ * ::provide name =size /color extra::
+ *
+ * ## options
+ *
+ * - provide: iconify | iconfont | fontawesome
+ * - prefix:
+ * - iconify: collect:name - prefix = "collect"
+ * - iconfont: iconfont icon-name - prefix = "iconfont icon-"
+ * - fontawesome: fa-solid fa-name -> fas:name - prefix = "fas"
+ *
+ * - assets: url[]
+ *
+ * ## iconify
+ *
+ * - full syntax
+ * ::fluent-mdl2:toggle-filled::
+ * ::fluent-mdl2:toggle-filled =128px /#fff::
+ *
+ * - prefix: fluent-mdl2
+ * ::toggle-filled::
+ * ::toggle-filled =128px /#fff::
+ *
+ * ### iconfont
+ *
+ * ::name::
+ * ::name =size /color::
+ *
+ * ### fontawesome
+ *
+ * ::fa-solid:name:: -> ::fas:name:: -> ::s:name:: -> ::name::
+ * ::fa-brands:name:: -> ::fab:name:: -> ::b:name::
+ * ::fa-regular:name:: -> ::far:name:: -> ::r:name::
+ *
+ * ::name fa-sm::
+ *
+ * @deprecated :[fluent-mdl2:toggle-filled 128px/#fff]: 此语法已废弃
+ */
+export * from './icon.js'
+export * from './prepareIcon.js'
diff --git a/plugins/plugin-md-power/src/node/icon/prepareIcon.ts b/plugins/plugin-md-power/src/node/icon/prepareIcon.ts
new file mode 100644
index 00000000..91dce054
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/prepareIcon.ts
@@ -0,0 +1,88 @@
+import type { IconOptions } from '../../shared/index.js'
+import { notNullish, toArray, uniqueBy } from '@pengzhanbo/utils'
+import { isLinkAbsolute } from '@vuepress/helper'
+import { isLinkHttp } from 'vuepress/shared'
+
+interface AssetInfo {
+ type: 'style' | 'script'
+ link: string
+ provide?: string
+}
+
+function getFontAwesomeCDNLink(type: string): string {
+ return `https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6/js/${type}.min.js`
+}
+
+export function prepareIcon(
+ imports: Set,
+ options: IconOptions = {},
+): string {
+ const setupContent: string[] = []
+ const assets: AssetInfo[] = []
+
+ if (options.provider === 'iconfont') {
+ assets.push(
+ ...toArray(options.assets)
+ .map(asset => normalizeAsset(asset))
+ .filter(notNullish),
+ )
+ }
+ else if (options.provider === 'fontawesome') {
+ assets.push(...toArray(options.assets || 'fontawesome').map((asset) => {
+ if (asset === 'fontawesome') {
+ return ['solid', 'regular', 'fontawesome']
+ .map(getFontAwesomeCDNLink)
+ .map(asset => normalizeAsset(asset, 'fontawesome'))
+ }
+ if (asset === 'fontawesome-with-brands') {
+ return normalizeAsset(getFontAwesomeCDNLink('brands'), 'fontawesome')
+ }
+ return null
+ }).flat().filter(notNullish))
+ }
+ let hasStyle = false
+ let hasScript = false
+
+ for (const asset of uniqueBy(assets, (a, b) => a.link === b.link)) {
+ if (asset.type === 'style') {
+ hasStyle = true
+ setupContent.push(`useStyleTag('@import url("${asset.link}");')`)
+ }
+ else if (asset.type === 'script') {
+ hasScript = true
+ setupContent.push(asset.provide === 'fontawesome'
+ ? `useScriptTag("${asset.link}", () => {}, { attrs: { "data-auto-replace-svg": "nest" } })`
+ : `useScriptTag("${asset.link}")`,
+ )
+ }
+ }
+ if (hasScript || hasStyle) {
+ const exports: string[] = []
+ if (hasScript)
+ exports.push('useScriptTag')
+ if (hasStyle)
+ exports.push('useStyleTag')
+ imports.add(`import { ${exports.join(', ')} } from '@vueuse/core'`)
+ }
+
+ return setupContent.join('\n ')
+}
+
+function normalizeAsset(asset: string, provide?: string): AssetInfo | null {
+ const link = normalizeLink(asset)
+ if (asset.endsWith('.js')) {
+ return { type: 'script', link, provide }
+ }
+ if (asset.endsWith('.css')) {
+ return { type: 'style', link, provide }
+ }
+ console.error(`[vuepress:icon] Can not recognize icon link: "${asset}"`)
+ return null
+}
+
+function normalizeLink(link: string): string {
+ if (isLinkHttp(link) || isLinkAbsolute(link))
+ return link
+
+ return `//${link}`
+}
diff --git a/plugins/plugin-md-power/src/node/icon/resolveIcon.ts b/plugins/plugin-md-power/src/node/icon/resolveIcon.ts
new file mode 100644
index 00000000..a9b8c2d7
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/icon/resolveIcon.ts
@@ -0,0 +1,57 @@
+import type { IconOptions } from '../../shared/index.js'
+import { kebabCase, omit } from '@pengzhanbo/utils'
+import { resolveAttrs } from '../utils/resolveAttrs.js'
+
+export interface ResolvedIcon {
+ provider: Exclude
+ size?: string | number
+ color?: string
+ name: string
+ extra?: string
+}
+
+const RE_SIZE = /(?<=\s|^)=(.+?)(?:\s|$)/
+const RE_COLOR = /(?<=\s|^)\/(.+?)(?:\s|$)/
+const RE_PROVIDER = /^(iconify|iconfont|fontawesome)\s+/
+const RE_EXTRA_KEY = /(?:^|-)\d-/g
+
+export function resolveIcon(content: string, options: IconOptions): ResolvedIcon {
+ let size = options.size
+ let color = options.color
+ let provider = options.provider || 'iconify'
+
+ content = content
+ .replace(RE_PROVIDER, (_, p) => {
+ provider = p
+ return ''
+ })
+ .replace(RE_SIZE, (_, s) => {
+ size = s
+ return ''
+ })
+ .replace(RE_COLOR, (_, c) => {
+ color = c
+ return ''
+ })
+ .trim()
+
+ const index = content.indexOf(' ')
+ const name = index === -1 ? content : content.slice(0, index)
+ const extra = index === -1 ? '' : content.slice(index + 1)
+ const props = { provider, size, color, name }
+ if (!extra) {
+ return props
+ }
+
+ const { attrs } = resolveAttrs(extra)
+ const info: string[] = []
+ const excludes: string[] = []
+
+ for (const key in attrs) {
+ if (attrs[key] === true) {
+ excludes.push(key)
+ info.push(kebabCase(key).replace(RE_EXTRA_KEY, m => `${m.slice(0, -1)}`))
+ }
+ }
+ return { ...props, extra: info.join(' '), ...omit(attrs, excludes) }
+}
diff --git a/plugins/plugin-md-power/src/node/inline/icons.ts b/plugins/plugin-md-power/src/node/inline/icons.ts
deleted file mode 100644
index 9d4ab5cc..00000000
--- a/plugins/plugin-md-power/src/node/inline/icons.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-/**
- * ::fluent-mdl2:toggle-filled::
- * ::fluent-mdl2:toggle-filled /#fff::
- * ::fluent-mdl2:toggle-filled =128px /#fff::
- *
- * @deprecated :[fluent-mdl2:toggle-filled 128px/#fff]: 此语法已废弃
- */
-
-import type { PluginWithOptions } from 'markdown-it'
-import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
-import type { MarkdownEnv } from 'vuepress/markdown'
-import type { IconsOptions } from '../../shared/index.js'
-import { colors } from 'vuepress/utils'
-import { stringifyAttrs } from '../utils/stringifyAttrs.js'
-
-function createIconRule(
- [l1, l2, r1, r2]: readonly [number, number, number, number],
- deprecated?: boolean,
-): RuleInline {
- return (state, silent) => {
- let found = false
- const max = state.posMax
- const start = state.pos
-
- // ::xxx
- // ^^
- if (
- state.src.charCodeAt(start) !== l1
- || state.src.charCodeAt(start + 1) !== l2
- ) {
- return false
- }
-
- const next = state.src.charCodeAt(start + 2)
-
- // :: xxx | :::xxx
- // ^ | ^
- if (next === 0x20 || next === 0x3A)
- return false
-
- /* istanbul ignore if -- @preserve */
- if (silent)
- return false
-
- // ::::
- if (max - start < 5)
- return false
-
- state.pos = start + 2
-
- while (state.pos < max) {
- // ::xxx::
- // ^^
- if (
- state.src.charCodeAt(state.pos) === r1
- && state.src.charCodeAt(state.pos + 1) === r2
- ) {
- found = true
- break
- }
-
- state.md.inline.skipToken(state)
- }
-
- if (
- !found
- || start + 2 === state.pos
- // ::xxx ::
- // ^
- || state.src.charCodeAt(state.pos - 1) === 0x20
- ) {
- state.pos = start
-
- return false
- }
-
- const info = state.src.slice(start + 2, state.pos)
-
- // found
- state.posMax = state.pos
- state.pos = start + 2
-
- const icon = state.push('icon', 'i', 0)
-
- icon.markup = '::'
- icon.content = info
- icon.meta = { deprecated }
-
- state.pos = state.posMax + 2
- state.posMax = max
-
- return true
- }
-}
-
-const RE_SIZE = /(?<=\s|^)=(.+?)(?:\s|$)/
-const RE_COLOR = /(?<=\s|^)\/(.+?)(?:\s|$)/
-
-function iconRender(content: string, options: IconsOptions): string {
- let size = options.size
- let color = options.color
-
- content = content
- .replace(RE_SIZE, (_, s) => {
- size = s
- return ''
- })
- .replace(RE_COLOR, (_, c) => {
- color = c
- return ''
- })
- .trim()
-
- const [name, ...extra] = content.split(/\s+/)
-
- return ` `
-}
-
-export const iconPlugin: PluginWithOptions = (md, options = {}) => {
- /**
- * ::collect:icon_name =size /color::
- */
- md.inline.ruler.before(
- 'link',
- 'icon',
- // : : : :
- createIconRule([0x3A, 0x3A, 0x3A, 0x3A]),
- )
- /**
- * :[collect:icon_name size/color]:
- * @deprecated
- */
- md.inline.ruler.before(
- 'link',
- 'icon_deprecated',
- // : [ ] :
- createIconRule([0x3A, 0x5B, 0x5D, 0x3A], true),
- )
-
- md.renderer.rules.icon = (tokens, idx, _, env: MarkdownEnv) => {
- const { content, meta } = tokens[idx]
- let icon = content
-
- /* istanbul ignore if -- @preserve */
- if (meta.deprecated) {
- const [name, opt = ''] = content.split(' ')
- const [size, color] = opt.trim().split('/')
- icon = `${name}${size ? ` =${size}` : ''}${color ? ` /${color}` : ''}`
-
- console.warn(`The icon syntax of \`${colors.yellow(`:[${content}]:`)}\` is deprecated, please use \`${colors.green(`::${icon}::`)}\` instead. (${colors.gray(env.filePathRelative || env.filePath)})`)
- }
-
- return iconRender(icon, options)
- }
-}
diff --git a/plugins/plugin-md-power/src/node/inline/index.ts b/plugins/plugin-md-power/src/node/inline/index.ts
index 6da53684..9ede681e 100644
--- a/plugins/plugin-md-power/src/node/inline/index.ts
+++ b/plugins/plugin-md-power/src/node/inline/index.ts
@@ -9,7 +9,6 @@ import { tasklist } from '@mdit/plugin-tasklist'
import { isPlainObject } from '@vuepress/helper'
import { abbrPlugin } from './abbr.js'
import { annotationPlugin } from './annotation.js'
-import { iconPlugin } from './icons.js'
import { plotPlugin } from './plot.js'
export function inlineSyntaxPlugin(
@@ -41,11 +40,6 @@ export function inlineSyntaxPlugin(
md.use(abbrPlugin)
}
- if (options.icons) {
- // ::collect:name::
- md.use(iconPlugin, isPlainObject(options.icons) ? options.icons : {})
- }
-
if (
options.plot === true
|| (isPlainObject(options.plot) && options.plot.tag !== false)
diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts
index a0c0fb42..546d5c1f 100644
--- a/plugins/plugin-md-power/src/node/plugin.ts
+++ b/plugins/plugin-md-power/src/node/plugin.ts
@@ -1,15 +1,17 @@
import type { Plugin } from 'vuepress/core'
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
+import { isPlainObject } from '@pengzhanbo/utils'
import { addViteOptimizeDepsInclude } from '@vuepress/helper'
-import { isPackageExists } from 'local-pkg'
import { extendsPageWithCodeTree } from './container/codeTree.js'
import { containerPlugin } from './container/index.js'
import { demoPlugin, demoWatcher, extendsPageWithDemo, waitDemoRender } from './demo/index.js'
import { embedSyntaxPlugin } from './embed/index.js'
import { docsTitlePlugin } from './enhance/docsTitle.js'
import { imageSizePlugin } from './enhance/imageSize.js'
+import { iconPlugin } from './icon/index.js'
import { inlineSyntaxPlugin } from './inline/index.js'
import { prepareConfigFile } from './prepareConfigFile.js'
+import { provideData } from './provideData.js'
export function markdownPowerPlugin(
options: MarkdownPowerPluginOptions = {},
@@ -19,12 +21,7 @@ export function markdownPowerPlugin(
clientConfigFile: app => prepareConfigFile(app, options),
- define: {
- __MD_POWER_INJECT_OPTIONS__: options,
- __MD_POWER_DASHJS_INSTALLED__: isPackageExists('dashjs'),
- __MD_POWER_HLSJS_INSTALLED__: isPackageExists('hls.js'),
- __MD_POWER_MPEGTSJS_INSTALLED__: isPackageExists('mpegts.js'),
- },
+ define: provideData(options),
extendsBundlerOptions(bundlerOptions, app) {
if (options.repl) {
@@ -47,6 +44,7 @@ export function markdownPowerPlugin(
docsTitlePlugin(md)
embedSyntaxPlugin(md, options)
inlineSyntaxPlugin(md, options)
+ iconPlugin(md, options.icon ?? (isPlainObject(options.icons) ? options.icons : {}))
if (options.demo)
demoPlugin(app, md)
diff --git a/plugins/plugin-md-power/src/node/prepareConfigFile.ts b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
index 9cd0aaf8..7bd6e7f7 100644
--- a/plugins/plugin-md-power/src/node/prepareConfigFile.ts
+++ b/plugins/plugin-md-power/src/node/prepareConfigFile.ts
@@ -2,6 +2,7 @@ import type { App } from 'vuepress/core'
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
import { ensureEndingSlash } from '@vuepress/helper'
import { getDirname, path } from 'vuepress/utils'
+import { prepareIcon } from './icon/index.js'
const { url: filepath } = import.meta
const __dirname = getDirname(filepath)
@@ -130,6 +131,8 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('VPField', VPField)`)
}
+ const setupIcon = prepareIcon(imports, options.icon)
+
return app.writeTemp(
'md-power/config.js',
`\
@@ -143,6 +146,9 @@ export default defineClientConfig({
${Array.from(enhances.values())
.map(item => ` ${item}`)
.join('\n')}
+ },
+ setup() {
+ ${setupIcon}
}
})
`,
diff --git a/plugins/plugin-md-power/src/node/provideData.ts b/plugins/plugin-md-power/src/node/provideData.ts
new file mode 100644
index 00000000..8b21ec70
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/provideData.ts
@@ -0,0 +1,19 @@
+import type { MarkdownPowerPluginOptions } from '../shared/index.js'
+import { isPackageExists } from 'local-pkg'
+
+export function provideData(options: MarkdownPowerPluginOptions): Record {
+ const mardownOptions = {
+ plot: options.plot,
+ pdf: options.pdf,
+ }
+ const icon = options.icon ?? { provider: 'iconify' }
+
+ return {
+ __MD_POWER_INJECT_OPTIONS__: mardownOptions,
+ __MD_POWER_DASHJS_INSTALLED__: isPackageExists('dashjs'),
+ __MD_POWER_HLSJS_INSTALLED__: isPackageExists('hls.js'),
+ __MD_POWER_MPEGTSJS_INSTALLED__: isPackageExists('mpegts.js'),
+ __MD_POWER_ICON_PROVIDER__: icon.provider || 'iconify',
+ __MD_POWER_ICON_PREFIX__: icon.prefix || '',
+ }
+}
diff --git a/plugins/plugin-md-power/src/shared/icon.ts b/plugins/plugin-md-power/src/shared/icon.ts
new file mode 100644
index 00000000..63f4f984
--- /dev/null
+++ b/plugins/plugin-md-power/src/shared/icon.ts
@@ -0,0 +1,87 @@
+export type IconOptions = IconifyProvider | IconFontProvider | FontAwesomeProvider
+
+export interface IconProviderBase {
+ /**
+ * The provider of the icon
+ *
+ * 图标提供商
+ * @default 'iconify'
+ */
+ provider?: 'iconify' | 'iconfont' | 'fontawesome'
+
+ /**
+ * The size of the icon
+ * @default '1em'
+ */
+ size?: string | number
+
+ /**
+ * The color of the icon
+ * @default 'currentColor'
+ */
+ color?: string
+}
+
+export interface IconFontProvider extends IconProviderBase {
+ provider?: 'iconfont'
+
+ /**
+ * The prefix of the iconfont
+ * @default 'iconfont icon-'
+ */
+ prefix?: string
+
+ /**
+ * The assets of the iconfont
+ */
+ assets?: IconAssetLink | IconAssetLink[]
+}
+
+export interface FontAwesomeProvider extends IconProviderBase {
+
+ provider?: 'fontawesome'
+
+ /**
+ * The prefix of the fontawesome icon
+ * @default 'fas'
+ */
+ prefix?: LiteralUnion
+
+ /**
+ * The assets of the fontawesome
+ * @default 'fontawesome'
+ */
+ assets?: FontAwesomeAssetBuiltIn | IconAssetLink | (IconAssetLink | FontAwesomeAssetBuiltIn)[]
+}
+
+export interface IconifyProvider extends IconProviderBase {
+ provider?: 'iconify'
+
+ /**
+ * The prefix of the icon
+ * @default ''
+ */
+ prefix?: LiteralUnion
+}
+
+export type FontAwesomeAssetBuiltIn = 'fontawesome' | 'fontawesome-with-brands'
+export type IconAssetLink = `//${string}` | `//${string}` | `https://${string}` | `http://${string}`
+
+export type FontAwesomePrefix =
+ | 'fas' | 's' // fa-solid fa-name
+ | 'far' | 'r' // fa-regular fa-name
+ | 'fal' | 'l' // fa-light fa-name
+ | 'fat' | 't' // fa-thin fa-name
+ | 'fads' | 'ds' // fa-duotone fa-solid fa-name
+ | 'fass' | 'ss' // fa-sharp fa-solid fa-name
+ | 'fasr' | 'sr' // fa-sharp fa-regular fa-name
+ | 'fasl' | 'sl' // fa-sharp fa-light fa-name
+ | 'fast' | 'st' // fa-sharp fa-thin fa-name
+ | 'fasds' | 'sds' // fa-sharp-duotone fa-solid fa-name
+ | 'fab' | 'b' // fa-brands fa-name
+
+export type IconifyPrefix = 'material-symbols' | 'material-symbols-light' | 'ic' | 'mdi' | 'mdi-light' | 'line-md' | 'solar' | 'tabler' | 'hugeicons' | 'mingcute' | 'ri' | 'mynaui' | 'iconamoon' | 'iconoir' | 'lucide' | 'lucide-lab' | 'uil' | 'tdesign' | 'si' | 'bx' | 'bxs' | 'majesticons' | 'gg' | 'flowbite' | 'basil' | 'pixelarticons' | 'pixel' | 'akar-icons' | 'ci' | 'proicons' | 'typcn' | 'meteor-icons' | 'prime' | 'circum' | 'fe' | 'eos-icons' | 'bitcoin-icons' | 'humbleicons' | 'uim' | 'uit' | 'uis' | 'gridicons' | 'mi' | 'cuida' | 'weui' | 'duo-icons' | 'svg-spinners' | 'lets-icons' | 'mage' | 'stash' | 'lineicons' | 'icon-park-outline' | 'icon-park-solid' | 'icon-park-twotone' | 'jam' | 'guidance' | 'carbon' | 'ion' | 'famicons' | 'ant-design' | 'lsicon' | 'gravity-ui' | 'cil' | 'ep' | 'charm' | 'quill' | 'bytesize' | 'bi' | 'rivet-icons' | 'nimbus' | 'formkit' | 'fluent' | 'ph' | 'teenyicons' | 'clarity' | 'ix' | 'octicon' | 'memory' | 'system-uicons' | 'radix-icons' | 'zondicons' | 'uiw' | 'maki' | 'codex' | 'ei' | 'heroicons' | 'pepicons-pop' | 'pepicons-print' | 'pepicons-pencil' | 'f7' | 'pajamas' | 'garden' | 'streamline' | 'fa6-solid' | 'fa6-regular' | 'picon' | 'ooui' | 'oui' | 'nrk' | 'qlementine-icons' | 'fluent-color' | 'icon-park' | 'marketeq' | 'vscode-icons' | 'codicon' | 'material-icon-theme' | 'file-icons' | 'devicon' | 'devicon-plain' | 'catppuccin' | 'skill-icons' | 'unjs' | 'simple-icons' | 'logos' | 'cib' | 'fa6-brands' | 'bxl' | 'nonicons' | 'arcticons' | 'cbi' | 'brandico' | 'entypo-social' | 'token' | 'token-branded' | 'cryptocurrency' | 'cryptocurrency-color' | 'openmoji' | 'twemoji' | 'noto' | 'fluent-emoji' | 'fluent-emoji-flat' | 'fluent-emoji-high-contrast' | 'noto-v1' | 'emojione' | 'emojione-monotone' | 'emojione-v1' | 'fxemoji' | 'streamline-emojis' | 'circle-flags' | 'flag' | 'flagpack' | 'cif' | 'gis' | 'map' | 'geo' | 'game-icons' | 'fad' | 'academicons' | 'wi' | 'meteocons' | 'healthicons' | 'medical-icon' | 'covid' | 'la' | 'eva' | 'dashicons' | 'flat-color-icons' | 'entypo' | 'foundation' | 'raphael' | 'icons8' | 'iwwa' | 'gala' | 'heroicons-outline' | 'heroicons-solid' | 'fa-solid' | 'fa-regular' | 'fa-brands' | 'fa' | 'fluent-mdl2' | 'fontisto' | 'icomoon-free' | 'subway' | 'oi' | 'wpf' | 'simple-line-icons' | 'et' | 'el' | 'vaadin' | 'grommet-icons' | 'whh' | 'si-glyph' | 'zmdi' | 'ls' | 'bpmn' | 'flat-ui' | 'vs' | 'topcoat' | 'il' | 'websymbol' | 'fontelico' | 'ps' | 'feather' | 'mono-icons' | 'pepicons'
+
+export type LiteralUnion =
+ | Union
+ | (Base & { zz_IGNORE_ME?: never })
diff --git a/plugins/plugin-md-power/src/shared/icons.ts b/plugins/plugin-md-power/src/shared/icons.ts
deleted file mode 100644
index 9c249ea2..00000000
--- a/plugins/plugin-md-power/src/shared/icons.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export interface IconsOptions {
- /**
- * The size of the icon
- * @default '1em'
- */
- size?: string | number
-
- /**
- * The color of the icon
- * @default 'currentColor'
- */
- color?: string
-}
diff --git a/plugins/plugin-md-power/src/shared/index.ts b/plugins/plugin-md-power/src/shared/index.ts
index 24ef127e..225c6017 100644
--- a/plugins/plugin-md-power/src/shared/index.ts
+++ b/plugins/plugin-md-power/src/shared/index.ts
@@ -4,7 +4,7 @@ export * from './codeSandbox.js'
export * from './codeTabs.js'
export * from './demo.js'
export * from './fileTree.js'
-export * from './icons.js'
+export * from './icon.js'
export * from './jsfiddle.js'
export * from './npmTo.js'
export * from './pdf.js'
diff --git a/plugins/plugin-md-power/src/shared/plugin.ts b/plugins/plugin-md-power/src/shared/plugin.ts
index ced08c89..7884bed7 100644
--- a/plugins/plugin-md-power/src/shared/plugin.ts
+++ b/plugins/plugin-md-power/src/shared/plugin.ts
@@ -2,7 +2,7 @@ import type { CanIUseOptions } from './caniuse.js'
import type { CodeTabsOptions } from './codeTabs.js'
import type { CodeTreeOptions } from './codeTree.js'
import type { FileTreeOptions } from './fileTree.js'
-import type { IconsOptions } from './icons.js'
+import type { IconOptions } from './icon.js'
import type { NpmToOptions } from './npmTo.js'
import type { PDFOptions } from './pdf.js'
import type { PlotOptions } from './plot.js'
@@ -40,14 +40,25 @@ export interface MarkdownPowerPluginOptions {
pdf?: boolean | PDFOptions
// new syntax
+ /**
+ * 是否启用 图标支持
+ * - iconify - `::collect:icon_name::` => ` `
+ * - iconfont - `::name::` => ``
+ * - fontawesome - `::fas:name::` => ``
+ *
+ * @default false
+ */
+ icon?: IconOptions
+
/**
* 是否启用 iconify 图标嵌入语法
*
* `::collect:icon_name::`
*
* @default false
+ * @deprecated use `icon` instead 该配置已弃用,请使用 `icon` 代替
*/
- icons?: boolean | IconsOptions
+ icons?: boolean | IconOptions
/**
* 是否启用 隐秘文本 语法
*
diff --git a/theme/src/client/components/VPIcon.vue b/theme/src/client/components/VPIcon.vue
index 11f680e9..8babca06 100644
--- a/theme/src/client/components/VPIcon.vue
+++ b/theme/src/client/components/VPIcon.vue
@@ -1,98 +1,85 @@
-
-
-
+
+
-
-
-
diff --git a/theme/src/client/components/VPIconFa.vue b/theme/src/client/components/VPIconFa.vue
new file mode 100644
index 00000000..2a574916
--- /dev/null
+++ b/theme/src/client/components/VPIconFa.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
diff --git a/theme/src/client/components/VPIconImage.vue b/theme/src/client/components/VPIconImage.vue
new file mode 100644
index 00000000..dd067bfe
--- /dev/null
+++ b/theme/src/client/components/VPIconImage.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
diff --git a/theme/src/client/components/VPIconfont.vue b/theme/src/client/components/VPIconfont.vue
new file mode 100644
index 00000000..586b73d5
--- /dev/null
+++ b/theme/src/client/components/VPIconfont.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/theme/src/client/components/VPIconify.vue b/theme/src/client/components/VPIconify.vue
index 36c2a1e5..e28e1d37 100644
--- a/theme/src/client/components/VPIconify.vue
+++ b/theme/src/client/components/VPIconify.vue
@@ -3,61 +3,63 @@ import type { IconifyIcon } from '@iconify/vue/offline'
import { loadIcon } from '@iconify/vue'
import { Icon as OfflineIcon } from '@iconify/vue/offline'
import { computed, ref, watch } from 'vue'
+import { useIconsData } from '../composables/index.js'
-const props = withDefaults(
- defineProps<{
- name?: string
- size?: string | number
- color?: string
- }>(),
- {
- name: '',
- size: '',
- color: '',
- },
-)
+const props = defineProps<{
+ name: string
+ size?: { width?: string, height?: string }
+ color?: string
+ prefix?: string
+ extra?: string
+}>()
const icon = ref(null)
const loaded = ref(false)
-async function loadIconComponent() {
+const iconsData = useIconsData()
+
+const iconName = computed(() => {
+ const name = props.name
+ if (name.includes(':'))
+ return name
+ return props.prefix ? `${props.prefix}:${name}` : name
+})
+
+const localIconName = computed(() => iconsData.value[iconName.value])
+
+async function loadRemoteIcon() {
if (icon.value)
return
- if (!__VUEPRESS_SSR__) {
- try {
- loaded.value = false
- icon.value = await loadIcon(props.name)
- }
- finally {
- loaded.value = true
- }
- }
- else {
- loaded.value = true
+ if (!localIconName.value) {
+ loaded.value = false
+ icon.value = await loadIcon(props.name)
}
+ loaded.value = true
}
-watch(() => props.name, loadIconComponent, { immediate: true })
-
-const size = computed(() => {
- const size = props.size || '1em'
- if (String(Number(size)) === size)
- return `${size}px`
-
- return size
-})
-const color = computed(() => props.color || 'currentColor')
+if (!__VUEPRESS_SSR__)
+ watch(() => props.name, loadRemoteIcon, { immediate: true })
-
-
+
+
+
diff --git a/theme/src/client/components/VPSidebarItem.vue b/theme/src/client/components/VPSidebarItem.vue
index fab794d2..b83c5f3a 100644
--- a/theme/src/client/components/VPSidebarItem.vue
+++ b/theme/src/client/components/VPSidebarItem.vue
@@ -288,6 +288,10 @@ function onCaretClick() {
margin: 0 0.25rem 0 0;
}
+.item :deep(.vp-icon.fontawesome) {
+ line-height: 1;
+}
+
.item:hover .caret {
color: var(--vp-c-text-2);
}
diff --git a/theme/src/client/styles/icons.css b/theme/src/client/styles/icons.css
index c09beee3..7082d5a4 100644
--- a/theme/src/client/styles/icons.css
+++ b/theme/src/client/styles/icons.css
@@ -1,6 +1,5 @@
[class^="vpi-"],
-[class*=" vpi-"],
-.vp-icon {
+[class*=" vpi-"] {
display: inline-block;
width: 1em;
height: 1em;
@@ -8,8 +7,7 @@
}
[class^="vpi-"].bg,
-[class*=" vpi-"].bg,
-.vp-icon.bg {
+[class*=" vpi-"].bg {
background-color: transparent;
background-image: var(--icon);
background-repeat: no-repeat;
@@ -17,8 +15,7 @@
}
[class^="vpi-"]:not(.bg),
-[class*=" vpi-"]:not(.bg),
-.vp-icon:not(.bg) {
+[class*=" vpi-"]:not(.bg) {
color: inherit;
background-color: currentcolor;
-webkit-mask: var(--icon) no-repeat;
diff --git a/theme/src/client/styles/utils.css b/theme/src/client/styles/utils.css
index 41ae252e..3322cec6 100644
--- a/theme/src/client/styles/utils.css
+++ b/theme/src/client/styles/utils.css
@@ -48,3 +48,70 @@
opacity: 0 !important;
transform: translateX(-10px) !important;
}
+
+/* ----------------- Read More Link ------------------ */
+.vp-doc a.read-more,
+.vp-doc a.readmore {
+ position: relative;
+ display: block;
+ padding: 8px 22px 8px calc(1.25em + 16px);
+ margin: 16px 0;
+ font-size: inherit;
+ font-size: 14px;
+ font-weight: inherit;
+ color: currentcolor;
+ text-decoration: none;
+ background-color: var(--vp-c-bg-safe);
+ border: dashed 1px var(--vp-c-divider);
+ border-radius: 8px;
+ transition: border-color var(--vp-t-color), background-color var(--vp-t-color);
+}
+
+.vp-doc a.read-more:hover,
+.vp-doc a.readmore:hover {
+ color: currentcolor;
+ background-color: var(--vp-c-bg-soft);
+ border: solid 1px var(--vp-c-brand-2);
+}
+
+.vp-doc a.read-more::before,
+.vp-doc a.readmore::before {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1M7 4H5v16h14V4h-5v9l-3.5-2L7 13z'/%3E%3C/svg%3E");
+
+ position: absolute;
+ top: 50%;
+ left: 16px;
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ color: var(--vp-c-brand-1);
+ vertical-align: middle;
+ content: "";
+ background-color: currentcolor;
+ -webkit-mask: var(--icon) no-repeat;
+ mask: var(--icon) no-repeat;
+ -webkit-mask-size: 100% 100%;
+ mask-size: 100% 100%;
+ transform: translateY(-50%);
+}
+
+.vp-doc a.read-more[href*="://"]::after,
+.vp-doc a.readmore[target=_blank]::after {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 14px !important;
+ height: 14px !important;
+ margin: 0 !important;
+ color: var(--vp-c-text-3) !important;
+}
+
+.vp-doc a.read-more[href*="://"]:hover::after,
+.vp-doc a.readmore[target=_blank]:hover::after {
+ color: var(--vp-c-brand-2) !important;
+}
+
+.vp-doc a.read-more :where(strong),
+.vp-doc a.readmore :where(strong) {
+ color: var(--vp-c-brand-1);
+}
diff --git a/theme/src/node/detector/fields.ts b/theme/src/node/detector/fields.ts
index cd6985a9..a724b8fa 100644
--- a/theme/src/node/detector/fields.ts
+++ b/theme/src/node/detector/fields.ts
@@ -52,7 +52,8 @@ export const MARKDOWN_POWER_FIELDS: (keyof MarkdownPowerPluginOptions)[] = [
'demo',
'fileTree',
'field',
- 'icons',
+ 'icons', // deprecated
+ 'icon',
'imageSize',
'jsfiddle',
'npmTo',
diff --git a/theme/src/node/plugins/code.ts b/theme/src/node/plugins/code.ts
index 86060661..7e16e0b4 100644
--- a/theme/src/node/plugins/code.ts
+++ b/theme/src/node/plugins/code.ts
@@ -43,7 +43,7 @@ export function codePlugins(pluginOptions: ThemeBuiltinPlugins): PluginConfig {
langs: uniq([...twoslash ? ['ts', 'js', 'vue', 'json', 'bash', 'sh'] : [], ...langs]),
codeBlockTitle: (title, code) => {
const icon = getIcon(title)
- return `${code}`
+ return `${code}`
},
twoslash: isPlainObject(twoslashOptions)
? {
diff --git a/theme/src/node/prepare/prepareIcons.ts b/theme/src/node/prepare/prepareIcons.ts
index 38a15670..4665dd71 100644
--- a/theme/src/node/prepare/prepareIcons.ts
+++ b/theme/src/node/prepare/prepareIcons.ts
@@ -1,4 +1,5 @@
import type { App, Page } from 'vuepress'
+import type { IconOptions } from 'vuepress-plugin-md-power'
import type { ThemeHomeConfig, ThemeNavItem, ThemeOptions, ThemeSidebar } from '../../shared/index.js'
import type { FsCache } from '../utils/index.js'
import { getIconContentCSS, getIconData } from '@iconify/utils'
@@ -22,6 +23,7 @@ const ICON_REGEXP = /<(?:VP)?(Icon|Card|LinkCard|Button)([^>]*)>/g
const ICON_NAME_REGEXP = /(?:name|icon|suffix-icon)="([^"]+)"/
const URL_CONTENT_REGEXP = /(url\([\s\S]+\))/
const ICONIFY_NAME = /^[\w-]+:[\w-]+$/
+
const JS_FILENAME = 'internal/iconify.js'
const CSS_FILENAME = 'internal/iconify.css'
@@ -32,12 +34,6 @@ let fsCache: FsCache | null = null
// { iconName: { className, content } }
const cache: IconDataMap = {}
-function isIconify(icon: any): icon is string {
- if (!icon || typeof icon !== 'string' || isLinkAbsolute(icon) || isLinkHttp(icon))
- return false
- return icon[0] !== '{' && ICONIFY_NAME.test(icon)
-}
-
export async function prepareIcons(app: App): Promise {
perf.mark('prepare:icons:total')
const options = getThemeConfig()
@@ -51,9 +47,11 @@ export async function prepareIcons(app: App): Promise {
}
perf.mark('prepare:pages:icons')
+
+ const iconOptions = options.markdown?.icon || {}
const iconList: string[] = []
- app.pages.forEach(page => iconList.push(...getIconsWithPage(page)))
- iconList.push(...getIconWithThemeConfig(options))
+ app.pages.forEach(page => iconList.push(...getIconsWithPage(page, iconOptions)))
+ iconList.push(...getIconWithThemeConfig(options, iconOptions))
const collectMap: CollectMap = {}
uniq(iconList).filter((icon) => {
@@ -108,31 +106,49 @@ export async function prepareIcons(app: App): Promise {
perf.log('prepare:icons:total')
}
-function getIconsWithPage(page: Page): string[] {
- const list = page.contentRendered
- .match(ICON_REGEXP)
- ?.map(match => match.match(ICON_NAME_REGEXP)?.[1])
- .filter(isIconify) as string[] || []
+function isIconify(icon: any): icon is string {
+ if (!icon || typeof icon !== 'string' || isLinkAbsolute(icon) || isLinkHttp(icon))
+ return false
+ return icon[0] !== '{' && ICONIFY_NAME.test(icon)
+}
+
+function withPrefix(icon: string, prefix?: string): string {
+ if (!prefix)
+ return icon
+ return icon.includes(':') ? icon : `${prefix}:${icon}`
+}
+
+function getIconsWithPage(page: Page, { provider = 'iconify', prefix }: IconOptions): string[] {
+ const list: string[] = []
+ const matches = page.contentRendered.match(ICON_REGEXP) || []
+ for (const matched of matches) {
+ if (provider === 'iconify' || matched.includes('provider="iconify"')) {
+ const icon = matched.match(ICON_NAME_REGEXP)?.[1]
+ if (isIconify(icon))
+ list.push(withPrefix(icon, prefix))
+ }
+ }
+
+ const addIcon = (icon: unknown): void => {
+ if (isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
+ list.push(withPrefix(icon.replace(/^iconify /, ''), prefix))
+ }
+ }
const fm = page.frontmatter
- if (fm.icon && isIconify(fm.icon)) {
- list.push(fm.icon)
- }
+ addIcon(fm.icon)
if ((fm.home || fm.pageLayout === 'home') && (fm.config as ThemeHomeConfig[])?.length) {
for (const config of (fm.config as ThemeHomeConfig[])) {
if (config.type === 'features' && config.features.length) {
for (const feature of config.features) {
- if (feature.icon && isIconify(feature.icon))
- list.push(feature.icon)
+ addIcon(feature.icon)
}
}
if (config.type === 'hero' && config.hero?.actions?.length) {
for (const action of config.hero.actions) {
- if (action.icon && isIconify(action.icon))
- list.push(action.icon)
- if (action.suffixIcon && isIconify(action.suffixIcon))
- list.push(action.suffixIcon)
+ addIcon(action.icon)
+ addIcon(action.suffixIcon)
}
}
}
@@ -141,7 +157,7 @@ function getIconsWithPage(page: Page): string[] {
return list
}
-function getIconWithThemeConfig(options: ThemeOptions): string[] {
+function getIconWithThemeConfig(options: ThemeOptions, { provider = 'iconify', prefix }: IconOptions): string[] {
const list: string[] = []
// navbar notes sidebar
const locales = options.locales || {}
@@ -159,7 +175,13 @@ function getIconWithThemeConfig(options: ThemeOptions): string[] {
sidebarList.forEach(sidebar => list.push(...getIconWithSidebar(sidebar)))
})
- return list.filter(isIconify)
+ const addIcon = (icon: unknown): string | void => {
+ if (isIconify(icon) && (provider === 'iconify' || icon.startsWith('iconify'))) {
+ return withPrefix(icon.replace(/^iconify /, ''), prefix)
+ }
+ }
+
+ return list.map(addIcon).filter(Boolean) as string[]
}
function getIconWithNavbar(navbar: ThemeNavItem[]): string[] {