polish: scope content upgrade to #article-body container only
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s

- 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:
Yusuf Suleman
2026-04-03 21:47:17 -05:00
parent f10c356199
commit 670e2b2bac
2 changed files with 37 additions and 25 deletions

View File

@@ -244,7 +244,7 @@ enum ArticleHTMLBuilder {
<h1 class="article-title">\(safeTitle)</h1>
<div class="article-meta">\(meta)</div>
</div>
\(body)
<div id="article-body">\(body)</div>
</body>
</html>
"""

View File

@@ -62,21 +62,23 @@ struct ArticleWebView: UIViewRepresentable {
context.coordinator.lastHTML = newHTML
if isUpgrade {
// Content upgrade (partial full): capture scroll, replace body, restore scroll.
// Uses JavaScript DOM replacement instead of full page reload to avoid flash.
let escapedBody = extractBody(from: newHTML)
// Content upgrade (partial full): swap only #article-body contents.
// Header + outer document structure stay intact. Scroll preserved.
let escapedContent = escapeForJS(extractArticleBody(from: newHTML))
let js = """
(function() {
var el = document.getElementById('article-body');
if (!el) return 'no-container';
var scrollY = window.scrollY;
var header = document.querySelector('.article-header');
var headerHTML = header ? header.outerHTML : '';
document.body.innerHTML = headerHTML + \(escapedBody);
el.innerHTML = \(escapedContent);
window.scrollTo(0, scrollY);
return 'ok';
})();
"""
webView.evaluateJavaScript(js) { _, error in
// If JS replacement fails (e.g. different page structure), fall back to full reload
if error != nil {
webView.evaluateJavaScript(js) { result, error in
// Fall back to full reload if container missing or JS error
let status = result as? String
if error != nil || status != "ok" {
webView.loadHTMLString(newHTML, baseURL: nil)
}
}
@@ -90,23 +92,33 @@ struct ArticleWebView: UIViewRepresentable {
Coordinator()
}
/// Extract the content after </head><body>...</body> as a JS string literal
private func extractBody(from html: String) -> String {
// Find body content between <body> and </body>
if let bodyStart = html.range(of: "<body>"),
let bodyEnd = html.range(of: "</body>") {
let bodyContent = String(html[bodyStart.upperBound..<bodyEnd.lowerBound])
// Escape for JavaScript string literal
let escaped = bodyContent
/// Extract content inside <div id="article-body">...</div>
private func extractArticleBody(from html: String) -> String {
let marker = "<div id=\"article-body\">"
guard let start = html.range(of: marker) else { return "" }
let afterMarker = html[start.upperBound...]
// Find the matching </div> before </body>
guard let bodyEnd = afterMarker.range(of: "</div>\n </body>")
?? 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: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "")
return "'\(escaped)'"
}
// Fallback: can't parse, return empty
return "''"
}
class Coordinator: NSObject, WKNavigationDelegate {
var lastHTML: String?