chore: tweak

This commit is contained in:
pengzhanbo 2024-06-13 01:33:44 +08:00
parent 3018f6789b
commit cb71585079
13 changed files with 328 additions and 730 deletions

View File

@ -35,7 +35,7 @@ const source = computed(() => {
/>
</ClientOnly>
<p v-else>
<a :href="source" target="_blank" rel="noopener noreferrer" :aria-label="title || 'CodeSandbox'">
<a class="code-sandbox-link no-icon" :href="source" target="_blank" rel="noopener noreferrer" :aria-label="title || 'CodeSandbox'">
<svg xmlns="http://www.w3.org/2000/svg" width="165" height="32" viewBox="0 0 165 32" fill="none">
<rect width="165" height="32" rx="4" fill="#E3FF73" />
<rect x="0.5" y="0.5" width="164" height="31" rx="3.5" stroke="black" stroke-opacity="0.1" />
@ -54,6 +54,10 @@ const source = computed(() => {
</template>
<style>
.code-sandbox-link {
display: inline-block;
}
.code-sandbox-iframe {
width: 100%;
height: 500px;

View File

@ -4,107 +4,14 @@
*/
import type { PluginWithOptions } from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import type MarkdownIt from 'markdown-it'
import container from 'markdown-it-container'
import { customAlphabet } from 'nanoid'
import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../../shared/index.js'
import { createRuleBlock } from '../utils/createRuleBlock.js'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5)
// @[caniuse]()
const minLength = 12
// char codes of '@[caniuse'
const START_CODES = [64, 91, 99, 97, 110, 105, 117, 115, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[caniuse(?:\s*?(embed|image)?(?:{([0-9,\-]*?)})?)\]\(([^)]*)\)/
const UNDERLINE_RE = /_+/g
function createCanIUseRuleBlock(defaultMode: CanIUseMode): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + minLength > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, mode, versions = '', feature] = match
const meta: CanIUseTokenMeta = {
feature,
mode: (mode as CanIUseMode) || defaultMode,
versions,
}
const token = state.push('caniuse', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = mode || defaultMode
state.line = startLine + 1
return true
}
}
function resolveCanIUse({ feature, mode, versions }: CanIUseTokenMeta): string {
if (!feature)
return ''
if (mode === 'image') {
const link = 'https://caniuse.bitsofco.de/image/'
const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com`
return `<ClientOnly><p><picture>
<source type="image/webp" srcset="${link}${feature}.webp">
<source type="image/png" srcset="${link}${feature}.png">
<img src="${link}${feature}.jpg" alt="${alt}" width="100%">
</picture></p></ClientOnly>`
}
feature = feature.replace(UNDERLINE_RE, '_')
const { past, future } = resolveVersions(versions)
const meta = nanoid()
return `<CanIUseViewer feature="${feature}" meta="${meta}" past="${past}" future="${future}" />`
}
function resolveVersions(versions: string): { past: number, future: number } {
if (!versions)
return { past: 2, future: 1 }
const list = versions
.split(',')
.map(v => Number(v.trim()))
.filter(v => !Number.isNaN(v) && v >= -5 && v <= 3)
list.push(0)
const uniq = [...new Set(list)].sort((a, b) => b - a)
return {
future: uniq[0],
past: Math.abs(uniq[uniq.length - 1]),
}
}
/**
* @example
* ```md
@ -113,25 +20,18 @@ function resolveVersions(versions: string): { past: number, future: number } {
*/
export const caniusePlugin: PluginWithOptions<CanIUseOptions> = (
md,
{ mode = 'embed' }: CanIUseOptions = {},
{ mode: defaultMode = 'embed' }: CanIUseOptions = {},
): void => {
md.block.ruler.before(
'import_code',
'caniuse',
createCanIUseRuleBlock(mode),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.caniuse = (tokens, index) => {
const token = tokens[index]
const content = resolveCanIUse(token.meta)
token.content = content
return content
}
createRuleBlock<CanIUseTokenMeta>(md, {
type: 'caniuse',
syntaxPattern: /^@\[caniuse(?:\s*?(embed|image)?(?:{([0-9,\-]*?)})?)\]\(([^)]*)\)/,
meta: ([, mode, versions = '', feature]) => ({
feature,
mode: (mode as CanIUseMode) || defaultMode,
versions,
}),
content: meta => resolveCanIUse(meta),
})
}
/**
@ -174,3 +74,42 @@ export function legacyCaniuse(
md.use(container, type, { validate, render })
}
function resolveCanIUse({ feature, mode, versions }: CanIUseTokenMeta): string {
if (!feature)
return ''
if (mode === 'image') {
const link = 'https://caniuse.bitsofco.de/image/'
const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com`
return `<ClientOnly><p><picture>
<source type="image/webp" srcset="${link}${feature}.webp">
<source type="image/png" srcset="${link}${feature}.png">
<img src="${link}${feature}.jpg" alt="${alt}" width="100%">
</picture></p></ClientOnly>`
}
feature = feature.replace(UNDERLINE_RE, '_')
const { past, future } = resolveVersions(versions)
const meta = nanoid()
return `<CanIUseViewer feature="${feature}" meta="${meta}" past="${past}" future="${future}" />`
}
function resolveVersions(versions: string): { past: number, future: number } {
if (!versions)
return { past: 2, future: 1 }
const list = versions
.split(',')
.map(v => Number(v.trim()))
.filter(v => !Number.isNaN(v) && v >= -5 && v <= 3)
list.push(0)
const uniq = [...new Set(list)].sort((a, b) => b - a)
return {
future: uniq[0],
past: Math.abs(uniq[uniq.length - 1]),
}
}

View File

@ -4,94 +4,35 @@
* @[codesanbox title="xxx" layout="Editor+Preview" height="500px" navbar="false" console="false"](id#filepath)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { CodeSandboxTokenMeta } from '../../shared/codeSandbox.js'
// @[codesandbox]()
const MIN_LENGTH = 16
// char codes of `@[codesandbox`
const START_CODES = [64, 91, 99, 111, 100, 101, 115, 97, 110, 100, 98, 111, 120]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[codesandbox(?:\s+(embed|button))?(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createCodeSandboxRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, type, info = '', source] = match
const { attrs } = resolveAttrs(info)
const [profile, filepath = ''] = source.split('#')
const [user, id] = profile.includes('/') ? profile.split('/') : ['', profile]
const meta: CodeSandboxTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '500px',
user,
id,
title: attrs.title ?? '',
console: attrs.console ?? false,
navbar: attrs.navbar ?? true,
layout: attrs.layout ?? '',
type: (type || 'embed') as CodeSandboxTokenMeta['type'],
filepath,
}
const token = state.push('code_sandbox', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = match[0]
state.line = startLine + 1
return true
}
}
function resolveCodeSandbox(meta: CodeSandboxTokenMeta) {
const { title, height, width, user, id, type, filepath, console, navbar, layout } = meta
return `<CodeSandboxViewer title="${title}" height="${height}" width="${width}" user="${user}" id="${id}" type="${type}" filepath="${filepath}" :console=${console} :navbar=${navbar} layout="${layout}" />`
}
import { createRuleBlock } from '../utils/createRuleBlock.js'
export const codeSandboxPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'code_sandbox',
createCodeSandboxRuleBlock(),
)
createRuleBlock<CodeSandboxTokenMeta>(md, {
type: 'codesandbox',
syntaxPattern: /^@\[codesandbox(?:\s+(embed|button))?(?:\s+([^]*?))?\]\(([^)]*?)\)/,
meta([, type, info = '', source = '']) {
const { attrs } = resolveAttrs(info)
const [profile, filepath = ''] = source.split('#')
const [user, id] = profile.includes('/') ? profile.split('/') : ['', profile]
md.renderer.rules.code_sandbox = (tokens, index) => {
const token = tokens[index]
const content = resolveCodeSandbox(token.meta)
token.content = content
return content
}
return {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '500px',
user,
id,
title: attrs.title ?? '',
console: attrs.console ?? false,
navbar: attrs.navbar ?? true,
layout: attrs.layout ?? '',
type: (type || 'embed') as CodeSandboxTokenMeta['type'],
filepath,
}
},
content({ title, height, width, user, id, type, filepath, console, navbar, layout }) {
return `<CodeSandboxViewer title="${title}" height="${height}" width="${width}" user="${user}" id="${id}" type="${type}" filepath="${filepath}" :console=${console} :navbar=${navbar} layout="${layout}" />`
},
})
}

View File

@ -4,106 +4,46 @@
* @[codepen preview editable title="" height="400px" tab="css,result" theme="dark"](user/slash)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { CodepenTokenMeta } from '../../shared/codepen.js'
import { createRuleBlock } from '../utils/createRuleBlock.js'
const CODEPEN_LINK = 'https://codepen.io/'
// @[codepen]()
const MIN_LENGTH = 12
// char codes of `@[codepen`
const START_CODES = [64, 91, 99, 111, 100, 101, 112, 101, 110]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[codepen(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createCodepenRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', source] = match
const { attrs } = resolveAttrs(info)
const [user, slash] = source.split('/')
const meta: CodepenTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '400px',
user,
slash,
title: attrs.title,
preview: attrs.preview,
editable: attrs.editable,
tab: attrs.tab ?? 'result',
theme: attrs.theme,
}
const token = state.push('codepen', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveCodepen(meta: CodepenTokenMeta): string {
const { title = 'Codepen', height, width } = meta
const params = new URLSearchParams()
meta.editable && params.set('editable', 'true')
meta.tab && params.set('default-tab', meta.tab)
meta.theme && params.set('theme-id', meta.theme)
const middle = meta.preview ? '/embed/preview/' : '/embed/'
const link = `${CODEPEN_LINK}${meta.user}${middle}${meta.slash}?${params.toString()}`
const style = `width:${width};height:${height};margin:16px auto;border-radius:5px;`
return `<iframe class="code-pen-iframe-wrapper" src="${link}" title="${title}" style="${style}" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">See the Pen <a href="${CODEPEN_LINK}${meta.user}/pen/${meta.slash}">${title}</a> by ${meta.user} (<a href="${CODEPEN_LINK}${meta.user}">@${meta.user}</a>) on <a href="${CODEPEN_LINK}">CodePen</a>.</iframe>`
}
export const codepenPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'codepen',
createCodepenRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<CodepenTokenMeta>(md, {
type: 'codepen',
syntaxPattern: /^@\[codepen(?:\s+([^]*?))?\]\(([^)]*?)\)/,
meta: ([, info = '', source = '']) => {
const { attrs } = resolveAttrs(info)
const [user, slash] = source.split('/')
return {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '400px',
user,
slash,
title: attrs.title,
preview: attrs.preview,
editable: attrs.editable,
tab: attrs.tab ?? 'result',
theme: attrs.theme,
}
},
)
content: (meta) => {
const { title = 'Codepen', height, width } = meta
const params = new URLSearchParams()
meta.editable && params.set('editable', 'true')
meta.tab && params.set('default-tab', meta.tab)
meta.theme && params.set('theme-id', meta.theme)
md.renderer.rules.codepen = (tokens, index) => {
const token = tokens[index]
const middle = meta.preview ? '/embed/preview/' : '/embed/'
const content = resolveCodepen(token.meta)
token.content = content
const link = `${CODEPEN_LINK}${meta.user}${middle}${meta.slash}?${params.toString()}`
const style = `width:${width};height:${height};margin:16px auto;border-radius:5px;`
return content
}
return `<iframe class="code-pen-iframe-wrapper" src="${link}" title="${title}" style="${style}" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">See the Pen <a href="${CODEPEN_LINK}${meta.user}/pen/${meta.slash}">${title}</a> by ${meta.user} (<a href="${CODEPEN_LINK}${meta.user}">@${meta.user}</a>) on <a href="${CODEPEN_LINK}">CodePen</a>.</iframe>`
},
})
}

View File

@ -3,99 +3,35 @@
* @[jsfiddle theme="dark" tab="js,css,html,result"](user/id)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { JSFiddleTokenMeta } from '../../shared/jsfiddle.js'
const JS_FIDDLE_LINK = 'https://jsfiddle.net/'
// @[jsfiddle]()
const MIN_LENGTH = 13
// char codes of `@[jsfiddle`
const START_CODES = [64, 91, 106, 115, 102, 105, 100, 100, 108, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[jsfiddle(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createJsFiddleRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', source] = match
const { attrs } = resolveAttrs(info)
const [user, id] = source.split('/')
const meta: JSFiddleTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '400px',
user,
id,
title: attrs.title || 'JS Fiddle',
tab: attrs.tab?.replace(/\s+/g, '') || 'js,css,html,result',
theme: attrs.theme || 'dark',
}
const token = state.push('js_fiddle', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveJsFiddle(meta: JSFiddleTokenMeta): string {
const { title = 'JS Fiddle', height, width, user, id, tab } = meta
const theme = meta.theme === 'dark' ? '/dark/' : ''
const link = `${JS_FIDDLE_LINK}${user}/${id}/embedded/${tab}${theme}`
const style = `width:${width};height:${height};margin:16px auto;border:none;border-radius:5px;`
return `<iframe class="js-fiddle-iframe-wrapper" style="${style}" title="${title}" src="${link}" allowfullscreen="true" allowpaymentrequest="true"></iframe>`
}
import { createRuleBlock } from '../utils/createRuleBlock.js'
export const jsfiddlePlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'js_fiddle',
createJsFiddleRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<JSFiddleTokenMeta>(md, {
type: 'jsfiddle',
syntaxPattern: /^@\[jsfiddle(?:\s+([^]*?))?\]\(([^)]*?)\)/,
meta([, info = '', source]) {
const { attrs } = resolveAttrs(info)
const [user, id] = source.split('/')
return {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '400px',
user,
id,
title: attrs.title || 'JS Fiddle',
tab: attrs.tab?.replace(/\s+/g, '') || 'js,css,html,result',
theme: attrs.theme || 'dark',
}
},
)
content: ({ title = 'JS Fiddle', height, width, user, id, tab, theme }) => {
theme = theme === 'dark' ? '/dark/' : ''
md.renderer.rules.js_fiddle = (tokens, index) => {
const token = tokens[index]
const content = resolveJsFiddle(token.meta)
token.content = content
return content
}
const link = `https://jsfiddle.net/${user}/${id}/embedded/${tab}${theme}`
const style = `width:${width};height:${height};margin:16px auto;border:none;border-radius:5px;`
return `<iframe class="js-fiddle-iframe-wrapper" style="${style}" title="${title}" src="${link}" allowfullscreen="true" allowpaymentrequest="true"></iframe>`
},
})
}

View File

@ -5,93 +5,30 @@
*/
import { path } from 'vuepress/utils'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import type { PDFTokenMeta } from '../../shared/pdf.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
// @[pdf]()
const MIN_LENGTH = 8
// char codes of `@[pdf`
const START_CODES = [64, 91, 112, 100, 102]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[pdf(?:\s+(\d+))?(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createPDFRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, page, info = '', src] = match
const { attrs } = resolveAttrs(info)
const meta: PDFTokenMeta = {
src,
page: +page || 1,
noToolbar: Boolean(attrs.noToolbar ?? false),
zoom: +attrs.zoom || 50,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
title: path.basename(src || ''),
}
const token = state.push('pdf', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolvePDF(meta: PDFTokenMeta): string {
const { title, src, page, noToolbar, width, height, ratio, zoom } = meta
return `<PDFViewer src="${src}" title="${title}" :page="${page}" :no-toolbar="${noToolbar}" width="${width}" height="${height}" ratio="${ratio}" :zoom="${zoom}" />`
}
import { createRuleBlock } from '../utils/createRuleBlock.js'
export const pdfPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'pdf',
createPDFRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<PDFTokenMeta>(md, {
type: 'pdf',
syntaxPattern: /^@\[pdf(?:\s+(\d+))?(?:\s+([^]*?))?\]\(([^)]*?)\)/,
meta([, page, info = '', src = '']) {
const { attrs } = resolveAttrs(info)
return {
src,
page: +page || 1,
noToolbar: Boolean(attrs.noToolbar ?? false),
zoom: +attrs.zoom || 50,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
title: path.basename(src || ''),
}
},
)
md.renderer.rules.pdf = (tokens, index) => {
const token = tokens[index]
const content = resolvePDF(token.meta)
token.content = content
return content
}
content({ title, src, page, noToolbar, width, height, ratio, zoom }) {
return `<PDFViewer src="${src}" title="${title}" :page="${page}" :no-toolbar="${noToolbar}" width="${width}" height="${height}" ratio="${ratio}" :zoom="${zoom}" />`
},
})
}

View File

@ -4,90 +4,27 @@
* @[replit title="" height="400px" width="100%" theme="dark"](user/repl-name)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { ReplitTokenMeta } from '../../shared/replit.js'
// @[replit]()
const MIN_LENGTH = 11
// char codes of `@[replit`
const START_CODES = [64, 91, 114, 101, 112, 108, 105, 116]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[replit(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createReplitRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', source] = match
const { attrs } = resolveAttrs(info)
const meta: ReplitTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '450px',
source: source.startsWith('@') ? source : `@${source}`,
title: attrs.title,
theme: attrs.theme || '',
}
const token = state.push('replit', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveReplit(meta: ReplitTokenMeta): string {
const { title, height, width, source, theme } = meta
return `<ReplitViewer title="${title || ''}" height="${height}" width="${width}" source="${source}" theme="${theme}" />`
}
import { createRuleBlock } from '../utils/createRuleBlock.js'
export const replitPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'replit',
createReplitRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<ReplitTokenMeta>(md, {
type: 'replit',
syntaxPattern: /^@\[replit(?:\s+([^]*?))?\]\(([^)]*?)\)/,
meta: ([, info = '', source = '']) => {
const { attrs } = resolveAttrs(info)
return {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '450px',
source: source.startsWith('@') ? source : `@${source}`,
title: attrs.title,
theme: attrs.theme || '',
}
},
)
md.renderer.rules.replit = (tokens, index) => {
const token = tokens[index]
const content = resolveReplit(token.meta)
token.content = content
return content
}
content({ title, height, width, source, theme }) {
return `<ReplitViewer title="${title || ''}" height="${height}" width="${width}" source="${source}" theme="${theme}" />`
},
})
}

View File

@ -6,110 +6,51 @@
*/
import { URLSearchParams } from 'node:url'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import type { BilibiliTokenMeta } from '../../../shared/video.js'
import { resolveAttrs } from '../../utils/resolveAttrs.js'
import { parseRect } from '../../utils/parseRect.js'
import { timeToSeconds } from '../../utils/timeToSeconds.js'
import { createRuleBlock } from '../../utils/createRuleBlock.js'
const BILIBILI_LINK = 'https://player.bilibili.com/player.html'
// @[bilibili]()
const MIN_LENGTH = 13
// char codes of '@[bilibili'
const START_CODES = [64, 91, 98, 105, 108, 105, 98, 105, 108, 105]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[bilibili(?:\s+p(\d+))?(?:\s+([^]*?))?\]\(([^)]*)\)/
function createBilibiliRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, page, info = '', source = ''] = match
const { attrs } = resolveAttrs(info)
const ids = source.trim().split(/\s+/)
const bvid = ids.find(id => id.startsWith('BV'))
const [aid, cid] = ids.filter(id => !id.startsWith('BV'))
const meta: BilibiliTokenMeta = {
page: +page || 1,
bvid,
aid,
cid,
autoplay: attrs.autoplay ?? false,
time: timeToSeconds(attrs.time),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
const token = state.push('video_bilibili', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveBilibili(meta: BilibiliTokenMeta): string {
const params = new URLSearchParams()
meta.bvid && params.set('bvid', meta.bvid)
meta.aid && params.set('aid', meta.aid)
meta.cid && params.set('cid', meta.cid)
meta.page && params.set('p', meta.page.toString())
meta.time && params.set('t', meta.time.toString())
params.set('autoplay', meta.autoplay ? '1' : '0')
const source = `${BILIBILI_LINK}?${params.toString()}`
return `<VideoBilibili src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
}
export const bilibiliPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'video_bilibili',
createBilibiliRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<BilibiliTokenMeta>(md, {
type: 'bilibili',
name: 'video_bilibili',
syntaxPattern: /^@\[bilibili(?:\s+p(\d+))?(?:\s+([^]*?))?\]\(([^)]*)\)/,
meta([, page, info = '', source = '']) {
const { attrs } = resolveAttrs(info)
const ids = source.trim().split(/\s+/)
const bvid = ids.find(id => id.startsWith('BV'))
const [aid, cid] = ids.filter(id => !id.startsWith('BV'))
return {
page: +page || 1,
bvid,
aid,
cid,
autoplay: attrs.autoplay ?? false,
time: timeToSeconds(attrs.time),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
},
)
content(meta) {
const params = new URLSearchParams()
md.renderer.rules.video_bilibili = (tokens, index) => {
const token = tokens[index]
meta.bvid && params.set('bvid', meta.bvid)
meta.aid && params.set('aid', meta.aid)
meta.cid && params.set('cid', meta.cid)
meta.page && params.set('p', meta.page.toString())
meta.time && params.set('t', meta.time.toString())
params.set('autoplay', meta.autoplay ? '1' : '0')
const content = resolveBilibili(token.meta)
token.content = content
const source = `${BILIBILI_LINK}?${params.toString()}`
return content
}
return `<VideoBilibili src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
},
})
}

