130 lines
5.1 KiB
Swift
130 lines
5.1 KiB
Swift
import UIKit
|
|
|
|
/// 将纯文本文件构建为 RDEPUBTextBook,复用 EPUB 文本渲染和分页管线
|
|
public final class RDPlainTextBookBuilder {
|
|
private let renderer: RDEPUBTextRenderer
|
|
|
|
public init(renderer: RDEPUBTextRenderer = RDEPUBDTCoreTextRenderer()) {
|
|
self.renderer = renderer
|
|
}
|
|
|
|
public func build(
|
|
textFileURL: URL,
|
|
pageSize: CGSize,
|
|
style: RDEPUBTextRenderStyle
|
|
) throws -> RDEPUBTextBook {
|
|
let rawText = rd_decodeTextFile(url: textFileURL)
|
|
let chapterSpecs = splitChapters(from: rawText)
|
|
|
|
var chapters: [RDEPUBTextChapter] = []
|
|
var flatPages: [RDEPUBTextPage] = []
|
|
|
|
for (index, spec) in chapterSpecs.enumerated() {
|
|
let html = wrapTextAsHTML(spec.content)
|
|
let rendered = try renderer.renderChapter(html: html, baseURL: nil, style: style)
|
|
|
|
let content = NSMutableAttributedString(attributedString: rendered.attributedString)
|
|
let pageRanges = content.length > 0 ? content.ss_pageRanges(size: pageSize) : []
|
|
let effectivePageRanges = pageRanges.isEmpty && content.length > 0
|
|
? [NSRange(location: 0, length: content.length)]
|
|
: pageRanges
|
|
|
|
let href = "chapter_\(index).xhtml"
|
|
let pages = effectivePageRanges.enumerated().map { localPageIndex, range in
|
|
RDEPUBTextPage(
|
|
absolutePageIndex: flatPages.count + localPageIndex,
|
|
chapterIndex: index,
|
|
spineIndex: index,
|
|
href: href,
|
|
chapterTitle: spec.title ?? "第 \(index + 1) 章",
|
|
pageIndexInChapter: localPageIndex,
|
|
totalPagesInChapter: effectivePageRanges.count,
|
|
content: content.attributedSubstring(from: range),
|
|
contentRange: range,
|
|
pageStartOffset: range.location,
|
|
pageEndOffset: range.location + max(range.length - 1, 0)
|
|
)
|
|
}
|
|
|
|
chapters.append(
|
|
RDEPUBTextChapter(
|
|
chapterIndex: index,
|
|
spineIndex: index,
|
|
href: href,
|
|
title: spec.title ?? "第 \(index + 1) 章",
|
|
attributedContent: content.copy() as! NSAttributedString,
|
|
fragmentOffsets: [:],
|
|
pages: pages
|
|
)
|
|
)
|
|
flatPages.append(contentsOf: pages)
|
|
}
|
|
|
|
return RDEPUBTextBook(chapters: chapters, pages: flatPages)
|
|
}
|
|
|
|
// MARK: - 章节拆分
|
|
|
|
private struct ChapterSpec {
|
|
let title: String?
|
|
let content: String
|
|
}
|
|
|
|
/// 按中文章节正则拆分文本。无匹配时整体作为一章。
|
|
private func splitChapters(from text: String) -> [ChapterSpec] {
|
|
let pattern = #"^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$"#
|
|
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines]) else {
|
|
return [ChapterSpec(title: nil, content: text)]
|
|
}
|
|
|
|
let nsText = text as NSString
|
|
let matches = regex.matches(in: text, range: NSRange(location: 0, length: nsText.length))
|
|
guard !matches.isEmpty else {
|
|
return [ChapterSpec(title: nil, content: text)]
|
|
}
|
|
|
|
var specs: [ChapterSpec] = []
|
|
for (i, match) in matches.enumerated() {
|
|
let titleRange = match.range(at: 1)
|
|
let title = nsText.substring(with: titleRange).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let contentStart = titleRange.location + titleRange.length
|
|
let contentEnd = (i + 1 < matches.count) ? matches[i + 1].range.location : nsText.length
|
|
let contentRange = NSRange(location: contentStart, length: contentEnd - contentStart)
|
|
let content = nsText.substring(with: contentRange).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !content.isEmpty {
|
|
specs.append(ChapterSpec(title: title, content: content))
|
|
}
|
|
}
|
|
|
|
// 拆分结果为空(所有章节内容为空),整体作为一章
|
|
if specs.isEmpty {
|
|
return [ChapterSpec(title: nil, content: text)]
|
|
}
|
|
return specs
|
|
}
|
|
|
|
// MARK: - HTML 包装
|
|
|
|
/// 将纯文本包装为 HTML 段落
|
|
private func wrapTextAsHTML(_ text: String) -> String {
|
|
let paragraphs = text.components(separatedBy: "\n").filter { !$0.isEmpty }
|
|
let body = paragraphs.map { "<p>\($0)</p>" }.joined(separator: "\n")
|
|
return "<html><body>\(body)</body></html>"
|
|
}
|
|
|
|
// MARK: - 文本解码
|
|
|
|
private func rd_decodeTextFile(url: URL) -> String {
|
|
if let content = try? NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String {
|
|
return content
|
|
}
|
|
if let content = try? NSString(contentsOf: url, encoding: 0x80000632) as String {
|
|
return content
|
|
}
|
|
if let content = try? NSString(contentsOf: url, encoding: 0x80000631) as String {
|
|
return content
|
|
}
|
|
return ""
|
|
}
|
|
}
|