Skip to content

Commit feb15dd

Browse files
davidhooclaude
andcommitted
perf: avoid duplicate markdown render on full load
WebViewMarkdownView.loadContent() 之前对同一份 content 调用了两次 MarkdownHTMLService.render:一次在 buildFullHTML 内部装配页面壳, 一次仅为写入从未被读取的 currentHeadings。后者额外执行了 Markdown 预处理、Document 解析与 formatter 遍历,对功能无贡献。 新增 buildFullHTML(renderResult:...) 重载,只装配 HTML 页面壳,不再 调用 render;loadContent() 先渲染一次再把结果交给该重载。原 buildFullHTML(content:...) 签名保留给 PDF 等调用方,内部只渲染一次 后委托新重载,HTML 模板、属性转义、CSS/JS 顺序逐字不变。 同时删除从未被读取的 currentHeadings 与 lastLoadedURL 状态及其赋值。 新增 MarkdownHTMLServiceTests 覆盖新重载与旧便利入口的输出合同。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b7aea7f commit feb15dd

3 files changed

Lines changed: 63 additions & 9 deletions

File tree

Sources/MarkdownReader/Views/WebViewMarkdownView.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,8 @@ struct WebViewMarkdownView: View {
106106
@Binding var exportedPage: WebPage?
107107
@State private var scrollPosition = ScrollPosition(edge: .top)
108108
@State private var lastLoadedContent: String = ""
109-
@State private var lastLoadedURL: URL?
110109
@State private var scrollSyncTimer: Timer?
111110
@State private var isConfigured = false
112-
@State private var currentHeadings: [MarkdownHTMLService.HeadingInfo] = []
113111
@State private var pendingScrollToLine: Int?
114112
@State private var zoomLevel: CGFloat = 1.0
115113
/// 上次处理的 contentVersion,用于检测程序化内容更新(reload/load)
@@ -260,8 +258,9 @@ struct WebViewMarkdownView: View {
260258

261259
private func loadContent() {
262260
let baseURL = fileURL?.deletingLastPathComponent()
261+
let renderResult = MarkdownHTMLService.render(content, baseURL: baseURL)
263262
let html = MarkdownHTMLService.buildFullHTML(
264-
content: content,
263+
renderResult: renderResult,
265264
themeCSS: themeCSS,
266265
contentPadding: contentPadding,
267266
maxContentWidthFollowsWindow: maxContentWidthFollowsWindow,
@@ -271,15 +270,11 @@ struct WebViewMarkdownView: View {
271270
documentCopiedTitle: L10n.tr(.contentCopied, language: language)
272271
)
273272

274-
let renderResult = MarkdownHTMLService.render(content, baseURL: baseURL)
275-
currentHeadings = renderResult.headings
276-
277273
scrollPosition = ScrollPosition(edge: .top)
278274

279275
let effectiveBaseURL = baseURL ?? URL(string: "about:blank")!
280276
_ = page.load(html: html, baseURL: effectiveBaseURL)
281277
lastLoadedContent = content
282-
lastLoadedURL = fileURL
283278

284279
// 页面加载后同步查找栏显隐(查找栏打开期间渲染按钮隐藏)。
285280
syncDocumentCopyButtonVisibility()
@@ -299,7 +294,6 @@ struct WebViewMarkdownView: View {
299294
private func updateContent(_ content: String) {
300295
let baseURL = fileURL?.deletingLastPathComponent()
301296
let renderResult = MarkdownHTMLService.render(content, baseURL: baseURL)
302-
currentHeadings = renderResult.headings
303297

304298
let escapedHTML = renderResult.html
305299
.replacingOccurrences(of: "\\", with: "\\\\")

Sources/MarkdownReaderKit/Services/MarkdownHTMLService.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,21 @@ public enum MarkdownHTMLService {
4848
}
4949

5050
public static func buildFullHTML(content: String, themeCSS: String, contentPadding: CGFloat, maxContentWidthFollowsWindow: Bool = false, baseURL: URL?, isDark: Bool = true, documentCopyTitle: String = "", documentCopiedTitle: String = "") -> String {
51-
let renderResult = render(content, baseURL: baseURL)
51+
buildFullHTML(
52+
renderResult: render(content, baseURL: baseURL),
53+
themeCSS: themeCSS,
54+
contentPadding: contentPadding,
55+
maxContentWidthFollowsWindow: maxContentWidthFollowsWindow,
56+
baseURL: baseURL,
57+
isDark: isDark,
58+
documentCopyTitle: documentCopyTitle,
59+
documentCopiedTitle: documentCopiedTitle
60+
)
61+
}
5262

63+
/// 以已有 `RenderResult` 装配完整 HTML 页面壳。不接收原始 Markdown,也不再次调用
64+
/// `render`,仅供调用方(如 WebView 完整加载)复用单次解析结果,避免重复 Markdown 预处理与解析。
65+
public static func buildFullHTML(renderResult: RenderResult, themeCSS: String, contentPadding: CGFloat, maxContentWidthFollowsWindow: Bool = false, baseURL: URL?, isDark: Bool = true, documentCopyTitle: String = "", documentCopiedTitle: String = "") -> String {
5366
let baseURLAttr = baseURL != nil ? " data-base-url=\"\(baseURL!.path.addingXMLAttributeEscapes)\"" : ""
5467
let copyTitleAttr = " data-document-copy-title=\"\(documentCopyTitle.addingXMLAttributeEscapes)\""
5568
let copiedTitleAttr = " data-document-copied-title=\"\(documentCopiedTitle.addingXMLAttributeEscapes)\""
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import MarkdownReaderKit
2+
import XCTest
3+
4+
/// MarkdownHTMLService 页面壳装配单测:覆盖新增的 `buildFullHTML(renderResult:...)`
5+
/// 重载与原 `buildFullHTML(content:...)` 便利入口的输出合同。不创建 WebView,不联网。
6+
final class MarkdownHTMLServiceTests: XCTestCase {
7+
8+
// MARK: - 新重载:以 RenderResult 为输入装配页面壳
9+
10+
func testBuildFullHTMLFromRenderResultEmbedsRenderedBodyAndShell() {
11+
let result = MarkdownHTMLService.render("# Single parse")
12+
13+
let html = MarkdownHTMLService.buildFullHTML(
14+
renderResult: result,
15+
themeCSS: "--ink: #ffffff;",
16+
contentPadding: 20,
17+
baseURL: nil,
18+
isDark: true,
19+
documentCopyTitle: "Copy",
20+
documentCopiedTitle: "Copied"
21+
)
22+
23+
XCTAssertTrue(html.contains(result.html))
24+
XCTAssertTrue(html.contains("id=\"mr-content\""))
25+
XCTAssertTrue(html.contains("data-document-copy-title=\"Copy\""))
26+
XCTAssertTrue(html.contains("data-is-dark=\"true\""))
27+
}
28+
29+
// MARK: - 旧便利入口:签名与输出合同回归(PDF 等调用方未迁移)
30+
31+
func testBuildFullHTMLFromContentPreservesShellContract() {
32+
let html = MarkdownHTMLService.buildFullHTML(
33+
content: "# Heading",
34+
themeCSS: "--ink: #000000;",
35+
contentPadding: 16,
36+
baseURL: nil,
37+
isDark: false,
38+
documentCopyTitle: "Copy",
39+
documentCopiedTitle: "Copied"
40+
)
41+
42+
XCTAssertTrue(html.contains("<h1 id=\"heading-1\""))
43+
XCTAssertTrue(html.contains("id=\"mr-theme-style\""))
44+
XCTAssertTrue(html.contains("mr:///js/markdown-reader.js"))
45+
XCTAssertTrue(html.contains("id=\"mr-content\""))
46+
}
47+
}

0 commit comments

Comments
 (0)