View File

@ -3,104 +3,45 @@
*/
import { URLSearchParams } from 'node:url'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.mjs'
import type { YoutubeTokenMeta } from '../../../shared/video.js'
import { resolveAttrs } from '../../utils/resolveAttrs.js'
import { parseRect } from '../../utils/parseRect.js'
import { timeToSeconds } from '../../utils/timeToSeconds.js'
import { createRuleBlock } from '../../utils/createRuleBlock.js'
const YOUTUBE_LINK = 'https://www.youtube.com/embed/'
// @[youtube]()
const MIN_LENGTH = 13
// char codes of '@[youtube'
const START_CODES = [64, 91, 121, 111, 117, 116, 117, 98, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[youtube(?:\s+([^]*?))?\]\(([^)]*)\)/
function createYoutubeRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', id = ''] = match
const { attrs } = resolveAttrs(info)
const meta: YoutubeTokenMeta = {
id,
autoplay: attrs.autoplay ?? false,
loop: attrs.loop ?? false,
start: timeToSeconds(attrs.start),
end: timeToSeconds(attrs.end),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
const token = state.push('video_youtube', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveYoutube(meta: YoutubeTokenMeta): string {
const params = new URLSearchParams()
meta.autoplay && params.set('autoplay', '1')
meta.loop && params.set('loop', '1')
meta.start && params.set('start', meta.start.toString())
meta.end && params.set('end', meta.end.toString())
const source = `${YOUTUBE_LINK}/${meta.id}?${params.toString()}`
return `<VideoYoutube src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
}
export const youtubePlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'video_youtube',
createYoutubeRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
createRuleBlock<YoutubeTokenMeta>(md, {
type: 'youtube',
name: 'video_youtube',
syntaxPattern: /^@\[youtube(?:\s+([^]*?))?\]\(([^)]*)\)/,
meta([, info = '', id = '']) {
const { attrs } = resolveAttrs(info)
return {
id,
autoplay: attrs.autoplay ?? false,
loop: attrs.loop ?? false,
start: timeToSeconds(attrs.start),
end: timeToSeconds(attrs.end),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
},
)
content(meta) {
const params = new URLSearchParams()
md.renderer.rules.video_youtube = (tokens, index) => {
const token = tokens[index]
meta.autoplay && params.set('autoplay', '1')
meta.loop && params.set('loop', '1')
meta.start && params.set('start', meta.start.toString())
meta.end && params.set('end', meta.end.toString())
const content = resolveYoutube(token.meta)
token.content = content
const source = `${YOUTUBE_LINK}/${meta.id}?${params.toString()}`
return content
}
return `<VideoYoutube src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
},
})
}

