({
title: chapter.title,
- index
+ index,
+ level: chapter.level || 1
})),
metadata: formattedBook.metadata
}
diff --git a/services/DB.ts b/services/DB.ts
index 0ca338e..c894b60 100644
--- a/services/DB.ts
+++ b/services/DB.ts
@@ -321,6 +321,12 @@ function paragraphs2Lines(book: Book, chapterIndex: number): string[] {
const allSentences: string[] = []
paragraphs.forEach(paragraph => {
+ // 图片段落或Markdown不拆句,整段保留
+ if (paragraph.startsWith('![IMG]') || paragraph.startsWith('![MD]')) {
+ allSentences.push(paragraph, 'EOB')
+ return
+ }
+
// 判断是否主要为中文文本
const isChinese = /[\u4e00-\u9fa5]/.test(paragraph)
let sentences: string[] = []
diff --git a/services/MD.ts b/services/MD.ts
index 8b1cfe4..871c7cc 100644
--- a/services/MD.ts
+++ b/services/MD.ts
@@ -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(`
${content}
`)
- })
-
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()
})
return paragraphs
diff --git a/test_md2.js b/test_md2.js
new file mode 100644
index 0000000..5ad9c3d
--- /dev/null
+++ b/test_md2.js
@@ -0,0 +1,34 @@
+const cheerio = require('cheerio');
+const html = `
+
Title
+
P1
+
Subtitle 1
+
P2
+
Subsubtitle
+
P3
+
Subtitle 2
+
P4
+
Quote Heading
P5
+`;
+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);
+});
diff --git a/types/book.ts b/types/book.ts
index a0ea932..9696d9d 100644
--- a/types/book.ts
+++ b/types/book.ts
@@ -27,10 +27,12 @@ export interface FormattedBook {
export interface PlainTextChapter {
title: string;
paragraphs: string[];
+ level?: number;
}
interface TocItem {
title: string;
index: number;
+ level?: number;
}
export interface Book {