From 3ae6f954f0a29959944fff1463730b690425ead6 Mon Sep 17 00:00:00 2001
From: Patrick Schiller
Date: Sat, 1 Aug 2026 08:26:45 +0200
Subject: [PATCH] Align iOS capture methods and SourceBraid rebrand
Signed-off-by: Patrick Schiller
---
ios/README.md | 11 +++---
ios/SourceBraid.xcodeproj/project.pbxproj | 20 +++++------
ios/SourceBraid/Resources/Info.plist | 2 +-
.../Resources/SourceBraid.entitlements | 4 +--
ios/SourceBraid/Shared/ClipBuilder.swift | 35 +++++++++++++++----
ios/SourceBraid/Shared/GitHubClient.swift | 7 ++++
.../Shared/SourceBraidEnvironment.swift | 5 +--
ios/SourceBraidShare/Info.plist | 2 +-
ios/SourceBraidShare/ShareViewModel.swift | 2 +-
.../SourceBraidShare.entitlements | 4 +--
ios/SourceBraidShare/WebPageCapture.js | 32 +++++++++++++++--
ios/SourceBraidShare/WebPageCapture.swift | 1 +
ios/SourceBraidTests/ClipBuilderTests.swift | 15 +++++---
13 files changed, 103 insertions(+), 37 deletions(-)
diff --git a/ios/README.md b/ios/README.md
index 711e814..d0b514d 100644
--- a/ios/README.md
+++ b/ios/README.md
@@ -8,8 +8,8 @@ The iOS project contains a SwiftUI configuration app and a native Share Extensio
2. Select the **SourceBraid** project and the **SourceBraid** target.
3. Under **Signing & Capabilities**, select your Apple Developer team.
4. Repeat for the **SourceBraidShare** target.
-5. If the bundle identifiers are unavailable, replace `de.patrickschiller.sourcebraid` and `de.patrickschiller.sourcebraid.share` with identifiers owned by your team.
-6. Keep the App Group identical in both targets: `group.de.patrickschiller.sourcebraid`.
+5. The existing TestFlight app keeps its pre-rebrand bundle identifiers `de.patrickschiller.stowmark` and `de.patrickschiller.stowmark.share`; changing them would create a different app instead of an update.
+6. Keep the pre-rebrand App Group identical in both targets: `group.de.patrickschiller.stowmark`. This preserves settings and Keychain access across the SourceBraid rename.
7. Run the SourceBraid app on your iPhone once and configure GitHub.
The fine-grained GitHub token needs `Contents: Read and write` for the private SourceBraid repository. It is stored in a Keychain access group shared only by the app and extension.
@@ -38,8 +38,9 @@ xcodebuild \
-allowProvisioningUpdates
```
-The export options upload the archive directly to App Store Connect. The app
-record there must use bundle ID `de.patrickschiller.sourcebraid`.
+The export options upload the archive directly to App Store Connect. The existing
+app record uses bundle ID `de.patrickschiller.stowmark`; its customer-facing name
+is **SourceBraid**.
## Use
@@ -49,7 +50,7 @@ In FAZ, Safari, Files, or another app:
2. Choose **SourceBraid**. If it is hidden, use **More** to enable it.
3. Edit the title, add optional tags or a note, and tap **Save**.
-Apps such as Chrome and FAZ generally share only a URL. SourceBraid loads public web URLs in an isolated web view and converts the readable page content to Markdown before saving. Safari can additionally supply its already visible page text through the extension's preprocessing script. Pages that require an authenticated browser session or block the isolated request are saved as clearly labeled link-only clips.
+Apps such as Chrome and FAZ generally share only a URL. SourceBraid loads public web URLs in an isolated web view and converts the readable page content to Markdown before saving, recording the same extraction methods as the browser extension (including specialized DeepMind captures). Safari can additionally supply its already visible page text through the extension's preprocessing script. Shared PDFs are queued as `pdf-docling-pending`; the PDF is pushed last so the existing GitHub Actions workflow can safely convert it after its Markdown and index metadata exist. Pages that require an authenticated browser session or block the isolated request are saved as clearly labeled link-only clips.
## Build without signing
diff --git a/ios/SourceBraid.xcodeproj/project.pbxproj b/ios/SourceBraid.xcodeproj/project.pbxproj
index 3f1cf7d..e99fa28 100644
--- a/ios/SourceBraid.xcodeproj/project.pbxproj
+++ b/ios/SourceBraid.xcodeproj/project.pbxproj
@@ -451,7 +451,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SourceBraid/Resources/SourceBraid.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = H76K2HQUFB;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -462,7 +462,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -480,7 +480,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = SourceBraid/Resources/SourceBraid.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = H76K2HQUFB;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = SourceBraid/Resources/Info.plist;
@@ -490,7 +490,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.2.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -507,7 +507,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = SourceBraidShare/SourceBraidShare.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = H76K2HQUFB;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = SourceBraidShare/Info.plist;
@@ -516,7 +516,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.2.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.share;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -534,7 +534,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = SourceBraidShare/SourceBraidShare.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 4;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = H76K2HQUFB;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = SourceBraidShare/Info.plist;
@@ -543,7 +543,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.2.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.share;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -566,7 +566,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.tests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = iphonesimulator;
SWIFT_VERSION = 5.0;
@@ -586,7 +586,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.sourcebraid.tests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.patrickschiller.stowmark.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = iphonesimulator;
SWIFT_VERSION = 5.0;
diff --git a/ios/SourceBraid/Resources/Info.plist b/ios/SourceBraid/Resources/Info.plist
index a46768c..4bdbf0d 100644
--- a/ios/SourceBraid/Resources/Info.plist
+++ b/ios/SourceBraid/Resources/Info.plist
@@ -23,7 +23,7 @@
ITSAppUsesNonExemptEncryption
SourceBraidKeychainAccessGroup
- $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared
+ $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared
UILaunchScreen
UISupportedInterfaceOrientations
diff --git a/ios/SourceBraid/Resources/SourceBraid.entitlements b/ios/SourceBraid/Resources/SourceBraid.entitlements
index e6d1349..3e1e535 100644
--- a/ios/SourceBraid/Resources/SourceBraid.entitlements
+++ b/ios/SourceBraid/Resources/SourceBraid.entitlements
@@ -4,11 +4,11 @@
com.apple.security.application-groups
- group.de.patrickschiller.sourcebraid
+ group.de.patrickschiller.stowmark
keychain-access-groups
- $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared
+ $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared
diff --git a/ios/SourceBraid/Shared/ClipBuilder.swift b/ios/SourceBraid/Shared/ClipBuilder.swift
index 4e129a0..2da1a7e 100644
--- a/ios/SourceBraid/Shared/ClipBuilder.swift
+++ b/ios/SourceBraid/Shared/ClipBuilder.swift
@@ -45,16 +45,22 @@ struct SourceBraidIndexEntry: Encodable {
let captureMethod: String
let sourceType: String
let contentFormat: String
+ let conversionStatus: String?
+ let converter: String?
let capturedAt: String
let attachmentPath: String?
+ let pdfPath: String?
enum CodingKeys: String, CodingKey {
case title, url, path, date, tags, source
case captureMethod = "capture_method"
case sourceType = "source_type"
case contentFormat = "content_format"
+ case conversionStatus = "conversion_status"
+ case converter
case capturedAt = "captured_at"
case attachmentPath = "attachment_path"
+ case pdfPath = "pdf_path"
}
}
@@ -90,11 +96,16 @@ enum ClipBuilder {
let captureMethod: String
let sourceType: String
let contentFormat: String
+ let queuesPDFConversion = input.fileData != nil && input.mimeType == "application/pdf"
- if input.fileData != nil {
+ if queuesPDFConversion {
+ captureMethod = "pdf-docling-pending"
+ sourceType = "pdf"
+ contentFormat = "pdf"
+ } else if input.fileData != nil {
captureMethod = "ios-share-file"
sourceType = "document"
- contentFormat = input.mimeType == "application/pdf" ? "pdf" : "file"
+ contentFormat = "file"
} else if !input.articleText.trimmed.isEmpty {
captureMethod = input.articleCaptureMethod ?? "ios-share-safari"
sourceType = "article"
@@ -113,7 +124,8 @@ enum ClipBuilder {
if let data = input.fileData {
let ext = safeFileExtension(input.filename, mimeType: input.mimeType)
let clipSlug = path.split(separator: "/").last.map(String.init)?.replacingOccurrences(of: ".md", with: "") ?? "document"
- let attachmentPath = "\(normalizedConfiguration.rootFolder)/\(captureDate.prefix(4))/\(captureDate.dropFirst(5).prefix(2))/assets/\(clipSlug)/original.\(ext)"
+ let attachmentName = queuesPDFConversion ? "source.pdf" : "original.\(ext)"
+ let attachmentPath = "\(normalizedConfiguration.rootFolder)/\(captureDate.prefix(4))/\(captureDate.dropFirst(5).prefix(2))/assets/\(clipSlug)/\(attachmentName)"
attachment = CaptureAttachment(path: attachmentPath, data: data)
} else {
attachment = nil
@@ -129,6 +141,7 @@ enum ClipBuilder {
captureMethod: captureMethod,
sourceType: sourceType,
contentFormat: contentFormat,
+ queuesPDFConversion: queuesPDFConversion,
tags: tags,
notes: notes,
attachmentPath: attachment?.path,
@@ -144,8 +157,11 @@ enum ClipBuilder {
captureMethod: captureMethod,
sourceType: sourceType,
contentFormat: contentFormat,
+ conversionStatus: queuesPDFConversion ? "pending" : nil,
+ converter: queuesPDFConversion ? "docling" : nil,
capturedAt: capturedAt,
- attachmentPath: attachment?.path
+ attachmentPath: queuesPDFConversion ? nil : attachment?.path,
+ pdfPath: queuesPDFConversion ? attachment?.path : nil
)
return CaptureDraft(title: title, path: path, markdown: markdown, indexEntry: entry, attachment: attachment)
}
@@ -168,6 +184,7 @@ enum ClipBuilder {
captureMethod: String,
sourceType: String,
contentFormat: String,
+ queuesPDFConversion: Bool,
tags: [String],
notes: String,
attachmentPath: String?,
@@ -184,7 +201,11 @@ enum ClipBuilder {
"source_type: \(yamlQuote(sourceType))",
"content_format: \(yamlQuote(contentFormat))"
]
- if let attachmentPath {
+ if queuesPDFConversion, let attachmentPath {
+ lines.append("conversion_status: \(yamlQuote("pending"))")
+ lines.append("converter: \(yamlQuote("docling"))")
+ lines.append("pdf_path: \(yamlQuote(attachmentPath))")
+ } else if let attachmentPath {
lines.append("attachment_path: \(yamlQuote(attachmentPath))")
}
if !tags.isEmpty {
@@ -208,7 +229,9 @@ enum ClipBuilder {
if let url = input.url {
lines.append(contentsOf: ["[Open original source](\(url.absoluteString))", ""])
}
- if let attachmentPath {
+ if queuesPDFConversion {
+ lines.append(contentsOf: ["> The original PDF has been uploaded. GitHub Actions will replace this placeholder with Docling Markdown.", ""])
+ } else if let attachmentPath {
lines.append(contentsOf: ["[Open saved attachment](\(relativePath(from: markdownPath, to: attachmentPath)))", ""])
}
diff --git a/ios/SourceBraid/Shared/GitHubClient.swift b/ios/SourceBraid/Shared/GitHubClient.swift
index 5a6b7fe..1d0004e 100644
--- a/ios/SourceBraid/Shared/GitHubClient.swift
+++ b/ios/SourceBraid/Shared/GitHubClient.swift
@@ -14,6 +14,13 @@ struct GitHubClient {
}
func save(_ draft: CaptureDraft) async throws {
+ if draft.indexEntry.captureMethod == "pdf-docling-pending", let attachment = draft.attachment {
+ // The PDF push triggers the conversion workflow, so its metadata must exist first.
+ try await putReplacing(path: draft.path, data: Data(draft.markdown.utf8), message: "Queue PDF SourceBraid capture: \(draft.title)")
+ try await updateIndex(with: draft.indexEntry)
+ try await putReplacing(path: attachment.path, data: attachment.data, message: "Queue SourceBraid PDF: \(draft.title)")
+ return
+ }
if let attachment = draft.attachment {
try await putReplacing(path: attachment.path, data: attachment.data, message: "Save SourceBraid attachment: \(draft.title)")
}
diff --git a/ios/SourceBraid/Shared/SourceBraidEnvironment.swift b/ios/SourceBraid/Shared/SourceBraidEnvironment.swift
index e731a2f..4c51561 100644
--- a/ios/SourceBraid/Shared/SourceBraidEnvironment.swift
+++ b/ios/SourceBraid/Shared/SourceBraidEnvironment.swift
@@ -1,8 +1,9 @@
import Foundation
enum SourceBraidEnvironment {
- static let appGroupIdentifier = "group.de.patrickschiller.sourcebraid"
- static let keychainService = "de.patrickschiller.sourcebraid"
+ // Keep the pre-rebrand identifiers so TestFlight upgrades retain settings and credentials.
+ static let appGroupIdentifier = "group.de.patrickschiller.stowmark"
+ static let keychainService = "de.patrickschiller.stowmark"
static let keychainAccount = "github-token"
static let recentCapturesKey = "recent-captures"
diff --git a/ios/SourceBraidShare/Info.plist b/ios/SourceBraidShare/Info.plist
index 79e9158..a0553d2 100644
--- a/ios/SourceBraidShare/Info.plist
+++ b/ios/SourceBraidShare/Info.plist
@@ -19,7 +19,7 @@
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
SourceBraidKeychainAccessGroup
- $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared
+ $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared
NSExtension
NSExtensionAttributes
diff --git a/ios/SourceBraidShare/ShareViewModel.swift b/ios/SourceBraidShare/ShareViewModel.swift
index 3051e8b..7630dc2 100644
--- a/ios/SourceBraidShare/ShareViewModel.swift
+++ b/ios/SourceBraidShare/ShareViewModel.swift
@@ -83,7 +83,7 @@ final class ShareViewModel: ObservableObject {
let result = try await capture.capture(url: url)
input.articleText = result.markdown
input.articleContentFormat = "markdown"
- input.articleCaptureMethod = "ios-share-web"
+ input.articleCaptureMethod = result.captureMethod
if input.suggestedTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
input.suggestedTitle = result.title
}
diff --git a/ios/SourceBraidShare/SourceBraidShare.entitlements b/ios/SourceBraidShare/SourceBraidShare.entitlements
index e6d1349..3e1e535 100644
--- a/ios/SourceBraidShare/SourceBraidShare.entitlements
+++ b/ios/SourceBraidShare/SourceBraidShare.entitlements
@@ -4,11 +4,11 @@
com.apple.security.application-groups
- group.de.patrickschiller.sourcebraid
+ group.de.patrickschiller.stowmark
keychain-access-groups
- $(AppIdentifierPrefix)de.patrickschiller.sourcebraid.shared
+ $(AppIdentifierPrefix)de.patrickschiller.stowmark.shared
diff --git a/ios/SourceBraidShare/WebPageCapture.js b/ios/SourceBraidShare/WebPageCapture.js
index 010b136..4c88669 100644
--- a/ios/SourceBraidShare/WebPageCapture.js
+++ b/ios/SourceBraidShare/WebPageCapture.js
@@ -176,8 +176,33 @@
return candidates.sort(function (left, right) { return rootScore(right) - rootScore(left); })[0];
}
- var root = chooseRoot();
- if (!root) return JSON.stringify({ title: document.title || "", url: location.href, markdown: "" });
+ function chooseGoogleDeepMindRoot() {
+ if (location.hostname.toLowerCase() !== "deepmind.google" || location.pathname.indexOf("/blog/") !== 0) {
+ return null;
+ }
+ var main = document.querySelector("main");
+ if (!main) return null;
+
+ var content = document.createElement("div");
+ Array.from(main.children).some(function (section) {
+ if (section.tagName !== "SECTION") return false;
+ var heading = cleanText((section.querySelector("h1, h2, h3") || {}).textContent || "");
+ if (/^related posts$/i.test(heading)) return true;
+ if (section.matches(".section-cover") || section.querySelector("h1")) return false;
+ var clone = section.cloneNode(true);
+ clone.querySelectorAll([
+ "script", "style", "nav", "aside", "form", "iframe", "noscript", "svg", "button", "[aria-label='Share']"
+ ].join(",")).forEach(function (node) { node.remove(); });
+ if (cleanText(clone.textContent).length >= 40 || clone.querySelector("img, video")) content.appendChild(clone);
+ return false;
+ });
+ return cleanText(content.textContent).length >= 500 ? content : null;
+ }
+
+ var specializedRoot = chooseGoogleDeepMindRoot();
+ var root = specializedRoot || chooseRoot();
+ var captureMethod = specializedRoot ? "google-deepmind-dom" : "dom-readable";
+ if (!root) return JSON.stringify({ title: document.title || "", url: location.href, markdown: "", captureMethod: captureMethod });
var clone = root.cloneNode(true);
clone.querySelectorAll([
@@ -198,6 +223,7 @@
return JSON.stringify({
title: title,
url: location.href,
- markdown: markdown
+ markdown: markdown,
+ captureMethod: captureMethod
});
})();
diff --git a/ios/SourceBraidShare/WebPageCapture.swift b/ios/SourceBraidShare/WebPageCapture.swift
index 1e3da7b..373b10a 100644
--- a/ios/SourceBraidShare/WebPageCapture.swift
+++ b/ios/SourceBraidShare/WebPageCapture.swift
@@ -5,6 +5,7 @@ struct WebPageCaptureResult: Decodable {
let title: String
let url: String
let markdown: String
+ let captureMethod: String
}
@MainActor
diff --git a/ios/SourceBraidTests/ClipBuilderTests.swift b/ios/SourceBraidTests/ClipBuilderTests.swift
index cf1effd..a776202 100644
--- a/ios/SourceBraidTests/ClipBuilderTests.swift
+++ b/ios/SourceBraidTests/ClipBuilderTests.swift
@@ -56,9 +56,16 @@ final class ClipBuilderTests: XCTestCase {
)
XCTAssertEqual(draft.attachment?.data, Data("pdf".utf8))
- XCTAssertEqual(draft.attachment?.path.split(separator: ".").last, "pdf")
- XCTAssertTrue(draft.markdown.contains("[Open saved attachment](assets/"))
+ XCTAssertTrue(draft.attachment?.path.hasSuffix("/source.pdf") == true)
+ XCTAssertTrue(draft.markdown.contains("capture_method: \"pdf-docling-pending\""))
XCTAssertTrue(draft.markdown.contains("content_format: \"pdf\""))
+ XCTAssertTrue(draft.markdown.contains("conversion_status: \"pending\""))
+ XCTAssertTrue(draft.markdown.contains("converter: \"docling\""))
+ XCTAssertTrue(draft.markdown.contains("pdf_path: \"") && draft.markdown.contains("/source.pdf\""))
+ XCTAssertEqual(draft.indexEntry.captureMethod, "pdf-docling-pending")
+ XCTAssertEqual(draft.indexEntry.conversionStatus, "pending")
+ XCTAssertEqual(draft.indexEntry.converter, "docling")
+ XCTAssertEqual(draft.indexEntry.pdfPath, draft.attachment?.path)
}
func testFetchedWebCaptureStoresMarkdownMetadataAndBody() throws {
@@ -68,7 +75,7 @@ final class ClipBuilderTests: XCTestCase {
sharedText: "https://blog.example.com/article",
articleText: "## Introduction\n\nA [useful link](https://example.com).",
articleContentFormat: "markdown",
- articleCaptureMethod: "ios-share-web",
+ articleCaptureMethod: "dom-readable",
fileData: nil,
filename: nil,
mimeType: nil
@@ -83,7 +90,7 @@ final class ClipBuilderTests: XCTestCase {
now: date
)
- XCTAssertTrue(draft.markdown.contains("capture_method: \"ios-share-web\""))
+ XCTAssertTrue(draft.markdown.contains("capture_method: \"dom-readable\""))
XCTAssertTrue(draft.markdown.contains("content_format: \"markdown\""))
XCTAssertTrue(draft.markdown.contains("## Captured content\n\n## Introduction"))
XCTAssertFalse(draft.markdown.contains("## Shared text"))