View File

@ -0,0 +1,79 @@
import type { RuleOptions } from 'markdown-it/lib/ruler.mjs'
import type { Markdown } from 'vuepress/markdown'
export interface RuleBlockOptions<Meta extends Record<string, any>> {
/**
* @[type]()
*/
type: string
/**
* token name
*/
name?: string
beforeName?: string
syntaxPattern: RegExp
ruleOptions?: RuleOptions
meta: (match: RegExpMatchArray) => Meta
content: (meta: Meta) => string
}
// @[name]()
export function createRuleBlock<Meta extends Record<string, any> = Record<string, any>>(
md: Markdown,
{
type,
name = type,
syntaxPattern,
beforeName = 'import_code',
ruleOptions = { alt: ['paragraph', 'reference', 'blockquote', 'list'] },
meta,
content,
}: RuleBlockOptions<Meta>,
): void {
const MIN_LENGTH = type.length + 5
const START_CODES = [64, 91, ...type.split('').map(c => c.charCodeAt(0))]
md.block.ruler.before(
beforeName,
name,
(state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(syntaxPattern)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const token = state.push(name, '', 0)
token.meta = meta(match)
token.map = [startLine, startLine + 1]
state.line = startLine + 1
return true
},
ruleOptions,
)
md.renderer.rules[name] = (tokens, index) => {
const token = tokens[index]
token.content = content(token.meta)
return token.content
}
}

