Files
Mycontent/miniprogram/utils/contentParser.js
乘风 73d56a225a feat: enhance link handling in reading page
- Added functionality to open article links in a mini program web-view, allowing users to preview external links.
- Implemented mobile hints for external links to improve user experience on mobile devices.
- Updated content parser to support new link types, including regular hyperlinks.
- Enhanced styling for article links to improve visibility and interaction.

Made-with: Cursor
2026-04-24 16:17:07 +08:00

604 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 卡若创业派对 - 内容解析工具
* 解析 TipTap HTML 为阅读页可展示的 segments
*
* segment 类型:
* { type: 'text', text }
* { type: 'mention', userId, nickname } — @某人,点击加好友(提交存客宝见 utils/soulBridge.submitCkbLead
* { type: 'linkTag', label, url, ... } — #链接标签,点击跳转(阅读页 onLinkTagTap外链→link-preview、小程序→navigateToMiniProgram
* { type: 'link', text, url } — 普通超链接,点击外链预览(阅读页 onArticleLinkTap
* { type: 'image', src, alt } — 图片
* { type: 'video', src } — 内嵌视频(管理端 rich-video-wrap / <video>
*/
/** 判断内容是否为 HTML */
function isHtmlContent(content) {
if (!content || typeof content !== 'string') return false
const trimmed = content.trim()
return trimmed.includes('<') && trimmed.includes('>') && /<[a-z][^>]*>/i.test(trimmed)
}
/** 解码常见 HTML 实体 */
function decodeEntities(str) {
return str
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
}
/**
* 正文里 img 的 src管理端常见为 /uploads/...(相对 API 根),浏览器会自动补全域名;
* 小程序 <image> 不会拼接 baseUrl需在此用 assetBase 转成可请求的绝对地址。
* @param {string} src
* @param {string} [assetBase] - 如 https://soulapi.example.com无末尾 /
*/
function resolveArticleImageSrc(src, assetBase) {
if (!src || typeof src !== 'string') return src
const s = src.trim()
if (!s) return s
if (/^(https?:|wxfile:|data:|blob:)/i.test(s)) return s
if (s.startsWith('//')) return 'https:' + s
if (s.startsWith('/')) {
const base = String(assetBase || '').replace(/\/$/, '')
if (!base) return s
return base + s
}
return s
}
/** 从 <img ...> 标签取出 src双引号/单引号) */
function extractImgSrcFromTag(tag) {
const d = tag.match(/src\s*=\s*"([^"]*)"/i)
if (d && d[1] != null) return d[1]
const q = tag.match(/src\s*=\s*'([^']*)'/i)
if (q && q[1] != null) return q[1]
return ''
}
/** 从 <video ...> 或包含 video 的 HTML 片段取出首个 src */
function extractVideoSrcFromTag(tag) {
const vm = tag.match(/<video[^>]*>/i)
const chunk = vm ? vm[0] : tag
return extractImgSrcFromTag(chunk)
}
function pushResolvedVideoSrc(videos, rawSrc, config) {
if (!rawSrc || typeof rawSrc !== 'string') return
const decoded = decodeEntities(rawSrc.trim())
if (!decoded) return
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
videos.push({ src })
}
/**
* 单行展示用:昵称、#标签文案、章节外标题类字段 — 合并换行、<br>、连续空白(避免 TipTap/粘贴带入异常断行)
*/
function cleanSingleLineField(s) {
if (!s && s !== 0) return ''
let t = decodeEntities(String(s))
.replace(/<br\s*\/?>/gi, ' ')
.replace(/\r\n|\r|\n/g, ' ')
.replace(/[\s\u00a0\u200b\u200c\u200d\ufeff\u3000]+/g, ' ')
.trim()
return t
}
/** @提及昵称:去首尾空白、零宽、全角空格;合并内部换行/<br> */
function cleanMentionNickname(n) {
return cleanSingleLineField(n)
}
/** 纯文本在 mention 节点前若已有「@」,去掉末尾 @,避免渲染成「找@@阿浪」 */
function stripTrailingAtForMention(before) {
return before.replace(/[@][\s\u00a0\u200b]*$/u, '')
}
/**
* 将一个 HTML block 字符串解析为 segments 数组
* 处理内联元素mention / linkTag(span) / linkTag(a) / link(a) / img / video
*/
function parseBlockToSegments(block, config) {
const segs = []
const normalize = s => (s || '').trim().toLowerCase()
const personTokenSet = new Set()
for (const p of ((config && config.persons) || [])) {
const token = normalize((p && p.token) || '')
if (token) personTokenSet.add(token)
}
// 合并匹配所有内联元素
const tokenRe = /<span[^>]*data-type="mention"[^>]*>[\s\S]*?<\/span>|<span[^>]*data-type="linkTag"[^>]*>[\s\S]*?<\/span>|<a[^>]*href=(?:"[^"]*"|'[^']*')[^>]*>[\s\S]*?<\/a>|<video[^>]*>[\s\S]*?<\/video>|<video[^>]*\/>|<img[^>]*\/?>/gi
let lastEnd = 0
let m
while ((m = tokenRe.exec(block)) !== null) {
// 前置纯文本mention 紧挨手写「找@」时去掉重复 @
let before = decodeEntities(block.slice(lastEnd, m.index).replace(/<[^>]+>/g, ''))
const tag = m[0]
if (/data-type="mention"/i.test(tag)) {
before = stripTrailingAtForMention(before)
}
if (before.trim()) segs.push({ type: 'text', text: before })
if (/data-type="mention"/i.test(tag)) {
// @mention — TipTap mention spanspan 内常见「@ 昵称」多空格,统一紧挨显示)
const idMatch = tag.match(/data-id="([^"]*)"/)
const labelMatch = tag.match(/data-label="([^"]*)"/)
const innerText = tag.replace(/<[^>]+>/g, '')
const userId = idMatch ? idMatch[1].trim() : ''
let nickname = labelMatch ? labelMatch[1] : innerText.replace(/^[@]\s*/, '')
nickname = cleanMentionNickname((nickname || '').trim())
const userExists = !!normalize(userId) && personTokenSet.has(normalize(userId))
if (userExists && nickname) {
segs.push({ type: 'mention', userId, nickname, mentionDisplay: '@' + nickname })
} else if (nickname) {
// 被 @ 人物不存在时降级为普通文本,保持“静态 @某人”展示
segs.push({ type: 'text', text: '@' + nickname })
}
} else if (/data-type="linkTag"/i.test(tag)) {
// #linkTag — 自定义 span 格式data-type="linkTag" data-url="..." data-tag-type="..." data-page-path="..." data-app-id="..."
const urlMatch = tag.match(/data-url="([^"]*)"/)
const tagTypeMatch = tag.match(/data-tag-type="([^"]*)"/)
const pagePathMatch = tag.match(/data-page-path="([^"]*)"/)
const tagIdMatch = tag.match(/data-tag-id="([^"]*)"/)
const appIdMatch = tag.match(/data-app-id="([^"]*)"/)
const mpKeyMatch = tag.match(/data-mp-key="([^"]*)"/)
const passPhoneMatch = tag.match(/data-pass-phone="([^"]*)"/)
const phoneParamMatch = tag.match(/data-phone-param-name="([^"]*)"/)
const innerText = cleanSingleLineField(tag.replace(/<[^>]+>/g, '').replace(/^#/, ''))
const url = urlMatch ? urlMatch[1] : ''
const tagType = tagTypeMatch ? tagTypeMatch[1] : 'url'
const pagePath = pagePathMatch ? pagePathMatch[1] : ''
const tagId = tagIdMatch ? tagIdMatch[1] : ''
const appId = appIdMatch ? appIdMatch[1] : ''
const mpKey = mpKeyMatch ? mpKeyMatch[1] : ''
const passPhone = !!(passPhoneMatch && (passPhoneMatch[1] === '1' || /^true$/i.test(passPhoneMatch[1])))
const phoneParamName = (phoneParamMatch && phoneParamMatch[1]) ? phoneParamMatch[1] : 'phone'
segs.push({ type: 'linkTag', label: innerText || '#', url, tagType, pagePath, tagId, appId, mpKey, passPhone, phoneParamName })
} else if (/^<a /i.test(tag)) {
// <a href>:兼容旧 #linkTag 与普通超链接
const hrefMatch =
tag.match(/href\s*=\s*"([^"]*)"/i) ||
tag.match(/href\s*=\s*'([^']*)'/i)
const url = hrefMatch ? decodeEntities((hrefMatch[1] || '').trim()) : ''
const anchorText = decodeEntities((tag.replace(/<[^>]+>/g, '') || '').trim())
// 旧格式:#标签仍走 linkTag 逻辑,复用现有 onLinkTagTap
if (/^[#]/u.test(anchorText)) {
const label = cleanSingleLineField(anchorText.replace(/^[#]/u, ''))
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
} else {
// 普通链接:展示锚文本;无锚文本则回退展示 url
segs.push({ type: 'link', text: anchorText || url, url })
}
} else if (/^<video/i.test(tag)) {
const rawSrc = extractVideoSrcFromTag(tag)
if (rawSrc) {
const decoded = decodeEntities(rawSrc)
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
segs.push({ type: 'video', src })
}
} else if (/^<img /i.test(tag)) {
// 图片src 可能为相对路径,需结合 config.assetBase
const rawSrc = extractImgSrcFromTag(tag)
const altMatch = tag.match(/alt\s*=\s*"([^"]*)"/i) || tag.match(/alt\s*=\s*'([^']*)'/i)
if (rawSrc) {
const decoded = decodeEntities(rawSrc)
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
segs.push({ type: 'image', src, alt: altMatch ? decodeEntities(altMatch[1]) : '' })
}
}
lastEnd = m.index + tag.length
}
// 尾部纯文本
const after = decodeEntities(block.slice(lastEnd).replace(/<[^>]+>/g, ''))
if (after.trim()) segs.push({ type: 'text', text: after })
return segs
}
/**
* 解析 <table> HTML 为结构化数据 { headers: string[], rows: string[][] }
*/
function parseTableHtml(tableHtml) {
const stripTags = s => decodeEntities((s || '').replace(/<[^>]+>/g, '').trim())
const headers = []
const rows = []
const headMatch = tableHtml.match(/<thead[^>]*>([\s\S]*?)<\/thead>/i)
if (headMatch) {
const thRe = /<th[^>]*>([\s\S]*?)<\/th>/gi
let m
while ((m = thRe.exec(headMatch[1])) !== null) {
headers.push(stripTags(m[1]))
}
}
const bodyMatch = tableHtml.match(/<tbody[^>]*>([\s\S]*?)<\/tbody>/i)
const bodyContent = bodyMatch ? bodyMatch[1] : tableHtml
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi
let trM
let isFirst = !headMatch
while ((trM = trRe.exec(bodyContent)) !== null) {
if (headMatch && trM[0] === (headMatch[0].match(/<tr[^>]*>[\s\S]*?<\/tr>/i) || [])[0]) continue
const cells = []
const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi
let cM
while ((cM = cellRe.exec(trM[1])) !== null) {
cells.push(stripTags(cM[1]))
}
if (!cells.length) continue
if (isFirst && !headers.length) {
headers.push(...cells)
isFirst = false
} else {
rows.push(cells)
}
}
return { headers, rows }
}
/**
* 从 HTML 中解析出 lines纯文本行和 segments含富文本片段
* segment 新增类型heading / quote / listItem
* @param {string} html
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string }
*/
function parseHtmlToSegments(html, config) {
const lines = []
const segments = []
// 0. 提取 <table> 块
const tables = []
let text = html.replace(/<table[^>]*>[\s\S]*?<\/table>/gi, (match) => {
const idx = tables.length
tables.push(parseTableHtml(match))
return '\n__TABLE_' + idx + '__\n'
})
// 0.5 管理端 TipTapdiv.rich-video-wrap > video +可选caption须在剥离 div 前整体替换
const videos = []
const videoWrapRe =
/<div[^>]*(?:class="[^"]*rich-video-wrap[^"]*"|class='[^']*rich-video-wrap[^']*')[^>]*>\s*<video[^>]*>\s*<\/video>\s*(?:<div[^>]*(?:class="[^"]*rich-video-caption[^"]*"|class='[^']*rich-video-caption[^']*')[^>]*>[\s\S]*?<\/div>)?\s*<\/div>/gi
text = text.replace(videoWrapRe, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
// 未包在 rich-video-wrap 内的裸 <video>(兼容粘贴或其它导出)
text = text.replace(/<video[^>]*\/>/gi, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
text = text.replace(/<video[^>]*>\s*<\/video>/gi, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
// 1. 提取 <h2>/<h3> → heading 占位
const headings = []
text = text.replace(/<h([2-6])[^>]*>([\s\S]*?)<\/h\1>/gi, function (_, lvl, inner) {
const idx = headings.length
headings.push({ level: parseInt(lvl, 10), raw: inner.trim() })
return '\n__H_' + idx + '__\n'
})
// 2. 提取 <blockquote> → quote 占位
const quotes = []
text = text.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, function (_, inner) {
const idx = quotes.length
quotes.push(inner.trim())
return '\n__Q_' + idx + '__\n'
})
// 3. 列表 → listItem 占位(保留 ordered 标记)
var olDepth = 0
var olCounter = 0
text = text.replace(/<ol[^>]*>/gi, function () { olDepth++; olCounter = 0; return '\n' })
text = text.replace(/<\/ol>/gi, function () { olDepth = Math.max(0, olDepth - 1); return '\n' })
text = text.replace(/<ul[^>]*>/gi, '\n')
text = text.replace(/<\/ul>/gi, '\n')
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, function (_, inner) {
var cleaned = inner.replace(/<p[^>]*>/gi, '').replace(/<\/p>/gi, '').trim()
if (!cleaned) return '\n'
if (olDepth > 0) {
olCounter++
return '\n__LI_O_' + olCounter + '__ ' + cleaned + '\n'
}
return '\n__LI_U__ ' + cleaned + '\n'
})
// 4. 块级标签换行
text = text.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
text = text.replace(/<p[^>]*>/gi, '')
text = text.replace(/<\/p>/gi, '\n')
text = text.replace(/<div[^>]*>/gi, '')
text = text.replace(/<\/div>/gi, '\n')
text = text.replace(/<br\s*\/?>/gi, '\n')
text = text.replace(/<hr\s*\/?>/gi, '\n')
// 5. 逐段解析
var blocks = text.split(/\n+/)
for (var bi = 0; bi < blocks.length; bi++) {
var block = blocks[bi]
if (!block.trim()) continue
// table
var tblM = block.trim().match(/^__TABLE_(\d+)__$/)
if (tblM) {
var tbl = tables[parseInt(tblM[1], 10)]
if (tbl) {
lines.push([tbl.headers.join(' | '), ...tbl.rows.map(function (r) { return r.join(' | ') })].join('\n'))
segments.push([{ type: 'table', headers: tbl.headers, rows: tbl.rows }])
}
continue
}
// video整块占位
var vidM = block.trim().match(/^__VIDEO_(\d+)__$/)
if (vidM) {
var v = videos[parseInt(vidM[1], 10)]
if (v && v.src) {
lines.push('')
segments.push([{ type: 'video', src: v.src }])
}
continue
}
// heading
var hM = block.trim().match(/^__H_(\d+)__$/)
if (hM) {
var h = headings[parseInt(hM[1], 10)]
if (h) {
var hText = decodeEntities(h.raw.replace(/<[^>]+>/g, '').trim())
lines.push(hText)
segments.push([{ type: 'heading', level: h.level, text: hText }])
}
continue
}
// quote
var qM = block.trim().match(/^__Q_(\d+)__$/)
if (qM) {
var qHtml = quotes[parseInt(qM[1], 10)]
if (qHtml) {
var qText = decodeEntities(
qHtml.replace(/<\/?p[^>]*>/gi, '\n').replace(/<[^>]+>/g, '').trim()
).replace(/\n{2,}/g, '\n')
lines.push(qText)
segments.push([{ type: 'quote', text: qText }])
}
continue
}
// ordered list item
var liOM = block.match(/^__LI_O_(\d+)__\s*(.*)/)
if (liOM) {
var liNum = parseInt(liOM[1], 10)
var liContent = liOM[2].trim()
var liSegs = parseBlockToSegments(liContent, config)
var liText = decodeEntities(liContent.replace(/<[^>]+>/g, '').trim())
lines.push(liNum + '. ' + liText)
segments.push([{ type: 'listItem', ordered: true, number: liNum, text: liText, segs: liSegs }])
continue
}
// unordered list item
var liUM = block.match(/^__LI_U__\s*(.*)/)
if (liUM) {
var liContent2 = liUM[1].trim()
var liSegs2 = parseBlockToSegments(liContent2, config)
var liText2 = decodeEntities(liContent2.replace(/<[^>]+>/g, '').trim())
lines.push(liText2)
segments.push([{ type: 'listItem', ordered: false, text: liText2, segs: liSegs2 }])
continue
}
// 普通段落
var blockSegs = parseBlockToSegments(block, config)
if (!blockSegs.length) continue
if (
blockSegs.length === 1 &&
(blockSegs[0].type === 'image' || blockSegs[0].type === 'video')
) {
lines.push('')
segments.push(blockSegs)
continue
}
if (config && (config.persons?.length || config.linkTags?.length)) {
var expanded = []
for (var si = 0; si < blockSegs.length; si++) {
var seg = blockSegs[si]
if (seg.type === 'text' && seg.text) {
expanded.push.apply(expanded, matchLineToSegments(seg.text, config))
} else {
expanded.push(seg)
}
}
blockSegs = expanded
}
var lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
lines.push(lineText)
segments.push(blockSegs)
}
return { lines, segments }
}
/** 清理 Markdown 格式标记(**加粗** *斜体* __加粗__ _斜体_ ~~删除线~~ `代码` 等)*/
function stripMarkdownFormatting(text) {
if (!text) return text
let s = text
s = s.replace(/^#{1,6}\s+/gm, '')
s = s.replace(/\*\*(.+?)\*\*/g, '$1')
s = s.replace(/__(.+?)__/g, '$1')
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '$1')
s = s.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '$1')
s = s.replace(/~~(.+?)~~/g, '$1')
s = s.replace(/`([^`]+)`/g, '$1')
s = s.replace(/^>\s+/gm, '')
s = s.replace(/^---$/gm, '')
s = s.replace(/^\* /gm, '• ')
s = s.replace(/^- /gm, '• ')
s = s.replace(/^\d+\.\s/gm, '')
return s
}
/**
* 对一行纯文本进行 @人名 / #标签 自动匹配,返回 segments 数组
* config: { persons: [{ personId, token, name, label, aliases }], linkTags: [...] }
* 点击加好友时须传 persons.token与 CKB lead 的 targetUserId 一致),不能用 personId。
*/
function matchLineToSegments(line, config) {
if (!config || (!config.persons?.length && !config.linkTags?.length)) {
return [{ type: 'text', text: line }]
}
// 编辑器/系统在 @ 与人名之间插入的普通空格,合并为紧挨 @(避免「找@ 阿浪」无法匹配人名)
line = line.replace(/([@])\s+(?=[\u4e00-\u9fffA-Za-z0-9_\u00b7])/g, '$1')
const normalize = s => (s || '').trim().toLowerCase()
const personMap = {}
const tagMap = {}
for (const p of (config.persons || [])) {
const token = (p.token || '').trim()
if (!token) continue
const display = (p.name || p.label || '').trim()
const aliasStr = p.aliases != null ? String(p.aliases) : ''
const keys = [display, p.label, ...(aliasStr ? aliasStr.split(',') : [])]
.map((x) => (x != null ? String(x) : '').trim())
.filter(Boolean)
.map(normalize)
.filter(Boolean)
for (const k of keys) {
if (!personMap[k]) personMap[k] = p
}
}
for (const t of (config.linkTags || [])) {
const keys = [t.label, ...(t.aliases ? t.aliases.split(',') : [])].map(normalize).filter(Boolean)
for (const k of keys) { if (!tagMap[k]) tagMap[k] = t }
}
const esc = n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const personNames = Object.keys(personMap).sort((a, b) => b.length - a.length).map(esc)
const tagLabels = Object.keys(tagMap).sort((a, b) => b.length - a.length).map(esc)
if (!personNames.length && !tagLabels.length) return [{ type: 'text', text: line }]
const parts = []
if (personNames.length) parts.push('[@]\\s*(' + personNames.join('|') + ')')
if (tagLabels.length) parts.push('[#]\\s*(' + tagLabels.join('|') + ')')
const pattern = new RegExp(parts.join('|'), 'gi')
const segs = []
let lastEnd = 0
let m
while ((m = pattern.exec(line)) !== null) {
if (m.index > lastEnd) {
segs.push({ type: 'text', text: line.slice(lastEnd, m.index) })
}
const full = m[0]
if (/^[@]/u.test(full)) {
const body = full.replace(/^[@]\s*/u, '')
const person = personMap[normalize(body)]
if (person) {
const nick = cleanSingleLineField(person.name || person.label || body)
const uid = (person.token || '').trim()
if (uid) {
segs.push({ type: 'mention', userId: uid, nickname: nick, mentionDisplay: '@' + nick })
} else {
segs.push({ type: 'text', text: full })
}
} else {
segs.push({ type: 'text', text: full })
}
} else {
const body = full.replace(/^[#]\s*/u, '')
const tag = tagMap[normalize(body)]
if (tag) {
segs.push({
type: 'linkTag',
label: tag.label || body,
url: tag.url || '',
tagType: tag.type || 'url',
pagePath: tag.pagePath || '',
tagId: tag.tagId || '',
appId: tag.appId || '',
mpKey: tag.mpKey || '',
passPhone: !!tag.passPhone,
phoneParamName: tag.phoneParamName || 'phone'
})
} else {
segs.push({ type: 'text', text: full })
}
}
lastEnd = m.index + full.length
}
if (lastEnd < line.length) {
segs.push({ type: 'text', text: line.slice(lastEnd) })
}
return segs.length ? segs : [{ type: 'text', text: line }]
}
/** 纯文本/Markdown 按行解析 */
function parsePlainTextToSegments(text, config) {
const cleaned = stripMarkdownFormatting(text)
const lines = cleaned.split('\n').map(l => l.trim()).filter(l => l.length > 0)
const segments = lines.map(line => matchLineToSegments(line, config))
return { lines, segments }
}
/** 清理残留的 Markdown 图片引用文本(如 "image.png![](xxx)" */
function stripOrphanImageRefs(text) {
if (!text) return text
text = text.replace(/[^\s]*\.(?:png|jpg|jpeg|gif|webp|svg|bmp)!\[[^\]]*\]\([^)]*\)/gi, '')
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
return text
}
/**
* 将原始内容解析为 contentSegments用于阅读页展示
* @param {string} rawContent
* @param {object} [config] - { persons: [], linkTags: [] }
* @returns {{ lines: string[], segments: Array<Array<segment>> }}
*/
function parseContent(rawContent, config) {
if (!rawContent || typeof rawContent !== 'string') {
return { lines: [], segments: [] }
}
let content = stripOrphanImageRefs(rawContent)
if (isHtmlContent(content)) {
return parseHtmlToSegments(content, config)
}
return parsePlainTextToSegments(content, config)
}
module.exports = {
parseContent,
isHtmlContent,
cleanSingleLineField,
}