-
Notifications
You must be signed in to change notification settings - Fork 42
feat: 优化 Markdown 渲染(树状多级目录、保留首段引言、图片全屏预览) #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,46 +11,58 @@ export function initMDBook(buffer: Buffer, name: string): FormattedBook { | |
| const title = $('h1').text() || name | ||
| const language = detectLanguage(mdString.slice(0, 500)) | ||
|
|
||
| // 将h3和h4转换为普通段落 | ||
| $('h3, h4, h5').each((_, elem) => { | ||
| const content = $(elem).html() || '' | ||
| $(elem).replaceWith(`<p>${content}</p>`) | ||
| }) | ||
|
|
||
| const chapterList: PlainTextChapter[] = [] | ||
|
|
||
| // 找到所有h2元素 | ||
| const h2Elements = $('h2') | ||
| // 找到所有顶层标题元素 (h1~h6) | ||
| const headingElements = $('body > h1, body > h2, body > h3, body > h4, body > h5, body > h6') | ||
|
|
||
| if (h2Elements.length === 0) { | ||
| // 如果没有h2元素,将整个内容作为一个章节 | ||
| if (headingElements.length === 0) { | ||
| // 如果没有标题元素,将整个内容作为一个章节 | ||
| const content = $('body').html() || '' | ||
| // 把HTML内容转换为段落数组 | ||
| const paragraphs = extractParagraphs($, content) | ||
|
|
||
| chapterList.push({ | ||
| title: title, | ||
| paragraphs | ||
| paragraphs, | ||
| level: 1 | ||
| }) | ||
| } else { | ||
| // 根据h2元素分割内容 | ||
| h2Elements.each((_, elem) => { | ||
| // 检查第一个标题之前的内容(保留文章开头的引言/前言) | ||
| const $bodyChildren = $('body').children() | ||
| const firstHeadingIndex = $bodyChildren.index(headingElements.first()) | ||
| if (firstHeadingIndex > 0) { | ||
| const beforeHeading = $bodyChildren.slice(0, firstHeadingIndex) | ||
| const content = beforeHeading.map((_, el) => $.html(el)).get().join('') | ||
| const paragraphs = extractParagraphs($, content) | ||
| if (paragraphs.length > 0) { | ||
| chapterList.push({ | ||
| title: title || '引言', | ||
| paragraphs, | ||
| level: 1 | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // 根据标题元素分割内容 | ||
| headingElements.each((_, elem) => { | ||
| const chapterTitle = $(elem).text() | ||
| const level = parseInt(elem.tagName.replace(/h/i, ''), 10) || 1 | ||
| let content = '' | ||
|
|
||
| // 获取当前h2元素 | ||
| // 获取当前标题元素 | ||
| const $elem = $(elem) | ||
|
|
||
| // 获取当前h2到下一个h2之间的内容 | ||
| // 获取当前标题到下一个顶层标题之间的内容 | ||
| let $nextAll = $elem.nextAll() | ||
| let $nextH2 = $nextAll.filter('h2').first() | ||
| let $nextHeading = $nextAll.filter('h1, h2, h3, h4, h5, h6').first() | ||
|
|
||
| if ($nextH2.length > 0) { | ||
| // 获取到下一个h2之前的所有元素 | ||
| let $contents = $nextAll.slice(0, $nextAll.index($nextH2)) | ||
| if ($nextHeading.length > 0) { | ||
| // 获取到下一个标题之前的所有元素 | ||
| let $contents = $nextAll.slice(0, $nextAll.index($nextHeading)) | ||
| content = $contents.map((_, el) => $.html(el)).get().join('') | ||
| } else { | ||
| // 如果没有下一个h2,获取当前h2后面的所有内容 | ||
| // 如果没有下一个标题,获取当前标题后面的所有内容 | ||
| content = $nextAll.map((_, el) => $.html(el)).get().join('') | ||
| } | ||
|
|
||
|
|
@@ -59,7 +71,8 @@ export function initMDBook(buffer: Buffer, name: string): FormattedBook { | |
|
|
||
| chapterList.push({ | ||
| title: chapterTitle, | ||
| paragraphs | ||
| paragraphs, | ||
| level | ||
| }) | ||
| }) | ||
| } | ||
|
|
@@ -80,20 +93,50 @@ function extractParagraphs($: cheerio.CheerioAPI, htmlContent: string): string[] | |
| const $content = cheerio.load(htmlContent) | ||
| const paragraphs: string[] = [] | ||
|
|
||
| // 提取所有段落元素 | ||
| $content('p').each((_, elem) => { | ||
| const text = $content(elem).text().trim() | ||
| if (text) { | ||
| paragraphs.push(text) | ||
| $content('body').children().each((_, elem) => { | ||
| const tagName = elem.tagName | ||
|
|
||
| if (tagName === 'h1' || tagName === 'h2') { | ||
| return // 跳过(章节分割用) | ||
| } | ||
| }) | ||
|
|
||
| // 处理其他可能的内容元素(如列表、引用等) | ||
| $content('li, blockquote').each((_, elem) => { | ||
| const text = $content(elem).text().trim() | ||
| if (text) { | ||
| paragraphs.push(text) | ||
| // 保留原始HTML供MarkdownRenderer渲染,比如表格和代码块 | ||
| if (tagName === 'table' || tagName === 'pre') { | ||
| const html = $content(elem).prop('outerHTML') || $content.html(elem) | ||
| if (html) { | ||
| paragraphs.push(`![MD]${html}`) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| let currentText = '' | ||
| const flushText = () => { | ||
| const t = currentText.trim() | ||
| if (t) paragraphs.push(t) | ||
| currentText = '' | ||
| } | ||
|
|
||
| const traverse = (node: any) => { | ||
| if (node.type === 'text') { | ||
| currentText += node.data | ||
| } else if (node.type === 'tag' && node.tagName === 'br') { | ||
| currentText += '\n' | ||
| } else if (node.type === 'tag' && node.tagName === 'img') { | ||
| flushText() | ||
| const src = node.attribs?.src | ||
| const alt = node.attribs?.alt || '' | ||
| if (src) { | ||
| paragraphs.push(`![IMG]${alt ? alt + '|' : ''}${src}`) | ||
| } | ||
| } else if (node.type === 'tag') { | ||
| if (node.children) { | ||
| node.children.forEach(traverse) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| traverse(elem) | ||
| flushText() | ||
|
Comment on lines
+96
to
+139
|
||
| }) | ||
|
|
||
| return paragraphs | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cheerio = require('cheerio'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const html = `<body> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1>Title</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>P1</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2>Subtitle 1</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>P2</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3>Subsubtitle</h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>P3</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2>Subtitle 2</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>P4</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <blockquote><h2>Quote Heading</h2><p>P5</p></blockquote> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </body>`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const $ = cheerio.load(html); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headingElements = $('body > h1, body > h2, body > h3, body > h4, body > h5, body > h6'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Found headings:", headingElements.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headingElements.each((_, elem) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chapterTitle = $(elem).text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const level = parseInt(elem.tagName.replace('h', ''), 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Heading:", chapterTitle, "Level:", level); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const $elem = $(elem); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let $nextAll = $elem.nextAll(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let $nextHeading = $nextAll.filter('h1, h2, h3, h4, h5, h6').first(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let content = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($nextHeading.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let $contents = $nextAll.slice(0, $nextAll.index($nextHeading)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = $contents.map((_, el) => $.html(el)).get().join('').trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = $nextAll.map((_, el) => $.html(el)).get().join('').trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(" Content:", content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+2
to
+34
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const html = `<body> | |
| <h1>Title</h1> | |
| <p>P1</p> | |
| <h2>Subtitle 1</h2> | |
| <p>P2</p> | |
| <h3>Subsubtitle</h3> | |
| <p>P3</p> | |
| <h2>Subtitle 2</h2> | |
| <p>P4</p> | |
| <blockquote><h2>Quote Heading</h2><p>P5</p></blockquote> | |
| </body>`; | |
| const $ = cheerio.load(html); | |
| const headingElements = $('body > h1, body > h2, body > h3, body > h4, body > h5, body > h6'); | |
| console.log("Found headings:", headingElements.length); | |
| headingElements.each((_, elem) => { | |
| const chapterTitle = $(elem).text(); | |
| const level = parseInt(elem.tagName.replace('h', ''), 10); | |
| console.log("Heading:", chapterTitle, "Level:", level); | |
| const $elem = $(elem); | |
| let $nextAll = $elem.nextAll(); | |
| let $nextHeading = $nextAll.filter('h1, h2, h3, h4, h5, h6').first(); | |
| let content = ''; | |
| if ($nextHeading.length > 0) { | |
| let $contents = $nextAll.slice(0, $nextAll.index($nextHeading)); | |
| content = $contents.map((_, el) => $.html(el)).get().join('').trim(); | |
| } else { | |
| content = $nextAll.map((_, el) => $.html(el)).get().join('').trim(); | |
| } | |
| console.log(" Content:", content); | |
| }); | |
| /** | |
| * Extract headings and their associated HTML content from the given HTML string. | |
| * | |
| * Only direct child heading elements of <body> are considered (h1–h6). For each | |
| * heading, all following siblings up to (but not including) the next heading | |
| * are collected as that heading's content. | |
| * | |
| * @param {string} html - The HTML string to parse. | |
| * @returns {Array<{ title: string, level: number, content: string }>} | |
| */ | |
| function extractHeadings(html) { | |
| const $ = cheerio.load(html); | |
| const headingElements = $('body > h1, body > h2, body > h3, body > h4, body > h5, body > h6'); | |
| return headingElements | |
| .map((_, elem) => { | |
| const chapterTitle = $(elem).text(); | |
| const level = parseInt(elem.tagName.replace('h', ''), 10); | |
| const $elem = $(elem); | |
| const $nextAll = $elem.nextAll(); | |
| const $nextHeading = $nextAll.filter('h1, h2, h3, h4, h5, h6').first(); | |
| let content = ''; | |
| if ($nextHeading.length > 0) { | |
| const $contents = $nextAll.slice(0, $nextAll.index($nextHeading)); | |
| content = $contents.map((_, el) => $.html(el)).get().join('').trim(); | |
| } else { | |
| content = $nextAll.map((_, el) => $.html(el)).get().join('').trim(); | |
| } | |
| return { | |
| title: chapterTitle, | |
| level, | |
| content, | |
| }; | |
| }) | |
| .get(); | |
| } | |
| module.exports = { | |
| extractHeadings, | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
图片行目前没有任何地方会调用 handleLineClick(index),导致无法选中该行、不会触发 SEND_LINE_INDEX,也不会在点击时更新 currentLocation(只有滚动才会保存)。建议让图片行的容器或左侧区域响应点击并调用 handleLineClick(index),同时保留放大镜按钮的 stopPropagation 以避免误触。