View File

@ -16,7 +16,11 @@ defineProps<{
{{ group.desc }}
</p>
<section v-if="group.list?.length" class="friends-list">
<FriendsItem v-for="(friend, index) in group.list" :key="friend.name + index" :friend="friend" />
<FriendsItem
v-for="(friend, index) in group.list"
:key="friend.name + index"
:friend="friend"
/>
</section>
</div>
</template>

View File

@ -14,4 +14,3 @@ const { resolve: onBeforeEnter, pending: onBeforeLeave } = useScrollPromise()
<slot />
</Transition>
</template>
../composables/scroll-promise.js

View File

@ -2,10 +2,10 @@
import { useRouteLocale, withBase } from 'vuepress/client'
import LayoutContent from '../components/LayoutContent.vue'
import Nav from '../components/Nav/index.vue'
import { useThemeLocaleData } from '../composables'
import { useData } from '../composables/data.js'
const root = useRouteLocale()
const themeData = useThemeLocaleData()
const { theme } = useData()
</script>
<template>
@ -14,19 +14,19 @@ const themeData = useThemeLocaleData()
<LayoutContent is-not-found>
<div class="not-found">
<p class="code">
{{ themeData.notFound?.code ?? '404' }}
{{ theme.notFound?.code ?? '404' }}
</p>
<h1 class="title">
{{ themeData.notFound?.title ?? 'PAGE NOT FOUND' }}
{{ theme.notFound?.title ?? 'PAGE NOT FOUND' }}
</h1>
<div class="divider" />
<blockquote class="quote">
{{ themeData.notFound?.quote ?? `But if you don't change your direction, and if you keep looking, you may end up where you are heading.` }}
{{ theme.notFound?.quote ?? `But if you don't change your direction, and if you keep looking, you may end up where you are heading.` }}
</blockquote>
<div class="action">
<a class="link" :href="withBase(root)" :aria-label="themeData.notFound?.linkLabel ?? 'go to home'">
{{ themeData.notFound?.linkText ?? 'Take me home' }}
<a class="link" :href="withBase(root)" :aria-label="theme.notFound?.linkLabel ?? 'go to home'">
{{ theme.notFound?.linkText ?? 'Take me home' }}
</a>
</div>
</div>