Improve markdown stability for 0.5.1 and finalize release checks

This commit is contained in:
h3p 2026-03-08 14:07:50 +01:00
parent f7fe16dfa5
commit b265701637
8 changed files with 192 additions and 6 deletions

View file

@ -4,6 +4,16 @@ All notable changes to **Neon Vision Editor** are documented in this file.
The format follows *Keep a Changelog*. Versions use semantic versioning with prerelease tags.
## [Unreleased]
### Improved
- Improved Markdown preview stability by preserving relative scroll position during preview refreshes.
- Improved Markdown preview behavior for very large files by using a safe plain-text fallback with explicit status messaging instead of full HTML conversion.
### Fixed
- Fixed diagnostics export safety by redacting token-like updater status fragments before copying.
- Fixed Markdown regression coverage with new tests for Claude-style mixed-content Markdown and code-fence matching behavior.
## [v0.5.0] - 2026-03-06
### Added

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 431;
CURRENT_PROJECT_VERSION = 432;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -444,7 +444,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 431;
CURRENT_PROJECT_VERSION = 432;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -1150,6 +1150,31 @@ final class AppUpdateManager: ObservableObject {
return "\(normalized)+\(buildValue)"
}
nonisolated static func sanitizedDiagnosticSummary(_ text: String) -> String {
var sanitized = text
if let bearerRegex = try? NSRegularExpression(pattern: #"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+"#) {
let range = NSRange(sanitized.startIndex..., in: sanitized)
sanitized = bearerRegex.stringByReplacingMatches(
in: sanitized,
options: [],
range: range,
withTemplate: "Bearer [redacted]"
)
}
if let keyValueRegex = try? NSRegularExpression(
pattern: #"(?i)\b(authorization|token|api[_-]?key|password)\b\s*[:=]\s*([^\s,;]+)"#
) {
let range = NSRange(sanitized.startIndex..., in: sanitized)
sanitized = keyValueRegex.stringByReplacingMatches(
in: sanitized,
options: [],
range: range,
withTemplate: "$1=[redacted]"
)
}
return sanitized
}
nonisolated static func isVersionSkipped(_ version: String, skippedValue: String?) -> Bool {
skippedValue == version
}

View file

