polish: scope content upgrade to #article-body container only
- HTML template now wraps article content in <div id="article-body"> - Content upgrade JS targets only #article-body.innerHTML, leaving header, CSS, and outer document structure untouched - Returns 'ok'/'no-container' status for reliable fallback detection - extractArticleBody() parses the #article-body content from HTML - escapeForJS() separated into its own method - Full reload fallback if container not found or JS fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,7 @@ enum ArticleHTMLBuilder {
|
|||||||
<h1 class="article-title">\(safeTitle)</h1>
|
<h1 class="article-title">\(safeTitle)</h1>
|
||||||
<div class="article-meta">\(meta)</div>
|
<div class="article-meta">\(meta)</div>
|
||||||
</div>
|
</div>
|
||||||
\(body)
|
<div id="article-body">\(body)</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -62,21 +62,23 @@ struct ArticleWebView: UIViewRepresentable {
|
|||||||
context.coordinator.lastHTML = newHTML
|
context.coordinator.lastHTML = newHTML
|
||||||
|
|
||||||
if isUpgrade {
|
if isUpgrade {
|
||||||
// Content upgrade (partial → full): capture scroll, replace body, restore scroll.
|
// Content upgrade (partial → full): swap only #article-body contents.
|
||||||
// Uses JavaScript DOM replacement instead of full page reload to avoid flash.
|
// Header + outer document structure stay intact. Scroll preserved.
|
||||||
let escapedBody = extractBody(from: newHTML)
|
let escapedContent = escapeForJS(extractArticleBody(from: newHTML))
|
||||||
let js = """
|
let js = """
|
||||||
(function() {
|
(function() {
|
||||||
|
var el = document.getElementById('article-body');
|
||||||
|
if (!el) return 'no-container';
|
||||||
var scrollY = window.scrollY;
|
var scrollY = window.scrollY;
|
||||||
var header = document.querySelector('.article-header');
|
el.innerHTML = \(escapedContent);
|
||||||
var headerHTML = header ? header.outerHTML : '';
|
|
||||||
document.body.innerHTML = headerHTML + \(escapedBody);
|
|
||||||
window.scrollTo(0, scrollY);
|
window.scrollTo(0, scrollY);
|
||||||
|
return 'ok';
|
||||||
})();
|
})();
|
||||||
"""
|
"""
|
||||||
webView.evaluateJavaScript(js) { _, error in
|
webView.evaluateJavaScript(js) { result, error in
|
||||||
// If JS replacement fails (e.g. different page structure), fall back to full reload
|
// Fall back to full reload if container missing or JS error
|
||||||
if error != nil {
|
let status = result as? String
|
||||||
|
if error != nil || status != "ok" {
|
||||||
webView.loadHTMLString(newHTML, baseURL: nil)
|
webView.loadHTMLString(newHTML, baseURL: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,23 +92,33 @@ struct ArticleWebView: UIViewRepresentable {
|
|||||||
Coordinator()
|
Coordinator()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the content after </head><body>...</body> as a JS string literal
|
/// Extract content inside <div id="article-body">...</div>
|
||||||
private func extractBody(from html: String) -> String {
|
private func extractArticleBody(from html: String) -> String {
|
||||||
// Find body content between <body> and </body>
|
let marker = "<div id=\"article-body\">"
|
||||||
if let bodyStart = html.range(of: "<body>"),
|
guard let start = html.range(of: marker) else { return "" }
|
||||||
let bodyEnd = html.range(of: "</body>") {
|
let afterMarker = html[start.upperBound...]
|
||||||
let bodyContent = String(html[bodyStart.upperBound..<bodyEnd.lowerBound])
|
// Find the matching </div> before </body>
|
||||||
// Escape for JavaScript string literal
|
guard let bodyEnd = afterMarker.range(of: "</div>\n </body>")
|
||||||
let escaped = bodyContent
|
?? afterMarker.range(of: "</div></body>")
|
||||||
|
?? afterMarker.range(of: "</div>\n</body>") else {
|
||||||
|
// Fallback: take everything up to </body>
|
||||||
|
if let end = afterMarker.range(of: "</body>") {
|
||||||
|
return String(afterMarker[..<end.lowerBound])
|
||||||
|
}
|
||||||
|
return String(afterMarker)
|
||||||
|
}
|
||||||
|
return String(afterMarker[..<bodyEnd.lowerBound])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escape a string for use as a JavaScript string literal
|
||||||
|
private func escapeForJS(_ str: String) -> String {
|
||||||
|
let escaped = str
|
||||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||||
.replacingOccurrences(of: "'", with: "\\'")
|
.replacingOccurrences(of: "'", with: "\\'")
|
||||||
.replacingOccurrences(of: "\n", with: "\\n")
|
.replacingOccurrences(of: "\n", with: "\\n")
|
||||||
.replacingOccurrences(of: "\r", with: "")
|
.replacingOccurrences(of: "\r", with: "")
|
||||||
return "'\(escaped)'"
|
return "'\(escaped)'"
|
||||||
}
|
}
|
||||||
// Fallback: can't parse, return empty
|
|
||||||
return "''"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Coordinator: NSObject, WKNavigationDelegate {
|
class Coordinator: NSObject, WKNavigationDelegate {
|
||||||
var lastHTML: String?
|
var lastHTML: String?
|
||||||
|
|||||||
Reference in New Issue
Block a user