文章目录

    • 背景
    • 界面
    • 当前支持的 Markdown 语法
    • 不支持的Markdown 语法
    • 代码节选

背景

出于兴趣,我使用js实现了一个 markdown语法 -> ast语法树 -> html富文本的库, 其速度应当慢于正则实现的同类js库, 但是语法扩展性更好, 嵌套列表处理起来更方便.

界面

基于此js实现vue组件了, 可在uniapp中使用,支持微信小程序和h5.
访问地址: https://ext.dcloud.net.cn/plugin?id=24280#detail
在这里插入图片描述

当前支持的 Markdown 语法

  • 标题(# ~ ######)
  • 粗体(加粗
  • 斜体(斜体
  • 删除线(删除线
  • 行内代码(code
  • 代码块(code
  • 链接(文本)
  • 自动链接(http/https 链接自动转为 <a>
  • 有序列表(1. 2. 3.)
  • 无序列表(- * +)
  • 嵌套的无序列表(- * +, 四格缩进)
  • 表格(| head | head | …)
  • 引用块(> 引用内容,多行合并)
  • 段落、换行
  • 图片

不支持的Markdown 语法

  • ~内嵌 HTML~
  • 脚注、目录、注释等扩展语法
  • ~GFM 扩展:@提及、emoji、自动任务列表渲染等~
  • 多级嵌套列表/引用的递归渲染
  • 代码块高亮(需配合 highlight.js 等)
  • 表格对齐(:—:)等高级表格特性
  • 数学公式

代码节选

// nimd.js - 轻量级 markdown AST解析与渲染库
const nimd = {// 1. Markdown -> ASTparse(md) {if (!md) return []const lines = md.split(/\r?\n/)const ast = []let i = 0// 嵌套列表解析辅助函数function parseList(start, indent, parentOrdered) {const items = []let idx = startwhile (idx < lines.length) {let line = lines[idx]if (/^\s*$/.test(line)) { idx++; continue; }// 动态判断当前行是有序、无序还是任务列表let match = line.match(/^(\s*)(\d+)\.\s+(.*)$/)let ordered = false, task = false, checked = falseif (match) {ordered = true} else {match = line.match(/^(\s*)[-\*\+] \[( |x)\] (.*)$/i)if (match) {task = truechecked = /\[x\]/i.test(line)} else {match = line.match(/^(\s*)[-\*\+]\s+(.*)$/)if (!match) break}}const currIndent = match[1].lengthif (currIndent < indent) breakif (currIndent > indent) {// 递归收集所有同级缩进的子项,类型动态判断const sublist = parseList(idx, currIndent, undefined)if (items.length > 0) {if (!items[items.length - 1].children) items[items.length - 1].children = []items[items.length - 1].children = items[items.length - 1].children.concat(sublist.items)}idx = sublist.nextIdxcontinue}if (task) {items.push({ type: 'task_item', content: match[3], checked, children: [] })} else {items.push({ type: 'list_item', content: match[ordered ? 3 : 2], children: [], ordered })}idx++}// 返回时,主列表类型以 parentOrdered 为准,否则以第一个元素类型为准let ordered = parentOrderedif (typeof ordered === 'undefined' && items.length > 0) ordered = items[0].ordered// 清理 ordered 字段for (const item of items) delete item.orderedreturn { items, nextIdx: idx, ordered }}while (i < lines.length) {let line = lines[i]// 表格(优先判断,表头和分隔符之间不能有空行)if (/^\|(.+)\|$/.test(line) &&i + 1 < lines.length &&/^\|([ \-:|]+)\|$/.test(lines[i + 1])) {const header = line.replace(/^\||\|$/g, '').split('|').map(s => s.trim())const aligns = lines[i + 1].replace(/^\||\|$/g, '').split('|').map(s => s.trim())let rows = []i += 2while (i < lines.length) {if (/^\s*$/.test(lines[i])) { i++; continue; }if (!/^\|(.+)\|$/.test(lines[i])) breakrows.push(lines[i].replace(/^\||\|$/g, '').split('|').map(s => s.trim()))i++}ast.push({ type: 'table', header, aligns, rows })continue}// blockquote 引用块if (/^>\s?(.*)/.test(line)) {let quoteLines = []while (i < lines.length && /^>\s?(.*)/.test(lines[i])) {quoteLines.push(lines[i].replace(/^>\s?/, ''))i++}ast.push({ type: 'blockquote', content: quoteLines.join('\n') })continue}// 空行if (/^\s*$/.test(line)) {ast.push({ type: 'newline' })i++continue}// 标题let m = line.match(/^(#{1,6})\s+(.*)$/)if (m) {ast.push({ type: 'heading', level: m[1].length, content: m[2] })i++continue}// 代码块if (/^```/.test(line)) {let code = []let lang = line.replace(/^```/, '').trim()i++while (i < lines.length && !/^```/.test(lines[i])) {code.push(lines[i])i++}i++ast.push({ type: 'codeblock', lang, content: code.join('\n') })continue}// 嵌套列表(自动类型判断)if (/^\s*([-\*\+]|\d+\.)\s+/.test(line)) {const { items, nextIdx, ordered } = parseList(i, line.match(/^(\s*)/)[1].length, undefined)ast.push({ type: 'list', ordered, items })i = nextIdxcontinue}// 任务列表(不支持嵌套,原逻辑保留)m = line.match(/^\s*[-\*\+] \[( |x)\] (.*)$/i)if (m) {let items = []while (i < lines.length && /^\s*[-\*\+] \[( |x)\] /.test(lines[i])) {let checked = /\[x\]/i.test(lines[i])items.push({ type: 'task_item', checked, content: lines[i].replace(/^\s*[-\*\+] \[( |x)\] /, '') })i++}ast.push({ type: 'task_list', items })continue}// 普通段落(最后判断)ast.push({ type: 'paragraph', content: line })i++}return ast},// 2. AST -> HTMLrender(md) {if (!md) return ''const ast = typeof md === 'string' ? this.parse(md) : mdif (!Array.isArray(ast)) return ''// 嵌套列表渲染辅助函数function renderList(items, ordered, ctx) {let html = ordered ? '<ol>' : '<ul>'for (const item of items) {if (item.type === 'task_item') {html += `<li><input type="checkbox" disabled${item.checked ? ' checked' : ''}> ${ctx.inline(item.content)}`if (item.children && item.children.length) {html += renderList(item.children, false, ctx)}html += '</li>'} else {html += '<li>' + ctx.inline(item.content)if (item.children && item.children.length) {html += renderList(item.children, ordered, ctx)}html += '</li>'}}html += ordered ? '</ol>' : '</ul>'return html}let html = ''for (const node of ast) {switch (node.type) {case 'heading':html += `<h${node.level}>${this.inline(node.content)}</h${node.level}>`breakcase 'paragraph':html += `<p>${this.inline(node.content)}</p>`breakcase 'codeblock':html += `<pre><code>${this.escape(node.content)}</code></pre>`breakcase 'list':html += renderList(node.items, node.ordered, this)break// 嵌套任务列表已合并到 list 渲染case 'table':const tableStyle = 'border-collapse:collapse;border:1px solid #e5e5e5;width:100%;margin:1em 0;'const thStyle = 'border:1px solid #e5e5e5;padding:8px 12px;text-align:left;background:#fafafa;'const tdStyle = 'border:1px solid #e5e5e5;padding:8px 12px;text-align:left;'html += `<table style="${tableStyle}"><thead><tr>`for (const h of node.header) html += `<th style="${thStyle}">${this.inline(h)}</th>`html += '</tr></thead><tbody>'for (const row of node.rows) {html += '<tr>'for (const c of row) html += `<td style="${tdStyle}">${this.inline(c)}</td>`html += '</tr>'}html += `</tbody></table><br/>`breakcase 'blockquote':html += `<blockquote>${this.inline(node.content).replace(/\n/g, '<br/>')}</blockquote>`breakcase 'newline':html += '<br/>'breakdefault:break}}return html},// 行内语法处理inline(text) {if (!text) return ''return text// 图片 ![alt](url).replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1">')// 删除线.replace(/~~(.*?)~~/g, '<del>$1</del>')// 粗体.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')// 斜体.replace(/\*(.*?)\*/g, '<i>$1</i>')// 行内代码.replace(/`([^`]+)`/g, '<code>$1</code>')// 先处理 [text](url) 链接.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>')// 再处理自动链接(排除已在 a 标签内的).replace(/(^|[^\">])(https?:\/\/[^\s<]+)/g, '$1<a href="$2" target="_blank">$2</a>')},// 代码块转义escape(str) {return str.replace(/[&<>]/g, t => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[t]))}
}
// // 兼容 ES Module 和 CommonJS
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
//   module.exports = { default: nimd }
// }export default nimd

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/88754.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/88754.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/88754.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【echarts踩坑记录】为什么第二个Y轴最大值不整洁

目录问题复现示意图&#xff1a;解决方法有以下几种&#xff1a;1. 在y轴配置中手动设置max属性&#xff1a;2. 使用ECharts提供的坐标轴标签格式化功能&#xff1a;&#x1f680;写在最后问题复现示意图&#xff1a; 今天在用echarts图表的时候&#xff0c;出现了一个小问题。…

Duplicate cleaner pro 的使用技巧

Duplicate cleaner pro 的使用技巧前言文件去重基本介绍经验之谈目录结构修改盘符起因方法手动分配方法‌数据修改方法安装sqlite-web修改数据库GPU加速安装驱动获取驱动和硬件信息安装CUDA配置环境变量&#xff08;如果是自定义安装&#xff09;创建程序<1>获取参数和命…

数字孪生技术引领UI前端设计新趋势:增强现实与虚拟现实的融合应用

hello宝子们...我们是艾斯视觉擅长ui设计和前端数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言&#xff1a;AR 与 VR 的 “割裂” 与数字孪生的 “融合” 契机增强现实&#xff08;AR&…

Qt使用dump文件记录并查找软件奔溃信息详细教程

Qt使用dump文件记录并查找软件奔溃信息一、dump文件概述1、dump文件的基本概念2、dump文件的常见类型3、dump文件的分析工具4、dump文件的应用场景二、具体实现步骤1、下载dbghelp库2、将库添加到自己的工程中3、main.cpp添加代码记录奔溃日志4、编写测试代码5、测试6、结果查看…

UI前端与数字孪生结合案例分享:智慧城市的智慧能源管理系统

hello宝子们...我们是艾斯视觉擅长ui设计、前端开发、数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言&#xff1a;能源管理的 “数字孪生革命”智慧城市的能源系统正面临 “供需失衡、损耗…

Android 16系统源码_SplashScreen窗口的创建流程(一)

一 点击桌面图标触发SplashScreen 1.1 点击桌面图标打开应用 点击桌面的短信图标&#xff0c;然后打开短信页面&#xff0c;使用winscope获取数据。从点击短信图标到应用内容完全展开&#xff0c;中间有出现一个标题带有“Splash Screen”字符串的窗口。 二 Splash Screen窗口创…

线性代数学习笔记

矩阵 矩阵是一种非常重要的数学对象,它通常由一个由数字排成的矩形阵列来定义。一个矩阵由若干行和若干列组成,被称为矩阵的行数和列数。一般情况下,矩阵的行数和列数分别用 n n n 和 m m m 表示。<

2025.7.13总结

每次写日记&#xff0c;总觉得自我感受不是很好&#xff0c;脑子总会有许多消极思想。在网上&#xff0c;我曾看到一个关于“人生是一场巨大的事与愿违”&#xff0c;可能&#xff0c;真的是这个样子吧。以前的我&#xff0c;有上进心&#xff0c;有目标感&#xff0c;脚踏实地…

linux-网络-网络管理发展历程

Linux 的网络管理机制经历了多个阶段的发展&#xff0c;从早期的静态配置到现代动态管理工具的出现&#xff0c;反映了 Linux 系统在网络连接、自动化和用户体验方面的不断演进。以下是 Linux 网络管理发展的主要阶段&#xff1a;1. 早期的静态网络配置&#xff08;传统方式&am…

华为 GaussDB :技术特性、应用局限与市场争议

3-5月间&#xff0c;老夫在某学校带了这门课&#xff0c;简单总结一下课程外的看法&#xff1a;华为 GaussDB 作为华为云生态中的核心数据库产品&#xff0c;自推出以来便承载着华为在数据基础设施领域的战略野心。其技术路线既延续了开源数据库的兼容性优势&#xff0c;又深度…

从零开始学习深度学习—水果分类之PyQt5App

一、项目背景⭐&#xff1a;本项目是“从零开始学习深度学习”系列中的第二个实战项目&#xff0c;旨在实现第一个简易App(图像分类任务——水果分类)&#xff0c;进一步地落地AI模型应用&#xff0c;帮助初学者初步了解模型落地。基于PyQt5图形界面的水果图像分类系统&#xf…

小架构step系列13:测试用例的加载

1 概述测试用例的编写要有一些基础的规范&#xff0c;在本文先定义文件名称和测试用例方法名的规范。2 文件加载原理先从源码来看一下测试用例的文件加载原理。2.1 文件的匹配主要是通过注解来扫描测试用例。// 在IDEA测试用例启动时&#xff0c;调用junit-platform-launcher-x…

K8S的CNI之calico插件升级至3.30.2

前言宿主机ping不通K8S的pod&#xff0c;一直存在丢包的现象&#xff0c;排查了防火墙、日志、详细信息等没发现什么问题&#xff0c;最后搜索发现&#xff0c;是因为把K8S的版本升级之后&#xff0c;旧版本的CNI插件不适配原因导致的&#xff0c;于是就把calico也一并升级并且…

Spring Boot RESTful API 设计指南:查询接口规范与最佳实践

Spring Boot RESTful API 设计指南&#xff1a;查询接口规范与最佳实践 引言 在 Spring Boot 开发中&#xff0c;查询接口的设计直接影响着系统的可用性、可维护性和性能。本文将深入探讨如何规范设计查询接口&#xff0c;包括 GET/POST 的选择、参数定义、校验规则等&#xff…

ctfshow萌新题集

记录一下前半部分是能自己写出来的&#xff0c;后半部分是需要提示的&#xff0c;感觉自己归来两年仍是萌新 misc部分 知识点 base家族密文特征 Base16 (Hex) 字符集&#xff1a;0-9, A-F&#xff08;不区分大小写&#xff09;。特征&#xff1a; 长度是 2 的倍数&#xff…

2025年语言处理、大数据与人机交互国际会议(DHCI 2025)

&#x1f310;&#x1f916;&#x1f9e0; 语言处理、大数据与人机交互&#xff1a;探索智能未来 —— DHCI 2025国际会议2025年语言处理、大数据与人机交互国际会议&#xff08;DHCI 2025&#xff09; 将于2025年在中国重庆市召开。这次盛会将汇聚全球顶尖专家、学者及行业领袖…

RIP实验以及核心原理

RIP&#xff08;Routing Information Protocol&#xff0c;路由信息协议&#xff09;是一种内部网关协议&#xff0c;基于距离矢量算法&#xff0c;用于在自治系统内交换路由信息。RIP 核心原理距离矢量算法&#xff1a;RIP 使用跳数作为路径选择的唯一度量标准。每经过一个路由…

基于大数据的电力系统故障诊断技术研究

摘要本文提出了一种创新性的基于大数据技术的电力系统故障诊断方法&#xff0c;该方法通过整合先进的机器学习算法和交互式可视化技术&#xff0c;实现了对电力系统各类故障的智能化识别与深度分析。该系统采用随机森林算法作为核心分类器&#xff0c;构建了高精度的故障分类模…

MySQL 分区功能应用专门实现全方位详解与示例

MySQL 分区功能允许将表的数据分散存储在不同的物理分区中,同时保持逻辑上的单一表结构。下面我将从基础概念到高级应用,全面讲解 MySQL 分区实现。 一、分区核心作用 1. 性能提升 分区剪枝(Partition Pruning):查询时自动跳过不相关的分区,减少数据扫描量 并行处理:不…

汽车功能安全-嵌入式软件测试(软件合格性测试)【目的、验证输入、集成验证要求】11

文章目录1 嵌入式软件测试&#xff08;Testing of the embedded Software&#xff09;2 测试输入3 验证要求和建议3.1 测试环境3.2 测试方法3.2.1 基于需求的测试3.2.2 故障注入测试3.2.3 两种方法的区别与联系总结3.3 测试用例导出方法4 嵌入式软件的测试结果评价5 测试输出物…