/**
* @[bilibili](bid)
* @[bilibili](aid cid)
* @[bilibili](bid aid cid)
* @[bilibili p1 autoplay time=1](aid cid)
*/
import { URLSearchParams } from 'node:url'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
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'
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 ``
}
export const bilibiliPlugin: PluginWithOptions = (md) => {
md.block.ruler.before(
'import_code',
'video_bilibili',
createBilibiliRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.video_bilibili = (tokens, index) => {
const token = tokens[index]
const content = resolveBilibili(token.meta)
token.content = content
return content
}
}