commit
be80cbab2d
@ -0,0 +1,4 @@ |
|||||||
|
export default definePageConfig({ |
||||||
|
navigationBarTitleText: '链接', |
||||||
|
onReachBottomDistance: 30 |
||||||
|
}) |
@ -0,0 +1,9 @@ |
|||||||
|
import {FC} from "react"; |
||||||
|
import {WebView} from "@tarojs/components"; |
||||||
|
import {useRouter} from "@tarojs/taro"; |
||||||
|
|
||||||
|
const Web:FC = () => { |
||||||
|
const {url} = useRouter().params as unknown as { url: string } |
||||||
|
return <WebView src={url}></WebView> |
||||||
|
} |
||||||
|
export default Web |
@ -0,0 +1,30 @@ |
|||||||
|
import { FC, ReactNode} from "react"; |
||||||
|
import {View} from "@tarojs/components"; |
||||||
|
import Taro from "@tarojs/taro"; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface TabLinkProps{ |
||||||
|
className:string |
||||||
|
url:string |
||||||
|
children: ReactNode[] |
||||||
|
} |
||||||
|
|
||||||
|
const TabLink:FC<TabLinkProps> = (props:TabLinkProps) => { |
||||||
|
function linkTab(){ |
||||||
|
console.log(props.url) |
||||||
|
let webExp= new RegExp(/http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/) |
||||||
|
if(webExp.test(props.url)){ |
||||||
|
//跳转webview
|
||||||
|
Taro.navigateTo({url:`/pages/preview/webView/webView?url=${props.url}`}) |
||||||
|
}else{ |
||||||
|
//跳转小程序页面
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View onClick={linkTab} className="font-28" style={{display:"inline-block",color:'#45D4A8'}}>22222</View> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default TabLink |
@ -0,0 +1,174 @@ |
|||||||
|
.code {} |
||||||
|
|
||||||
|
.h1 { |
||||||
|
font-size: 48rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.h2 { |
||||||
|
font-size: 44rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.h3 { |
||||||
|
font-size: 40rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.h4 { |
||||||
|
font-size: 36rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.h5 { |
||||||
|
font-size: 32rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.h6 { |
||||||
|
font-size: 28rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
margin-bottom: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.hr { |
||||||
|
border-bottom: 1px solid #aaa; |
||||||
|
margin-top: 12rpx; |
||||||
|
margin-bottom: 12rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.blockquote { |
||||||
|
border-left: 8rpx solid #aaa; |
||||||
|
padding: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.list, |
||||||
|
.taskList, |
||||||
|
.orderedList { |
||||||
|
display: block; |
||||||
|
margin-bottom: 12rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.listItem, |
||||||
|
.taskListItem, |
||||||
|
.orderedListItem { |
||||||
|
padding-left: 1rem; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.taskListItem { |
||||||
|
padding-left: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.checkedTaskIndicator, |
||||||
|
.uncheckTaskIndicator, |
||||||
|
.listItemIndicator { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
user-select: none; |
||||||
|
} |
||||||
|
|
||||||
|
.checkedTaskIndicator, |
||||||
|
.uncheckTaskIndicator { |
||||||
|
width: 1rem; |
||||||
|
height: 1rem; |
||||||
|
overflow: hidden; |
||||||
|
border: 2px solid #ddd; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
font-size: 0.65rem; |
||||||
|
border-radius: 8rpx; |
||||||
|
color: #999; |
||||||
|
} |
||||||
|
|
||||||
|
.paragraph { |
||||||
|
margin-top: 4rpx; |
||||||
|
margin-bottom: 12rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
font-weight: 500; |
||||||
|
color: #444444; |
||||||
|
line-height: 35rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.blockHtml {} |
||||||
|
|
||||||
|
.inlineHtml {} |
||||||
|
|
||||||
|
.text { |
||||||
|
} |
||||||
|
|
||||||
|
.def {} |
||||||
|
|
||||||
|
.strong { |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
.em { |
||||||
|
font-style: italic; |
||||||
|
} |
||||||
|
|
||||||
|
.del { |
||||||
|
text-decoration: line-through; |
||||||
|
} |
||||||
|
|
||||||
|
.codespan { |
||||||
|
padding: 0 12rpx; |
||||||
|
font-size: 85%; |
||||||
|
border: 1px solid #aaa; |
||||||
|
background-color: #ccc; |
||||||
|
border-radius: 8rpx; |
||||||
|
} |
||||||
|
|
||||||
|
//.escape {} |
||||||
|
//.br {} |
||||||
|
|
||||||
|
.link { |
||||||
|
display: inline; |
||||||
|
text-decoration: underline; |
||||||
|
color: blue; |
||||||
|
// todo 取消点击时的背景 |
||||||
|
} |
||||||
|
|
||||||
|
.image { |
||||||
|
display: inline-block; |
||||||
|
max-width: 100%; |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.video { |
||||||
|
display: block; |
||||||
|
max-width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
// todo 自动换行 |
||||||
|
.table { |
||||||
|
max-width: 100%; |
||||||
|
overflow-x: auto; |
||||||
|
border: 1px solid #dee2e6; |
||||||
|
} |
||||||
|
|
||||||
|
.tableBodyRow, |
||||||
|
.tableHeader { |
||||||
|
display: flex; |
||||||
|
|
||||||
|
&:not(:last-child) { |
||||||
|
border-bottom: 1px solid #dee2e6; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.tableHeaderCell, |
||||||
|
.tableBodyCell { |
||||||
|
flex: 1; |
||||||
|
padding: 8rpx 12rpx; |
||||||
|
|
||||||
|
&:not(:last-child) { |
||||||
|
border-right: 1px solid #dee2e6; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,319 @@ |
|||||||
|
import {Token, Tokens, lexer} from "marked" |
||||||
|
import {Attributes, ComponentType, createElement, ReactElement, ReactNode} from "react" |
||||||
|
import {View, Text, Image, Video, Audio} from "@tarojs/components" |
||||||
|
import {parseHeadingId, walkTokens, unescape} from "./utils" |
||||||
|
import TabLink from "./components/Tablink"; |
||||||
|
import './marked.scss' |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析 Markdown 文本 |
||||||
|
* |
||||||
|
* ```ts
|
||||||
|
* const Markdown: FC<{ source: string }> = ({ source }) => { |
||||||
|
* const { children, headings } = useMemo(() => parse(source), [source]) |
||||||
|
* console.log(headings) // => [{level: 1, text: 'heading', id: 'custom-id'}]
|
||||||
|
* return (<View>{children}</View>) |
||||||
|
* } |
||||||
|
* |
||||||
|
* <Markdown source={'# heading {#custom-id}'} /> |
||||||
|
* ``` |
||||||
|
* |
||||||
|
* @param {string} content 原始文本 |
||||||
|
* |
||||||
|
* @see {@link https://www.markdownguide.org}
|
||||||
|
* @see {@link https://marked.js.org}
|
||||||
|
*/ |
||||||
|
export const parse = (content: string) => { |
||||||
|
const headings: Heading[] = [] |
||||||
|
let nextKey = 0 |
||||||
|
|
||||||
|
const ctx: RenderContext = { |
||||||
|
createReactElement<P>(type: ComponentType<P>, props?: Props<P>): ReactElement<P> { |
||||||
|
props ??= {} as Props<P> |
||||||
|
props.key ??= ++nextKey |
||||||
|
return createElement(type, props) |
||||||
|
}, |
||||||
|
|
||||||
|
ensureHeading(token: Tokens.Heading): string { |
||||||
|
// todo(hupeh): id 不能重复
|
||||||
|
// todo(hupeh): 第一层标题级别必须一致
|
||||||
|
const id = parseHeadingId(token) |
||||||
|
headings.push({id, text: token.text, level: token.depth}) |
||||||
|
return id |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const tokens = lexer(content) |
||||||
|
const children = render(ctx, tokens) |
||||||
|
|
||||||
|
return {children, headings} |
||||||
|
} |
||||||
|
|
||||||
|
export interface Heading { |
||||||
|
level: number |
||||||
|
text: string |
||||||
|
id: string |
||||||
|
} |
||||||
|
|
||||||
|
type Props<P> = Attributes & P |
||||||
|
|
||||||
|
interface RenderContext { |
||||||
|
createReactElement<P>(type: ComponentType<P>, props?: Props<P>): ReactElement<P> |
||||||
|
|
||||||
|
ensureHeading(token: Tokens.Heading): string |
||||||
|
} |
||||||
|
|
||||||
|
function render(ctx: RenderContext, tokens: Token[]) { |
||||||
|
return walkTokens<ReactNode>(tokens, (token) => renderToken(ctx, token)) |
||||||
|
} |
||||||
|
|
||||||
|
function renderToken(ctx: RenderContext, token: Token) { |
||||||
|
return (renderer[token.type] ?? renderer.generic)(ctx, token) |
||||||
|
} |
||||||
|
|
||||||
|
const renderer = { |
||||||
|
space(_: RenderContext, tk: Tokens.Space) { |
||||||
|
// todo(hupeh): 测试是否需要 Text 实现
|
||||||
|
return tk.raw |
||||||
|
}, |
||||||
|
|
||||||
|
code(ctx: RenderContext, tk: Tokens.Code) { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'code', |
||||||
|
children: tk.escaped ? escape(tk.text) : tk.text, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
heading(ctx: RenderContext, tk: Tokens.Heading) { |
||||||
|
const id = ctx.ensureHeading(tk) |
||||||
|
|
||||||
|
// todo(hupeh): 是否需要调整标题级别保证顺序一致
|
||||||
|
return ctx.createReactElement(View, { |
||||||
|
id, |
||||||
|
className: `h${tk.depth}`, |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
hr(ctx: RenderContext, _: Tokens.Hr) { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'hr', |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
blockquote(ctx: RenderContext, tk: Tokens.Blockquote) { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'blockquote', |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
list(ctx: RenderContext, tk: Tokens.List) { |
||||||
|
const isTaskList = tk.items[0]?.task == true; |
||||||
|
// todo(hupeh): 什么是 tk.loose
|
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: isTaskList ? 'taskList' : tk.ordered ? 'orderedList' : 'list', |
||||||
|
children: tk.items.map((item, index) => { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: isTaskList ? 'taskListItem' : tk.ordered ? 'orderedListItem' : "listItem", |
||||||
|
children: [ |
||||||
|
!isTaskList ? ctx.createReactElement(View, { |
||||||
|
className: 'listItemIndicator', |
||||||
|
children: tk.ordered ? index + 1 : '•', |
||||||
|
}) : null, |
||||||
|
item.task ? ctx.createReactElement(View, { |
||||||
|
className: item.checked ? 'checkedTaskIndicator' : 'uncheckTaskIndicator', |
||||||
|
children: item.checked ? ctx.createReactElement(Text, { |
||||||
|
children: '✔', |
||||||
|
}) : null, |
||||||
|
}) : null, |
||||||
|
...render(ctx, item.tokens), |
||||||
|
], |
||||||
|
}); |
||||||
|
}) |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
paragraph(ctx: RenderContext, tk: Tokens.Paragraph) { |
||||||
|
// todo(hupeh): tk.pre
|
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'paragraph', |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
html(ctx: RenderContext, tk: Tokens.HTML) { |
||||||
|
// todo(hupeh): 什么是 tk.pre ? 对应 <pre> 标签吗?
|
||||||
|
// if (tk.pre) {}
|
||||||
|
if (tk.block) { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'blockHtml', |
||||||
|
children: tk.text.split(/\r?\n/).map(line => { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'blockHtmlItem', |
||||||
|
children: ctx.createReactElement(Text, { |
||||||
|
space: 'nbsp', // 空格符号渲染方式
|
||||||
|
decode: true, // 正确显示实体
|
||||||
|
children: line.trimEnd(), |
||||||
|
}), |
||||||
|
}) |
||||||
|
}), |
||||||
|
}) |
||||||
|
} |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'inlineHtml', |
||||||
|
children: tk.text, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
text(ctx: RenderContext, tk: Tokens.Text) { |
||||||
|
if (tk.tokens?.length) { |
||||||
|
return render(ctx, tk.tokens) |
||||||
|
} |
||||||
|
// todo 转义 HTML 实体
|
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'text', |
||||||
|
children: unescape(tk.text), |
||||||
|
decode: true, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
def(ctx: RenderContext, tk: Tokens.Def) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'def', |
||||||
|
children: tk.title, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
escape(ctx: RenderContext, tk: Tokens.Escape) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'escape', |
||||||
|
children: escape(tk.text), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
link(ctx: RenderContext, tk: Tokens.Link) { |
||||||
|
// todo(hupeh): 页面内部跳转,根据 heading 的 ID 实现
|
||||||
|
return ctx.createReactElement(TabLink, { |
||||||
|
className: 'link', |
||||||
|
url: tk.href, |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
image(ctx: RenderContext, tk: Tokens.Image) { |
||||||
|
// todo(hupeh): 相关控制属性可以通过 QueryString 解析出来
|
||||||
|
const ext = tk.href.split('?').shift()!.split('.').pop()!.toLowerCase(); |
||||||
|
switch (ext) { |
||||||
|
case 'mp4': |
||||||
|
case 'm4v': |
||||||
|
case 'mov': |
||||||
|
case 'qt': |
||||||
|
case 'avi': |
||||||
|
case 'flv': |
||||||
|
case 'wmv': |
||||||
|
case 'asf': |
||||||
|
case 'mkv': |
||||||
|
case 'rm': |
||||||
|
case 'rmvb': |
||||||
|
case 'vob': |
||||||
|
case 'ts': |
||||||
|
case 'dat': |
||||||
|
return ctx.createReactElement(Video, { |
||||||
|
className: 'video', |
||||||
|
src: tk.href, |
||||||
|
}) |
||||||
|
case 'mp3': |
||||||
|
case 'wma': |
||||||
|
case 'flac': |
||||||
|
case 'wav': |
||||||
|
// todo(hupeh): 使用 Taro.createInnerAudioContext 接口封装组件
|
||||||
|
return ctx.createReactElement(Audio, { |
||||||
|
className: 'audio', |
||||||
|
src: tk.href |
||||||
|
}) |
||||||
|
default: |
||||||
|
return ctx.createReactElement(Image, { |
||||||
|
className: 'image', |
||||||
|
src: tk.href, |
||||||
|
mode: "widthFix", |
||||||
|
webp: ext === 'web', |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
strong(ctx: RenderContext, tk: Tokens.Strong) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'strong', |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
em(ctx: RenderContext, tk: Tokens.Em) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'em', |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
codespan(ctx: RenderContext, tk: Tokens.Codespan) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'codespan', |
||||||
|
children: tk.text, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
br(ctx: RenderContext, _: Tokens.Br) { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'br', |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
del(ctx: RenderContext, tk: Tokens.Del) { |
||||||
|
return ctx.createReactElement(Text, { |
||||||
|
className: 'del', |
||||||
|
children: render(ctx, tk.tokens), |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
table(ctx: RenderContext, tk: Tokens.Table) { |
||||||
|
// todo(hupeh): 使用自定义 table 组件,检测单元格宽度
|
||||||
|
// return "暂不支持表格渲染";
|
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'table', |
||||||
|
children: [ |
||||||
|
ctx.createReactElement(View, { |
||||||
|
className: 'tableHeader', |
||||||
|
children: tk.header.map((cell, index) => { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'tableHeaderCell', |
||||||
|
style: {textAlign: tk.align[index] || 'left'}, |
||||||
|
children: render(ctx, cell.tokens), |
||||||
|
}) |
||||||
|
}), |
||||||
|
}), |
||||||
|
ctx.createReactElement(View, { |
||||||
|
className: 'tableBody', |
||||||
|
children: tk.rows.map(rows => { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'tableBodyRow', |
||||||
|
children: rows.map((cell, index) => { |
||||||
|
return ctx.createReactElement(View, { |
||||||
|
className: 'tableBodyCell', |
||||||
|
style: {textAlign: tk.align[index] || 'left'}, |
||||||
|
children: render(ctx, cell.tokens), |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}), |
||||||
|
}), |
||||||
|
], |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
generic(ctx: RenderContext, tk: Tokens.Generic) { |
||||||
|
// todo(hupeh): 验证 tk.type
|
||||||
|
// todo(hupeh): 调试模式下不返回 null
|
||||||
|
return tk.tokens?.length ? render(ctx, tk.tokens) : null |
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
import {Token, Tokens, TokensList} from "@/utils/marked/marked" |
||||||
|
import {Attributes, ComponentType, createElement, ReactElement} from "react"; |
||||||
|
|
||||||
|
|
||||||
|
const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig |
||||||
|
|
||||||
|
export function unescape(html: string) { |
||||||
|
// explicitly match decimal, hex, and named HTML entities
|
||||||
|
return html.replace(unescapeTest, (_, n) => { |
||||||
|
n = n.toLowerCase() |
||||||
|
if (n === 'colon') return ':' |
||||||
|
if (n.charAt(0) === '#') { |
||||||
|
return n.charAt(1) === 'x' |
||||||
|
? String.fromCharCode(parseInt(n.substring(2), 16)) |
||||||
|
: String.fromCharCode(+n.substring(1)) |
||||||
|
} |
||||||
|
return '' |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// This alphabet uses `A-Za-z0-9_-` symbols.
|
||||||
|
// The order of characters is optimized for better gzip and brotli compression.
|
||||||
|
// Same as in non-secure/index.js
|
||||||
|
const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' |
||||||
|
|
||||||
|
export function hash(str: string) { |
||||||
|
const parts = Math.abs(str.split("") |
||||||
|
.reduce((a, b) => { |
||||||
|
a = ((a << 5) - a) + b.charCodeAt(0); |
||||||
|
return a & a; |
||||||
|
}, 0)) |
||||||
|
.toString() |
||||||
|
.split('') |
||||||
|
|
||||||
|
let id = '' |
||||||
|
// We are reading directly from the random pool to avoid creating new array
|
||||||
|
for (const part of parts) { |
||||||
|
const size = Number.parseInt(part) |
||||||
|
if (!Number.isNaN(size)) { |
||||||
|
// It is incorrect to use bytes exceeding the alphabet size.
|
||||||
|
// The following mask reduces the random byte in the 0-255 value
|
||||||
|
// range to the 0-63 value range. Therefore, adding hacks, such
|
||||||
|
// as empty string fallback or magic numbers, is unneccessary because
|
||||||
|
// the bitmask trims bytes down to the alphabet size.
|
||||||
|
id += urlAlphabet[size & 63] |
||||||
|
} |
||||||
|
} |
||||||
|
return id.replace(/^-+|-+$/g, '') |
||||||
|
} |
||||||
|
|
||||||
|
export function walkTokens<T = void>(tokens: Token[] | TokensList, callback: (token: Token) => T | T[]) { |
||||||
|
let values: T[] = []; |
||||||
|
for (const token of tokens) { |
||||||
|
values = values.concat(callback.call(this, token)) |
||||||
|
} |
||||||
|
return values |
||||||
|
} |
||||||
|
|
||||||
|
export function parseHeadingId(tk: Tokens.Heading) { |
||||||
|
const index = tk.tokens.length - 1; |
||||||
|
if (tk.tokens[index].type !== 'text') { |
||||||
|
return hash(tk.raw) |
||||||
|
} |
||||||
|
|
||||||
|
// https://github.com/markedjs/marked-custom-heading-id/blob/main/src/index.js
|
||||||
|
const child = tk.tokens[index] as Tokens.Text |
||||||
|
const headingIdRegex = /(?: +|^)\{#([a-z][\w-]*)\}(?: +|$)/i; |
||||||
|
const hasId = child.text.match(headingIdRegex); |
||||||
|
|
||||||
|
if (!hasId) { |
||||||
|
return hash(tk.raw) |
||||||
|
} |
||||||
|
|
||||||
|
child.text = child.text.replace(headingIdRegex, '') |
||||||
|
tk.text = tk.text.replace(headingIdRegex, '') |
||||||
|
|
||||||
|
return hasId[1] |
||||||
|
} |
||||||
|
|
||||||
|
let nextKey = 0 |
||||||
|
|
||||||
|
export function h<Props>(type: ComponentType<Props>, props?: Attributes & Props): ReactElement<Props> { |
||||||
|
props ??= {} as Attributes & Props |
||||||
|
props.key ??= ++nextKey |
||||||
|
return createElement(type, props) |
||||||
|
} |
Loading…
Reference in new issue