krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 845710a22b

845710a22b57469c87b66752c230ed8c274b4bf3

parent: 2b040ec5d8

Verified 路 cmc

cmc <hello@cleberg.net> 路 2026-10-04 23:14 UTC

Add OrgCore package and SourceText byte contract

Layout: unified 路 split

Package.swift added +14
@@ -0,0 +1,14 @@
1// swift-tools-version: 6.2
2import PackageDescription
3
4let package = Package(
5 name: "Orgstar",
6 platforms: [.macOS(.v26), .iOS(.v26)],
7 products: [
8 .library(name: "OrgCore", targets: ["OrgCore"])
9 ],
10 targets: [
11 .target(name: "OrgCore"),
12 .testTarget(name: "OrgCoreTests", dependencies: ["OrgCore"])
13 ]
14)
Sources/OrgCore/SourceText.swift added +40
@@ -0,0 +1,40 @@
1import Foundation
2
3/// A file's bytes and their decoded text. Only UTF-8 (with or without a BOM) is editable;
4/// anything else decodes with replacement characters for display and is never written back.
5public struct SourceText: Sendable {
6 public let originalBytes: [UInt8]
7 public let hasBOM: Bool
8 public let isValidUTF8: Bool
9 /// Decoded text without the BOM.
10 public let text: String
11
12 private static let bom: [UInt8] = [0xEF, 0xBB, 0xBF]
13
14 public init(bytes: [UInt8]) {
15 originalBytes = bytes
16 hasBOM = bytes.starts(with: Self.bom)
17 let body = hasBOM ? Array(bytes.dropFirst(3)) : bytes
18 if let decoded = String(validating: body, as: UTF8.self) {
19 text = decoded
20 isValidUTF8 = true
21 } else {
22 text = String(decoding: body, as: UTF8.self)
23 isValidUTF8 = false
24 }
25 }
26
27 public init(_ text: String) {
28 self.init(bytes: Array(text.utf8))
29 }
30
31 public var isEditable: Bool { isValidUTF8 }
32
33 /// Bytes to write for `newText`. Unchanged text returns the original bytes. Valid UTF-8
34 /// round-trips through `String` unchanged, so untouched spans keep their exact bytes.
35 public func encode(_ newText: String) -> [UInt8] {
36 if newText == text { return originalBytes }
37 precondition(isEditable, "a read-only document cannot be re-encoded")
38 return (hasBOM ? Self.bom : []) + Array(newText.utf8)
39 }
40}
Tests/OrgCoreTests/SourceTextTests.swift added +40
@@ -0,0 +1,40 @@
1import Testing
2@testable import OrgCore
3
4struct SourceTextTests {
5 @Test func plainUTF8() {
6 let source = SourceText(bytes: Array("* a\n".utf8))
7 #expect(source.text == "* a\n")
8 #expect(!source.hasBOM)
9 #expect(source.isEditable)
10 }
11
12 @Test func bomIsStrippedAndRestored() {
13 let bytes: [UInt8] = [0xEF, 0xBB, 0xBF] + Array("x\n".utf8)
14 let source = SourceText(bytes: bytes)
15 #expect(source.hasBOM)
16 #expect(source.text == "x\n")
17 #expect(source.encode(source.text) == bytes)
18 #expect(source.encode("y\n") == [0xEF, 0xBB, 0xBF] + Array("y\n".utf8))
19 }
20
21 @Test func crlfAndMixedEndingsSurvive() {
22 let bytes = Array("a\r\nb\nc\r\n".utf8)
23 let source = SourceText(bytes: bytes)
24 #expect(source.encode(source.text) == bytes)
25 }
26
27 @Test func invalidUTF8IsReadOnlyAndUnchanged() {
28 let bytes: [UInt8] = [0x61, 0xFF, 0x0A]
29 let source = SourceText(bytes: bytes)
30 #expect(!source.isValidUTF8)
31 #expect(!source.isEditable)
32 #expect(source.encode(source.text) == bytes)
33 }
34
35 @Test func nonBMPAndCombiningSurvive() {
36 let bytes = Array("馃榾 e\u{301}\n".utf8)
37 let source = SourceText(bytes: bytes)
38 #expect(source.encode(source.text) == bytes)
39 }
40}