| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- <!-- uniapp vue3 markdown解析 -->
- <template>
- <view class="ua__markdown">
- <rich-text :nodes="parsedContent" type="text" mode="web" space="nbsp"></rich-text>
- <view class="links" v-if="linkTmp != '' || linkMaps.length > 0">
- <view class="links-title">参考链接:</view>
- <template v-for="item in linkMaps">
- <view class="link-item" @click="copyLink(item.href)">{{item.text}}</view>
- </template>
- <view v-if="linkTmp != ''" class="link-item">{{linkTmp}}</view>
- </view>
- </view>
- </template>
- <script setup>
- import {
- ref,
- watch
- } from 'vue'
- import MarkdownIt from './lib/markdown-it.min.js'
- import hljs from './lib/highlight/uni-highlight.min.js'
- import './lib/highlight/atom-one-dark.css'
- import parseHtml from './lib/html-parser.js'
- const props = defineProps({
- // 解析内容
- source: String,
- showLine: {
- type: [Boolean, String],
- default: true
- }
- })
- // 存储解析后的内容
- const parsedContent = ref('');
- // 临时链接缓存
- const linkTmp = ref('');
- const clearLinkTmpTimeout = ref(null);
- // 存储链接数据映射
- const linkMaps = ref([]);
- // 解析富文本内容,提取并替换a标签
- const parseContent = () => {
- if (!props.source) return
- // 用于生成唯一ID的计数器
- let linkIdCounter = 0;
- let parseNodeStr = parseNodes(props.source);
- parseNodeStr = parseNodeStr.replace(/[\n\r]/g, '')
- linkMaps.value = [];
- const linkLine = /<p[^>]*>(?:<[^>]+>)*\s*参考链接(.*?)<\/p>/gi;
- // 匹配a标签的正则表达式
- const linkRegex = /<a[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>/gi;
- parsedContent.value = parseNodeStr.replace(linkLine, (match, content) => {
- content = content.replace(linkRegex, (match, href, text) => {
- // 存储链接信息
- linkMaps.value.push({
- text: text,
- href: href.replace(/&/g, "&")
- });
- return '';
- });
- linkTmp.value = content.replace(/<[^>]*>/g, '');
- return '';
- });
- if (clearLinkTmpTimeout.value) {
- clearTimeout(clearLinkTmpTimeout.value);
- }
- // 提取单独的链接(使用 exec 循环代替 matchAll,兼容小程序环境)
- const linkRegex2 = /<a[^>]+href="([^"]+)"[^>]*>(.*?)<\/a>/gi;
- let match;
- while ((match = linkRegex2.exec(parsedContent.value)) !== null) {
- linkMaps.value.push({
- text: match[2],
- href: match[1].replace(/&/g, "&")
- });
- }
- clearLinkTmpTimeout.value = setTimeout(() => {
- linkTmp.value = '';
- }, 60);
- }
- // 处理点击事件
- const copyLink = (link) => {
- uni.setClipboardData({
- data: link,
- success: () => {
- uni.showToast({
- title: "复制成功",
- icon: 'none'
- });
- },
- fail: () => {
- uni.showToast({
- title: "复制失败",
- icon: 'none'
- });
- }
- });
- }
- watch(() => props.source, () => {
- parseContent();
- });
- const markdown = MarkdownIt({
- html: true,
- highlight: function(str, lang) {
- let preCode = ""
- try {
- preCode = hljs.highlightAuto(str).value
- } catch (err) {
- preCode = markdown.utils.escapeHtml(str);
- }
- const lines = preCode.split(/\n/).slice(0, -1)
- // 添加自定义行号
- let html = lines.map((item, index) => {
- if (item == '') {
- return ''
- }
- return '<li><span class="line-num" data-line="' + (index + 1) + '"></span>' + item +
- '</li>'
- }).join('')
- if (props.showLine) {
- html = '<ol style="padding: 0px 30px;">' + html + '</ol>'
- } else {
- html = '<ol style="padding: 0px 7px;list-style:none;">' + html + '</ol>'
- }
- copyCodeData.push(str)
- let htmlCode = `<div class="markdown-wrap">`
- // #ifndef MP-WEIXIN
- htmlCode += `<div style="color: #aaa;text-align: right;font-size: 12px;padding:8px;">`
- htmlCode +=
- `${lang}<a class="copy-btn" code-data-index="${copyCodeData.length - 1}" style="margin-left: 8px;">复制代码</a>`
- htmlCode += `</div>`
- // #endif
- htmlCode +=
- `<pre class="hljs" style="padding:10px 8px 0;margin-bottom:5px;overflow: auto;display: block;border-radius: 5px;"><code>${html}</code></pre>`;
- htmlCode += '</div>'
- return htmlCode
- }
- })
- const parseNodes = (value) => {
- if (!value) return
- // 解析<br />到\n
- value = value.replace(/<br>|<br\/>|<br \/>/g, "\n")
- value = value.replace(/ /g, " ")
- let htmlString = ''
- if (value.split("```").length % 2) {
- let mdtext = value
- if (mdtext[mdtext.length - 1] != '\n') {
- mdtext += '\n'
- }
- htmlString = markdown.render(mdtext)
- } else {
- htmlString = markdown.render(value)
- }
-
- // 小程序 rich-text 不继承外部CSS,必须使用内联样式
- // 表格样式
- htmlString = htmlString.replace(/<table>/g, `<table style="border-collapse:collapse;width:100%;margin:0.8em 0;border:1px solid #e5e5e5;">`)
- htmlString = htmlString.replace(/<tr>/g, `<tr style="border-top:1px solid #e5e5e5;">`)
- htmlString = htmlString.replace(/<th>/g, `<th style="padding:6px 13px;border:1px solid #e5e5e5;font-weight:600;background-color:#f5f5f5;">`)
- htmlString = htmlString.replace(/<td>/g, `<td style="padding:6px 13px;border:1px solid #e5e5e5;">`)
- htmlString = htmlString.replace(/<hr>|<hr\/>|<hr \/>/g, `<hr style="margin:1em 0;border:0;border-top:1px solid #e5e5e5;">`)
-
- // 列表样式
- htmlString = htmlString.replace(/<ul>/g, `<ul style="padding-left:20px;margin:0.5em 0;list-style-type:disc;">`)
- htmlString = htmlString.replace(/<ol>/g, `<ol style="padding-left:20px;margin:0.5em 0;list-style-type:decimal;">`)
- htmlString = htmlString.replace(/<li>/g, `<li style="line-height:1.6;margin-bottom:0.2em;">`)
-
- // 标题样式
- htmlString = htmlString.replace(/<h1>/g, `<h1 style="font-size:1.8em;margin:1em 0 0.5em;font-weight:bold;line-height:1.3;">`)
- htmlString = htmlString.replace(/<h2>/g, `<h2 style="font-size:1.5em;margin:1em 0 0.5em;font-weight:bold;line-height:1.3;">`)
- htmlString = htmlString.replace(/<h3>/g, `<h3 style="font-size:1.25em;margin:0.8em 0 0.4em;font-weight:bold;line-height:1.3;">`)
- htmlString = htmlString.replace(/<h4>/g, `<h4 style="font-size:1.1em;margin:0.8em 0 0.4em;font-weight:bold;line-height:1.3;">`)
- htmlString = htmlString.replace(/<h5>/g, `<h5 style="font-size:1em;margin:0.6em 0 0.3em;font-weight:bold;line-height:1.3;">`)
- htmlString = htmlString.replace(/<h6>/g, `<h6 style="font-size:0.9em;margin:0.6em 0 0.3em;font-weight:bold;line-height:1.3;">`)
-
- // 段落样式
- htmlString = htmlString.replace(/<p>/g, `<p style="margin:0.5em 0;line-height:1.6;">`)
-
- // 引用块样式
- htmlString = htmlString.replace(/<blockquote>/g, `<blockquote style="padding:10px 15px;margin:0.8em 0;border-left:4px solid #ddd;color:#666;background:#f9f9f9;">`)
-
- // 行内代码样式
- htmlString = htmlString.replace(/<code>(?![\s\S]*<\/code>[\s\S]*<code>)/g, `<code style="padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:3px;font-family:monospace;">`)
-
- // 链接样式
- htmlString = htmlString.replace(/<a /g, `<a style="color:#3498db;text-decoration:underline;" `)
-
- // 强调样式
- htmlString = htmlString.replace(/<strong>/g, `<strong style="font-weight:bold;">`)
- htmlString = htmlString.replace(/<em>/g, `<em style="font-style:italic;">`)
- // #ifndef APP-NVUE
- return htmlString
- // #endif
- // 将htmlString转成htmlArray,反之使用rich-text解析
- // #ifdef APP-NVUE
- return parseHtml(htmlString)
- // #endif
- }
- const extractPlainText = (markdown) => {
- const md = new MarkdownIt({
- // 禁用所有HTML标签渲染
- html: false,
- // 禁用自动链接识别
- linkify: false,
- // 禁用自动换行转换
- breaks: false,
- // 禁用typographer转换(如引号转换)
- typographer: false
- });
- // 自定义渲染器,只返回文本内容
- const originalRender = md.renderer.rules.text;
- md.renderer.rules.text = function(tokens, idx, options, env, self) {
- return originalRender ? originalRender(tokens, idx, options, env, self) : tokens[idx].content;
- };
- // 处理各种token类型
- md.renderer.rules.link_open = md.renderer.rules.link_close =
- md.renderer.rules.strong_open = md.renderer.rules.strong_close =
- md.renderer.rules.em_open = md.renderer.rules.em_close =
- md.renderer.rules.heading_open = md.renderer.rules.heading_close =
- md.renderer.rules.paragraph_open = md.renderer.rules.paragraph_close =
- md.renderer.rules.list_item_open = md.renderer.rules.list_item_close =
- md.renderer.rules.ordered_list_open = md.renderer.rules.ordered_list_close =
- md.renderer.rules.bullet_list_open = md.renderer.rules.bullet_list_close =
- md.renderer.rules.code_block = md.renderer.rules.fence =
- md.renderer.rules.blockquote_open = md.renderer.rules.blockquote_close =
- function(tokens, idx, options, env, self) {
- return '';
- };
- // 处理图片(完全忽略)
- md.renderer.rules.image = function() {
- return '';
- };
- // 渲染并清理结果
- return md.render(markdown)
- .replace(/\s+/g, ' ')
- .trim();
- }
- /**
- * 复制文字内容
- */
- const copyContent = () => {
- const content = extractPlainText(props.source);
- uni.setClipboardData({
- data: content,
- success: () => {
- uni.showToast({
- title: '复制成功',
- icon: 'none'
- });
- }
- })
- }
- defineExpose({
- copyContent,
- extractPlainText
- });
- </script>
- <style lang="scss">
- /* 小程序 rich-text 不继承外部CSS,内部元素样式通过内联方式在 parseNodes 中处理 */
- /* 此处仅保留容器样式和非 rich-text 内的元素样式 */
- .ua__markdown {
- font-size: 14px;
- line-height: 1.6;
- word-break: break-all;
- color: #333;
- .links {
- margin-top: 16rpx;
-
- .links-title {
- color: #101333;
- margin-bottom: 8rpx;
- font-weight: bold;
- font-size: 28rpx;
- }
- .link-item {
- font-size: 24rpx;
- color: #3498db;
- text-decoration: underline;
- margin-bottom: 6rpx;
- }
- }
- }
- </style>
|