chore: 以本地工作区为准全量快照同步 GitHub
包含小程序、管理端、soul-api、脚本与静态资源等当前本地全部已跟踪与新增文件(排除 .DS_Store 与 .obsidian)。 Made-with: Cursor
This commit is contained in:
@@ -174,52 +174,189 @@ function parseBlockToSegments(block, config) {
|
||||
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 },用于对 text 段自动匹配 @人名 / #标签;assetBase 用于补全图片相对 URL
|
||||
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string }
|
||||
*/
|
||||
function parseHtmlToSegments(html, config) {
|
||||
const lines = []
|
||||
const segments = []
|
||||
|
||||
// 1. 块级标签换行,保留内联标签供后续解析
|
||||
let text = html
|
||||
// 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'
|
||||
})
|
||||
|
||||
// 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(/<\/?h[1-6][^>]*>/gi, '\n')
|
||||
text = text.replace(/<\/?blockquote[^>]*>/gi, '\n')
|
||||
text = text.replace(/<\/?ul[^>]*>/gi, '\n')
|
||||
text = text.replace(/<\/?ol[^>]*>/gi, '\n')
|
||||
text = text.replace(/<li[^>]*>/gi, '• ')
|
||||
text = text.replace(/<\/li>/gi, '\n')
|
||||
text = text.replace(/<hr\s*\/?>/gi, '\n')
|
||||
|
||||
// 2. 逐段解析
|
||||
const blocks = text.split(/\n+/)
|
||||
for (const block of blocks) {
|
||||
// 5. 逐段解析
|
||||
var blocks = text.split(/\n+/)
|
||||
for (var bi = 0; bi < blocks.length; bi++) {
|
||||
var block = blocks[bi]
|
||||
if (!block.trim()) continue
|
||||
|
||||
let blockSegs = parseBlockToSegments(block, config)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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') {
|
||||
lines.push('')
|
||||
segments.push(blockSegs)
|
||||
continue
|
||||
}
|
||||
|
||||
// 对 text 段再跑一遍 @人名 / #标签 自动匹配(处理未用 TipTap 插入而是手打的 @xxx)
|
||||
if (config && (config.persons?.length || config.linkTags?.length)) {
|
||||
const expanded = []
|
||||
for (const seg of blockSegs) {
|
||||
var expanded = []
|
||||
for (var si = 0; si < blockSegs.length; si++) {
|
||||
var seg = blockSegs[si]
|
||||
if (seg.type === 'text' && seg.text) {
|
||||
const sub = matchLineToSegments(seg.text, config)
|
||||
expanded.push(...sub)
|
||||
expanded.push.apply(expanded, matchLineToSegments(seg.text, config))
|
||||
} else {
|
||||
expanded.push(seg)
|
||||
}
|
||||
@@ -227,8 +364,7 @@ function parseHtmlToSegments(html, config) {
|
||||
blockSegs = expanded
|
||||
}
|
||||
|
||||
// 行纯文本用于 lines(previewParagraphs 降级展示)
|
||||
const lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
var lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
lines.push(lineText)
|
||||
segments.push(blockSegs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user