@ -3547,8 +3547,11 @@ struct ContentView: View {
}
}
private var markdownPreviewRenderByteLimit: Int { 180_000 }
private var markdownPreviewFallbackCharacterLimit: Int { 120_000 }
private func markdownPreviewHTML(from markdownText: String) -> String {
let bodyHTML = renderedMarkdownBodyHTML(from: markdownText) ?? "<pre>\(escapedHTML(markdownText))</pre>"
let bodyHTML = markdownPreviewBodyHTML(from: markdownText)
return """
<!doctype html>
<html lang="en">
@ -3568,6 +3571,27 @@ struct ContentView: View {
"""
}
private func markdownPreviewBodyHTML(from markdownText: String) -> String {
let byteCount = markdownText.lengthOfBytes(using: .utf8)
if byteCount > markdownPreviewRenderByteLimit {
return largeMarkdownFallbackHTML(from: markdownText, byteCount: byteCount)
}
return renderedMarkdownBodyHTML(from: markdownText) ?? "<pre>\(escapedHTML(markdownText))</pre>"
}
private func largeMarkdownFallbackHTML(from markdownText: String, byteCount: Int) -> String {
let previewText = String(markdownText.prefix(markdownPreviewFallbackCharacterLimit))
let truncated = previewText.count < markdownText.count
let statusSuffix = truncated ? " (truncated preview)" : ""
return """
<section class="preview-warning">
<p><strong>Large Markdown file</strong></p>
<p class="preview-warning-meta">Rendering full Markdown is skipped for stability (\(byteCount) bytes)\(statusSuffix).</p>
</section>
<pre>\(escapedHTML(previewText))</pre>
"""
}
private func renderedMarkdownBodyHTML(from markdownText: String) -> String? {
let html = simpleMarkdownToHTML(markdownText).trimmingCharacters(in: .whitespacesAndNewlines)
return html.isEmpty ? nil : html
@ -3861,6 +3885,21 @@ struct ContentView: View {
padding: \(basePadding);
margin: 0 auto;
}
.preview-warning {
margin: 0.5em 0 0.8em;
padding: 0.75em 0.9em;
border-radius: 9px;
border: 1px solid color-mix(in srgb, #f59e0b 45%, transparent);
background: color-mix(in srgb, #f59e0b 12%, transparent);
}
.preview-warning p {
margin: 0;
}
.preview-warning-meta {
margin-top: 0.4em !important;
font-size: 0.92em;
opacity: 0.9;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.25;
margin: 1.1em 0 0.55em;

View file

@ -18,12 +18,25 @@ struct MarkdownPreviewWebView: NSViewRepresentable {
func updateNSView(_ webView: WKWebView, context: Context) {
guard context.coordinator.lastHTML != html else { return }
webView.loadHTMLString(html, baseURL: nil)
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
context.coordinator.lastHTML = html
}
final class Coordinator {
var lastHTML: String = ""
func reloadPreservingScroll(webView: WKWebView, html: String) {
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
webView.evaluateJavaScript(capture) { value, _ in
let ratio = value as? Double ?? 0
webView.loadHTMLString(html, baseURL: nil)
let clamped = min(1.0, max(0.0, ratio))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
webView.evaluateJavaScript(restore, completionHandler: nil)
}
}
}
}
}
#elseif os(iOS)
@ -43,12 +56,25 @@ struct MarkdownPreviewWebView: UIViewRepresentable {
func updateUIView(_ webView: WKWebView, context: Context) {
guard context.coordinator.lastHTML != html else { return }
webView.loadHTMLString(html, baseURL: nil)
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
context.coordinator.lastHTML = html
}
final class Coordinator {
var lastHTML: String = ""
func reloadPreservingScroll(webView: WKWebView, html: String) {
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
webView.evaluateJavaScript(capture) { value, _ in
let ratio = value as? Double ?? 0
webView.loadHTMLString(html, baseURL: nil)
let clamped = min(1.0, max(0.0, ratio))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
webView.evaluateJavaScript(restore, completionHandler: nil)
}
}
}
}
}
#endif

View file

@ -1752,7 +1752,7 @@ struct NeonSettingsView: View {
var lines: [String] = []
lines.append("Neon Vision Editor Diagnostics")
lines.append("Timestamp: \(Date().formatted(date: .abbreviated, time: .shortened))")
lines.append("Updater.lastCheckResult: \(appUpdateManager.lastCheckResultSummary)")
lines.append("Updater.lastCheckResult: \(AppUpdateManager.sanitizedDiagnosticSummary(appUpdateManager.lastCheckResultSummary))")
lines.append("Updater.lastCheckedAt: \(appUpdateManager.lastCheckedAt?.formatted(date: .abbreviated, time: .shortened) ?? "never")")
if let pausedUntil = appUpdateManager.pausedUntil, pausedUntil > Date() {
lines.append("Updater.pauseUntil: \(pausedUntil.formatted(date: .abbreviated, time: .shortened))")

View file

@ -146,4 +146,14 @@ final class AppUpdateManagerTests: XCTestCase {
XCTAssertEqual(AppUpdateManager.releaseTrackingIdentifier(version: "v1.2.3", build: "45"), "1.2.3+45")
XCTAssertEqual(AppUpdateManager.releaseTrackingIdentifier(version: "1.2.3", build: nil), "1.2.3")
}
func testSanitizedDiagnosticSummaryRedactsSensitiveValues() {
let summary = "token=abc123 authorization:Bearer TOPSECRET api_key=my-key password=swordfish"
let redacted = AppUpdateManager.sanitizedDiagnosticSummary(summary)
XCTAssertFalse(redacted.localizedCaseInsensitiveContains("abc123"))
XCTAssertFalse(redacted.localizedCaseInsensitiveContains("TOPSECRET"))
XCTAssertFalse(redacted.localizedCaseInsensitiveContains("my-key"))
XCTAssertFalse(redacted.localizedCaseInsensitiveContains("swordfish"))
XCTAssertTrue(redacted.contains("token=[redacted]"))
}
}

View file

@ -0,0 +1,76 @@
import XCTest
import SwiftUI
@testable import Neon_Vision_Editor
final class MarkdownSyntaxHighlightingTests: XCTestCase {
private func markdownPatterns() -> [String: Color] {
getSyntaxPatterns(
for: "markdown",
colors: SyntaxColors.fromVibrantLightTheme(colorScheme: .dark)
)
}
func testMarkdownPatternsMatchClaudeStyleDocumentSections() {
let sample = """
# Claude Export
Here is prose with an [inline link](https://example.com).
- First bullet
- Second bullet with *emphasis*
```swift
struct Demo { let id: Int }
```
> This is a quoted block.
"""
let patterns = markdownPatterns()
let headingPattern = patterns.keys.first { $0.contains("#{1,6}") }
let listPattern = patterns.keys.first { $0.contains("[-*+]") }
let quotePattern = patterns.keys.first { $0.contains("^>\\s+") }
XCTAssertNotNil(headingPattern)
XCTAssertNotNil(listPattern)
XCTAssertNotNil(quotePattern)
for pattern in [headingPattern, listPattern, quotePattern].compactMap({ $0 }) {
guard let regex = cachedSyntaxRegex(pattern: pattern, options: [.dotMatchesLineSeparators]) else {
XCTFail("Failed to compile regex: \(pattern)")
continue
}
let matches = regex.matches(in: sample, options: [], range: NSRange(sample.startIndex..., in: sample))
XCTAssertFalse(matches.isEmpty, "Expected markdown regex to match sample sections: \(pattern)")
}
}
func testMarkdownCodeFenceRegexKeepsSeparateFences() {
let sample = """
Intro paragraph.
```swift
let x = 1
```
middle text with `inline` code
```json
{"a": 1}
```
"""
let patterns = markdownPatterns()
guard let fencePattern = patterns.keys.first(where: { $0.contains("```.*?```") }) else {
XCTFail("Fence regex pattern missing")
return
}
guard let regex = cachedSyntaxRegex(pattern: fencePattern, options: [.dotMatchesLineSeparators]) else {
XCTFail("Fence regex failed to compile")
return
}
let matches = regex.matches(in: sample, options: [], range: NSRange(sample.startIndex..., in: sample))
XCTAssertEqual(matches.count, 3, "Expected 2 fenced blocks plus 1 inline code span")
}
}