@swiftbrowser/swift 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/Package.swift +23 -0
  2. package/README.md +12 -2
  3. package/Sources/Charts/Charts.swift +65 -0
  4. package/Sources/FoundationBridge/FoundationBridge.swift +77 -0
  5. package/Sources/FoundationBridge/Locale.swift +86 -0
  6. package/Sources/FoundationBridge/Timer.swift +48 -0
  7. package/Sources/FoundationBridge/UITextChecker.swift +30 -0
  8. package/Sources/FoundationLite/Common.swift +3 -0
  9. package/Sources/FoundationLite/FoundationLite.swift +171 -0
  10. package/Sources/FoundationLite/Interaction.swift +146 -0
  11. package/Sources/FoundationLite/Timer.swift +48 -0
  12. package/Sources/SwiftUI/API/Accessibility.swift +132 -0
  13. package/Sources/SwiftUI/API/Alert.swift +221 -0
  14. package/Sources/SwiftUI/API/Animation.swift +253 -0
  15. package/Sources/SwiftUI/API/App.swift +57 -0
  16. package/Sources/SwiftUI/API/Color.swift +161 -0
  17. package/Sources/SwiftUI/API/Controls.swift +268 -0
  18. package/Sources/SwiftUI/API/CustomLayout.swift +403 -0
  19. package/Sources/SwiftUI/API/Decorations.swift +92 -0
  20. package/Sources/SwiftUI/API/EditMode.swift +269 -0
  21. package/Sources/SwiftUI/API/Effects.swift +290 -0
  22. package/Sources/SwiftUI/API/Environment.swift +355 -0
  23. package/Sources/SwiftUI/API/Focus.swift +129 -0
  24. package/Sources/SwiftUI/API/Font.swift +131 -0
  25. package/Sources/SwiftUI/API/Gauge.swift +289 -0
  26. package/Sources/SwiftUI/API/Geometry.swift +89 -0
  27. package/Sources/SwiftUI/API/GeometryReader.swift +124 -0
  28. package/Sources/SwiftUI/API/Gestures.swift +342 -0
  29. package/Sources/SwiftUI/API/Grid.swift +188 -0
  30. package/Sources/SwiftUI/API/LabelStyle.swift +117 -0
  31. package/Sources/SwiftUI/API/Layout.swift +165 -0
  32. package/Sources/SwiftUI/API/Lists.swift +276 -0
  33. package/Sources/SwiftUI/API/Menu.swift +51 -0
  34. package/Sources/SwiftUI/API/Modifiers.swift +243 -0
  35. package/Sources/SwiftUI/API/Navigation.swift +371 -0
  36. package/Sources/SwiftUI/API/ObservableObject.swift +279 -0
  37. package/Sources/SwiftUI/API/OnChange.swift +48 -0
  38. package/Sources/SwiftUI/API/Phase8Controls.swift +394 -0
  39. package/Sources/SwiftUI/API/Picker.swift +176 -0
  40. package/Sources/SwiftUI/API/Presentation.swift +109 -0
  41. package/Sources/SwiftUI/API/Primitives.swift +390 -0
  42. package/Sources/SwiftUI/API/Publishers.swift +107 -0
  43. package/Sources/SwiftUI/API/RowActions.swift +83 -0
  44. package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
  45. package/Sources/SwiftUI/API/ShapeStyle.swift +311 -0
  46. package/Sources/SwiftUI/API/State.swift +127 -0
  47. package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
  48. package/Sources/SwiftUI/API/Tabs.swift +119 -0
  49. package/Sources/SwiftUI/API/Tasks.swift +35 -0
  50. package/Sources/SwiftUI/API/Timeline.swift +212 -0
  51. package/Sources/SwiftUI/API/Toolbar.swift +215 -0
  52. package/Sources/SwiftUI/API/View.swift +207 -0
  53. package/Sources/SwiftUI/API/ViewModifier.swift +40 -0
  54. package/Sources/SwiftUI/API/VisualEffect.swift +82 -0
  55. package/Sources/SwiftUI/Charts/Axis.swift +319 -0
  56. package/Sources/SwiftUI/Charts/Chart.swift +646 -0
  57. package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
  58. package/Sources/SwiftUI/Charts/Marks.swift +204 -0
  59. package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
  60. package/Sources/SwiftUI/Charts/Scales.swift +326 -0
  61. package/Sources/SwiftUI/Runtime/Executor.swift +83 -0
  62. package/Sources/SwiftUI/Runtime/Exports.swift +65 -0
  63. package/Sources/SwiftUI/Runtime/JSON.swift +71 -0
  64. package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
  65. package/Sources/SwiftUI/Runtime/Ops.swift +106 -0
  66. package/Sources/SwiftUI/Runtime/Reconciler.swift +210 -0
  67. package/Sources/SwiftUI/Runtime/Runtime.swift +1367 -0
  68. package/Sources/_FoundationCommon/Bundle.swift +82 -0
  69. package/Sources/_FoundationCommon/UserDefaults.swift +170 -0
  70. package/Sources/_FoundationCommon/_FoundationCommon.swift +10 -0
  71. package/index.d.ts +2 -0
  72. package/index.mjs +16 -0
  73. package/package.json +28 -4
  74. package/sync-sources.mjs +19 -0
package/Package.swift ADDED
@@ -0,0 +1,23 @@
1
+ // swift-tools-version: 6.0
2
+ // The SwiftBrowser Swift package as @swiftbrowser/cli ships it: the SwiftUI
3
+ // shim, Swift Charts, FoundationLite and FoundationBridge, library products only. `Sources/` is
4
+ // copied from the repository's Sources/ at `npm pack` time (sync-sources.mjs);
5
+ // in a repository checkout the CLI uses the repository's own Package.swift.
6
+ import PackageDescription
7
+
8
+ let package = Package(
9
+ name: "SwiftBrowser",
10
+ products: [
11
+ .library(name: "SwiftUI", targets: ["SwiftUI"]),
12
+ .library(name: "Charts", targets: ["Charts"]),
13
+ .library(name: "FoundationLite", targets: ["FoundationLite"]),
14
+ .library(name: "FoundationBridge", targets: ["FoundationBridge"]),
15
+ ],
16
+ targets: [
17
+ .target(name: "SwiftUI", path: "Sources/SwiftUI"),
18
+ .target(name: "_FoundationCommon", dependencies: ["SwiftUI"], path: "Sources/_FoundationCommon"),
19
+ .target(name: "FoundationLite", dependencies: ["SwiftUI", "_FoundationCommon"], path: "Sources/FoundationLite"),
20
+ .target(name: "FoundationBridge", dependencies: ["SwiftUI", "_FoundationCommon"], path: "Sources/FoundationBridge"),
21
+ .target(name: "Charts", dependencies: ["SwiftUI"], path: "Sources/Charts"),
22
+ ]
23
+ )
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @swiftbrowser/swift
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The SwiftBrowser Swift package (the `SwiftUI` shim, `Charts`, `FoundationLite`
4
+ for lean builds, `FoundationBridge` for full-Foundation builds and the
5
+ `_FoundationCommon` they share) packaged for npm, so that `@swiftbrowser/cli` builds apps
6
+ against exactly the shim version it was released with. The CLI's generated
7
+ staging package depends on it by path (`swiftPackagePath()`).
8
+
9
+ In a repository checkout `Sources/` is not here; `swiftPackagePath()` then
10
+ points at the repository root, whose package also declares the example
11
+ executables (so an app named like one of them, `Counter` say, needs this
12
+ package synced: `node packages/swift/sync-sources.mjs`). `npm pack` copies the
13
+ sources in (`prepack`).
@@ -0,0 +1,65 @@
1
+ // Swift Charts for SwiftBrowser. The marks, axes and `Chart` itself are
2
+ // defined in the SwiftUI target (Sources/SwiftUI/Charts); this module
3
+ // re-exports them and adds the pieces that need Foundation's `Date` and
4
+ // `Calendar`. See docs/ops-protocol.md, "Phase 7".
5
+
6
+ @_exported import SwiftUI
7
+
8
+ #if canImport(FoundationEssentials)
9
+ import FoundationEssentials
10
+ #else
11
+ import Foundation
12
+ #endif
13
+
14
+ /// Dates plot as seconds since 1970; the chart treats them as UTC
15
+ /// (`Calendar.current` is UTC in the browser).
16
+ extension Date: Plottable {
17
+ public var _plottableValue: _PlottableValue { .date(timeIntervalSince1970) }
18
+
19
+ public init?(_plottableValue value: _PlottableValue) {
20
+ guard case .date(let seconds) = value else { return nil }
21
+ self.init(timeIntervalSince1970: seconds)
22
+ }
23
+ }
24
+
25
+ /// Calendar bins over a date range: `DateBins(unit: .hour, by: 3, range:)`.
26
+ /// `thresholds` are the bin boundaries, from the range's start (aligned to
27
+ /// `unit`) in steps of `by` units up to and including the first boundary at or
28
+ /// after the range's end.
29
+ public struct DateBins: Sendable {
30
+ public let unit: Calendar.Component
31
+ public let stride: Int
32
+ public let range: ClosedRange<Date>
33
+ public let thresholds: [Date]
34
+
35
+ public init(unit: Calendar.Component, by stride: Int = 1, range: ClosedRange<Date>,
36
+ calendar: Calendar = .current) {
37
+ self.unit = unit
38
+ self.stride = max(1, stride)
39
+ self.range = range
40
+ var thresholds: [Date] = []
41
+ var date = calendar.dateInterval(of: unit, for: range.lowerBound)?.start ?? range.lowerBound
42
+ while thresholds.count < 10_000 {
43
+ thresholds.append(date)
44
+ if date >= range.upperBound { break }
45
+ guard let next = calendar.date(byAdding: unit, value: self.stride, to: date), next > date else { break }
46
+ date = next
47
+ }
48
+ self.thresholds = thresholds
49
+ }
50
+
51
+ /// Bins over the dates' span.
52
+ public init(unit: Calendar.Component, by stride: Int = 1, dates: [Date], calendar: Calendar = .current) {
53
+ let lower = dates.min() ?? Date(timeIntervalSince1970: 0)
54
+ let upper = dates.max() ?? lower
55
+ self.init(unit: unit, by: stride, range: lower...upper, calendar: calendar)
56
+ }
57
+
58
+ /// The bin a date falls in (an index into `thresholds`), clamped to the bins.
59
+ public func index(for date: Date) -> Int {
60
+ let bins = max(thresholds.count - 1, 1)
61
+ var index = 0
62
+ for (i, threshold) in thresholds.enumerated() where date >= threshold { index = i }
63
+ return min(index, bins - 1)
64
+ }
65
+ }
@@ -0,0 +1,77 @@
1
+ /// What the SwiftUI shim needs from the real Foundation, for apps built with
2
+ /// `--foundation full`: the SDK's swift-corelibs Foundation (with ICU data
3
+ /// trimmed by the CLI) instead of FoundationEssentials + FoundationLite.
4
+ ///
5
+ /// The shim itself never imports Foundation (that would link its 10 MB into
6
+ /// every lean app), so it is generic over small protocols that this module
7
+ /// conforms Foundation's types to: `IndexSet` for `onDelete` / `onMove` /
8
+ /// `remove(atOffsets:)`, `Date` for `DatePicker`, and `FormatStyle` /
9
+ /// `ParseableFormatStyle` for `Text(_:format:)` and `TextField(_:value:format:)`.
10
+ /// `Timer.swift` adds the `Timer` and `RunLoop` that corelibs Foundation does
11
+ /// not have on WASI.
12
+ ///
13
+ /// Apps do not import this module themselves: the CLI's staging adds it next
14
+ /// to `import Foundation`, together with typealiases that settle the names
15
+ /// both the shim and Foundation declare (`CGFloat`, `CGPoint`, `CGSize`,
16
+ /// `CGRect`, `Timer`, `RunLoop`) in the shim's favor. See
17
+ /// docs/ops-protocol.md, "Phase 11".
18
+ import Foundation
19
+ import SwiftUI
20
+ import _FoundationCommon
21
+
22
+ // MARK: - Offset sets
23
+
24
+ extension IndexSet: _OffsetSet {
25
+ public init(_offsets: [Int]) { self.init(_offsets) }
26
+ /// `IndexSet` iterates its integers in ascending order without duplicates.
27
+ public var _offsets: [Int] { Array(self) }
28
+ }
29
+
30
+ // MARK: - Dates
31
+
32
+ extension Date: _DatePickerValue {
33
+ public var _secondsSince1970: Double { timeIntervalSince1970 }
34
+ public init(_secondsSince1970 seconds: Double) {
35
+ self.init(timeIntervalSince1970: seconds)
36
+ }
37
+ }
38
+
39
+ // MARK: - Format styles
40
+
41
+ /// A Foundation parseable format style as the shim's `_TextFormat`.
42
+ struct _ParseableFormatStyleText<F: ParseableFormatStyle>: _TextFormat where F.FormatOutput == String, F.Strategy.ParseInput == String {
43
+ let style: F
44
+ func _formatText(_ value: F.FormatInput) -> String { style.format(value) }
45
+ func _parseText(_ text: String) -> F.FormatInput? { try? style.parseStrategy.parse(text) }
46
+ }
47
+
48
+ extension Text {
49
+ /// `Text(price, format: .currency(code: "USD"))`, `Text(date, format: .dateTime.year())`.
50
+ public init<F: FormatStyle>(_ input: F.FormatInput, format: F) where F.FormatOutput == String {
51
+ self.init(verbatim: format.format(input))
52
+ }
53
+ }
54
+
55
+ extension TextField where Label == Text {
56
+ /// `TextField("Amount", value: $amount, format: .number)`: the field shows
57
+ /// the formatted value and sets it from what the user types when the
58
+ /// style's parse strategy accepts the text.
59
+ public init<S: StringProtocol, F: ParseableFormatStyle>(_ title: S, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil)
60
+ where F.FormatOutput == String, F.Strategy.ParseInput == String {
61
+ self.init(_placeholder: prompt?._content ?? String(title), value: value, format: _ParseableFormatStyleText(style: format))
62
+ }
63
+
64
+ public init<F: ParseableFormatStyle>(_ titleKey: LocalizedStringKey, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil)
65
+ where F.FormatOutput == String, F.Strategy.ParseInput == String {
66
+ self.init(_placeholder: prompt?._content ?? titleKey.key, value: value, format: _ParseableFormatStyleText(style: format))
67
+ }
68
+ }
69
+
70
+ // MARK: - Defaults and resources
71
+
72
+ /// `UserDefaults` that persists through the browser and the `Bundle` that
73
+ /// reads the app's resources (Sources/_FoundationCommon). The CLI's generated
74
+ /// file aliases both in the app, over corelibs Foundation's (whose
75
+ /// `UserDefaults` cannot persist on WASI and whose `Bundle.main` traps).
76
+ public typealias UserDefaults = _FoundationCommon.UserDefaults
77
+ public typealias Bundle = _FoundationCommon.Bundle
@@ -0,0 +1,86 @@
1
+ // `Locale.current` for the browser. swift-foundation has no way to set the
2
+ // current locale on WASI: `LocaleCache.preferences()` returns a hard-coded
3
+ // `en_001` (there are no CFPreferences and it does not read `LANG`), so a
4
+ // French user would get English formatting whatever the browser says.
5
+ //
6
+ // The CLI links full-Foundation apps with
7
+ // `--wrap=$s20FoundationEssentials11LocaleCacheV11preferencesAA0C11PreferencesV_SbtyF`
8
+ // (and the same for `Locale.preferredLanguages`), so the one call to that
9
+ // function, from `_currentAndCache`, lands here instead and reads the locale
10
+ // the host chose (`SB_LOCALE`, a BCP 47 tag among the app's shipped locales).
11
+ //
12
+ // This depends on the SDK's swift-foundation: the struct below mirrors
13
+ // `LocalePreferences` field for field (the non-framework build at Swift 6.4,
14
+ // 60 bytes on wasm32), and the method is on a class so the cache pointer rides
15
+ // in Swift's `self` slot like the original's. The SDK is pinned by the CLI
16
+ // (`SWIFT_VERSION`), and CI checks French formatting in a full-mode build, so
17
+ // a layout change in a future SDK shows up as a failed check, not a silent one.
18
+ // See docs/ops-protocol.md, "Phase 11".
19
+
20
+ #if os(WASI)
21
+ import Foundation
22
+
23
+ /// Mirror of `FoundationEssentials.LocalePreferences` (`package struct`, non-framework fields).
24
+ public struct _LocalePreferencesMirror {
25
+ public enum MeasurementUnit: Int { case centimeters, inches }
26
+ public enum TemperatureUnit: Int { case fahrenheit, celsius }
27
+ public enum CalendarIdentifier: Int { case gregorian }
28
+ public var metricUnits: Bool?
29
+ public var languages: [String]?
30
+ public var locale: String?
31
+ public var collationOrder: String?
32
+ public var firstWeekday: [CalendarIdentifier: Int]?
33
+ public var minDaysInFirstWeek: [CalendarIdentifier: Int]?
34
+ public var numberSymbols: [UInt32: String]?
35
+ public var country: String?
36
+ public var measurementUnits: MeasurementUnit?
37
+ public var temperatureUnit: TemperatureUnit?
38
+ public var force24Hour: Bool?
39
+ public var force12Hour: Bool?
40
+ public init() {}
41
+ }
42
+
43
+ /// The locale the host asked for, as ICU (`fr_FR`) and BCP 47 (`fr-FR`) identifiers; nil when unset.
44
+ func _requestedLocale() -> (icu: String, bcp47: String)? {
45
+ guard let raw = ProcessInfo.processInfo.environment["SB_LOCALE"]?.trimmingCharacters(in: .whitespaces), !raw.isEmpty else { return nil }
46
+ let parts = raw.split(whereSeparator: { $0 == "-" || $0 == "_" }).map(String.init)
47
+ guard let language = parts.first, (2...3).contains(language.count), language.allSatisfy(\.isLetter) else { return nil }
48
+ var icu = [language.lowercased()]
49
+ var bcp = [language.lowercased()]
50
+ for part in parts.dropFirst() {
51
+ if part.count == 4, part.allSatisfy(\.isLetter) {
52
+ let script = part.prefix(1).uppercased() + part.dropFirst().lowercased()
53
+ icu.append(script); bcp.append(script)
54
+ } else if part.count == 2 && part.allSatisfy(\.isLetter) || part.count == 3 && part.allSatisfy(\.isNumber) {
55
+ icu.append(part.uppercased()); bcp.append(part.uppercased())
56
+ }
57
+ }
58
+ return (icu.joined(separator: "_"), bcp.joined(separator: "-"))
59
+ }
60
+
61
+ /// Stands in for the `LocaleCache` instance: Swift passes `self` in its own
62
+ /// slot (an explicit trailing parameter on wasm) and a class method is what
63
+ /// puts it there. `self` is the cache's storage and is never touched.
64
+ public final class _LocaleCacheHandle {
65
+ @_silgen_name("__wrap_$s20FoundationEssentials11LocaleCacheV11preferencesAA0C11PreferencesV_SbtyF")
66
+ public func _preferences() -> (_LocalePreferencesMirror, Bool) {
67
+ var prefs = _LocalePreferencesMirror()
68
+ if let locale = _requestedLocale() {
69
+ prefs.locale = locale.icu
70
+ prefs.languages = [locale.bcp47]
71
+ } else {
72
+ prefs.locale = "en_001"
73
+ prefs.languages = ["en-001"]
74
+ }
75
+ return (prefs, true)
76
+ }
77
+ }
78
+
79
+ /// Stands in for the `Locale` metatype of the static `preferredLanguages` getter.
80
+ public final class _LocaleMetatypeHandle {
81
+ @_silgen_name("__wrap_$s20FoundationEssentials6LocaleV18preferredLanguagesSaySSGvgZ")
82
+ public func _preferredLanguages() -> [String] {
83
+ [_requestedLocale()?.bcp47 ?? "en-001"]
84
+ }
85
+ }
86
+ #endif
@@ -0,0 +1,48 @@
1
+ // `Timer` and `RunLoop` for full-Foundation apps in the browser. swift-corelibs
2
+ // Foundation marks `RunLoop` unavailable on WASI and its `Timer` needs
3
+ // CFRunLoop, which the WASI build does not have (the link fails), and
4
+ // `Timer.publish` is Combine's anyway. These two classes have the same names;
5
+ // the CLI's staging gives the app `typealias Timer = FoundationBridge.Timer`
6
+ // and `typealias RunLoop = FoundationBridge.RunLoop`, so `Timer.publish(every:
7
+ // 1, on: .main, in: .common).autoconnect()` and `Timer.scheduledTimer` resolve
8
+ // here. Ticks come from the runtime's executor (`_IntervalPublisher` and
9
+ // `_ExecutorTimer` in the SwiftUI shim), like FoundationLite's.
10
+
11
+ import Foundation
12
+ import SwiftUI
13
+
14
+ public final class RunLoop: @unchecked Sendable {
15
+ public struct Mode: Hashable, Sendable {
16
+ public let rawValue: String
17
+ public init(rawValue: String) { self.rawValue = rawValue }
18
+ public static let `default` = Mode(rawValue: "default")
19
+ public static let common = Mode(rawValue: "common")
20
+ }
21
+ nonisolated(unsafe) public static let main = RunLoop()
22
+ public static var current: RunLoop { main }
23
+ }
24
+
25
+ public final class Timer: @unchecked Sendable {
26
+ /// Emits the current date every `interval` seconds once subscribed.
27
+ public typealias TimerPublisher = _IntervalPublisher<Date>
28
+
29
+ public static func publish(every interval: TimeInterval, tolerance: TimeInterval? = nil, on runLoop: RunLoop = .main, in mode: RunLoop.Mode = .default, options: Never? = nil) -> TimerPublisher {
30
+ _IntervalPublisher(every: interval) { Date() }
31
+ }
32
+
33
+ private var core: _ExecutorTimer!
34
+
35
+ public var isValid: Bool { core.isValid }
36
+ public var timeInterval: TimeInterval { core.timeInterval }
37
+
38
+ init(interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (Timer) -> Void) {
39
+ core = _ExecutorTimer(interval: interval, repeats: repeats) { _ in block(self) }
40
+ }
41
+
42
+ @discardableResult
43
+ public static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping @Sendable (Timer) -> Void) -> Timer {
44
+ Timer(interval: interval, repeats: repeats, block: block)
45
+ }
46
+
47
+ public func invalidate() { core.invalidate() }
48
+ }
@@ -0,0 +1,30 @@
1
+ // A stand-in for UIKit's `UITextChecker`, which SwiftUI apps reach through
2
+ // SwiftUI's re-export of UIKit (Hacking with SwiftUI's WordScramble checks
3
+ // spelling with it). Full-Foundation builds only (NSRange is Foundation's). The
4
+ // browser has no spell checker to ask, so every word
5
+ // passes: `rangeOfMisspelledWord` is always `NSNotFound`, `guesses` and
6
+ // `completions` are nil. Enough for the code to compile and run; the game
7
+ // just stops rejecting misspellings.
8
+
9
+ import Foundation
10
+
11
+ public final class UITextChecker: @unchecked Sendable {
12
+ public init() {}
13
+
14
+ /// Always "no misspelling": `NSRange(location: NSNotFound, length: 0)`.
15
+ public func rangeOfMisspelledWord(in stringToCheck: String, range: NSRange, startingAt startingOffset: Int,
16
+ wrap wrapFlag: Bool, language: String) -> NSRange {
17
+ NSRange(location: NSNotFound, length: 0)
18
+ }
19
+
20
+ public func guesses(forWordRange range: NSRange, in string: String, language: String) -> [String]? { nil }
21
+ public func completions(forPartialWordRange range: NSRange, in string: String, language: String) -> [String]? { nil }
22
+
23
+ public static let availableLanguages: [String] = ["en"]
24
+ public static func hasLearnedWord(_ word: String) -> Bool { false }
25
+ public static func learnWord(_ word: String) {}
26
+ public static func unlearnWord(_ word: String) {}
27
+ public func ignoreWord(_ wordToIgnore: String) {}
28
+ public func ignoredWords() -> [String]? { nil }
29
+ public func setIgnoredWords(_ words: [String]?) {}
30
+ }
@@ -0,0 +1,3 @@
1
+ // `UserDefaults` and `Bundle` for the browser are shared with FoundationBridge
2
+ // (Sources/_FoundationCommon); they are part of this module's API.
3
+ @_exported import _FoundationCommon
@@ -0,0 +1,171 @@
1
+ /// The slice of Foundation's formatting API that SwiftUI apps lean on, for
2
+ /// platforms where only `FoundationEssentials` is linked (WebAssembly, Linux):
3
+ /// the CLI's lean mode and the repository's own examples.
4
+ ///
5
+ /// Full Foundation on Wasm costs about 14 MB more than FoundationEssentials
6
+ /// even with its ICU data trimmed (docs/ops-protocol.md, "Phase 11"), so apps
7
+ /// that must stay small import `FoundationEssentials` plus this module; the
8
+ /// CLI's default is the real Foundation with `FoundationBridge`.
9
+ /// The spellings match Foundation's, so a file that does
10
+ ///
11
+ /// #if canImport(FoundationEssentials)
12
+ /// import FoundationEssentials
13
+ /// import FoundationLite
14
+ /// #else
15
+ /// import Foundation
16
+ /// #endif
17
+ ///
18
+ /// compiles unchanged against Apple's SDKs. Output is fixed to US English
19
+ /// conventions (`1,234.5`, `Nov 14, 2023`, `10:13 PM`), which is also what
20
+ /// keeps headless snapshots byte-stable.
21
+ #if canImport(FoundationEssentials)
22
+ import FoundationEssentials
23
+
24
+ // MARK: - Dates
25
+
26
+ extension Date {
27
+ /// Namespaces for `formatted(date:time:)`, mirroring `Foundation.Date.FormatStyle`.
28
+ public struct FormatStyle: Sendable {
29
+ public enum DateStyle: Hashable, Sendable {
30
+ case omitted, numeric, abbreviated, long, complete
31
+ }
32
+
33
+ public enum TimeStyle: Hashable, Sendable {
34
+ case omitted, shortened, standard, complete
35
+ }
36
+ }
37
+
38
+ /// `11/14/2023, 10:13 PM`, `Nov 14, 2023`, `November 14, 2023`,
39
+ /// `Tuesday, November 14, 2023`; times `10:13 PM`, `10:13:20 PM`.
40
+ /// Uses `Calendar.current`, which is Gregorian/UTC on WebAssembly.
41
+ public func formatted(date dateStyle: FormatStyle.DateStyle = .numeric,
42
+ time timeStyle: FormatStyle.TimeStyle = .shortened) -> String {
43
+ let calendar = Calendar.current
44
+ let c = calendar.dateComponents([.year, .month, .day, .weekday, .hour, .minute, .second], from: self)
45
+ let year = c.year ?? 1970, month = c.month ?? 1, day = c.day ?? 1
46
+ let monthName = _monthNames[(month - 1) % 12]
47
+ let datePart: String?
48
+ switch dateStyle {
49
+ case .omitted: datePart = nil
50
+ case .numeric: datePart = "\(month)/\(day)/\(year)"
51
+ case .abbreviated: datePart = "\(monthName.prefix(3)) \(day), \(year)"
52
+ case .long: datePart = "\(monthName) \(day), \(year)"
53
+ case .complete: datePart = "\(_weekdayNames[((c.weekday ?? 1) - 1) % 7]), \(monthName) \(day), \(year)"
54
+ }
55
+ let timePart: String?
56
+ if timeStyle == .omitted {
57
+ timePart = nil
58
+ } else {
59
+ let hour24 = c.hour ?? 0, minute = c.minute ?? 0, second = c.second ?? 0
60
+ let hour12 = hour24 % 12 == 0 ? 12 : hour24 % 12
61
+ let suffix = hour24 < 12 ? "AM" : "PM"
62
+ let base = "\(hour12):\(_twoDigits(minute))"
63
+ switch timeStyle {
64
+ case .shortened: timePart = "\(base) \(suffix)"
65
+ case .standard, .complete: timePart = "\(base):\(_twoDigits(second)) \(suffix)"
66
+ case .omitted: timePart = nil
67
+ }
68
+ }
69
+ switch (datePart, timePart) {
70
+ case (let d?, let t?): return "\(d), \(t)"
71
+ case (let d?, nil): return d
72
+ case (nil, let t?): return t
73
+ case (nil, nil): return ""
74
+ }
75
+ }
76
+
77
+ public func formatted() -> String { formatted(date: .numeric, time: .shortened) }
78
+ }
79
+
80
+ private let _monthNames = ["January", "February", "March", "April", "May", "June", "July",
81
+ "August", "September", "October", "November", "December"]
82
+ private let _weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
83
+
84
+ func _twoDigits(_ value: Int) -> String { value < 10 ? "0\(value)" : "\(value)" }
85
+
86
+ // MARK: - Numbers
87
+
88
+ /// Inserts thousands separators into a run of digits (optionally signed).
89
+ func _grouped(_ digits: Substring) -> String {
90
+ var sign = ""
91
+ var body = digits
92
+ if body.first == "-" {
93
+ sign = "-"
94
+ body = body.dropFirst()
95
+ }
96
+ var out: [Character] = []
97
+ for (index, ch) in body.reversed().enumerated() {
98
+ if index > 0 && index % 3 == 0 { out.append(",") }
99
+ out.append(ch)
100
+ }
101
+ return sign + String(out.reversed())
102
+ }
103
+
104
+ /// Formats a decimal string (`-1234.5000`) the way `.number` does: grouped
105
+ /// integer digits and at most `maxFractionDigits` fraction digits, trailing
106
+ /// zeros dropped.
107
+ func _formatDecimalString(_ text: String, maxFractionDigits: Int = 3) -> String {
108
+ let parts = text.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: false)
109
+ let integer = _grouped(parts[0])
110
+ guard parts.count == 2 else { return integer }
111
+ var fraction = String(parts[1].prefix(maxFractionDigits))
112
+ while fraction.last == "0" { fraction.removeLast() }
113
+ return fraction.isEmpty ? integer : "\(integer).\(fraction)"
114
+ }
115
+
116
+ extension BinaryInteger {
117
+ /// `1234` → `1,234`.
118
+ public func formatted() -> String { _grouped(Substring(String(self))) }
119
+ }
120
+
121
+ extension BinaryFloatingPoint where Self: CustomStringConvertible {
122
+ /// `1234.5678` → `1,234.568`; whole numbers lose the `.0`.
123
+ public func formatted() -> String {
124
+ guard isFinite else { return isNaN ? "NaN" : (self < 0 ? "-∞" : "∞") }
125
+ // Round to 3 fraction digits in integer arithmetic to avoid exponents.
126
+ let scaled = (Double(self) * 1000).rounded()
127
+ let negative = scaled < 0
128
+ let magnitude = Int64(min(abs(scaled), Double(Int64.max / 2)))
129
+ let integer = magnitude / 1000
130
+ let fraction = magnitude % 1000
131
+ let text = "\(negative && magnitude != 0 ? "-" : "")\(integer)." + _threeDigits(fraction)
132
+ return _formatDecimalString(text)
133
+ }
134
+ }
135
+
136
+ /// Three-digit zero-padded fraction.
137
+ private func _threeDigits(_ value: Int64) -> String {
138
+ let raw = String(value)
139
+ return String(repeating: "0", count: max(0, 3 - raw.count)) + raw
140
+ }
141
+
142
+ extension Decimal {
143
+ /// `Decimal(string: "3.75")! * 4` → `15`; `Decimal(string: "1234.5")!` → `1,234.5`.
144
+ public func formatted() -> String { _formatDecimalString(description) }
145
+ }
146
+
147
+ // MARK: - Strings
148
+
149
+ extension StringProtocol {
150
+ public func localizedCaseInsensitiveContains<T: StringProtocol>(_ other: T) -> Bool {
151
+ lowercased().contains(other.lowercased())
152
+ }
153
+
154
+ public func localizedStandardContains<T: StringProtocol>(_ other: T) -> Bool {
155
+ localizedCaseInsensitiveContains(other)
156
+ }
157
+
158
+ public func localizedCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
159
+ let a = String(self), b = String(other)
160
+ return a == b ? .orderedSame : (a < b ? .orderedAscending : .orderedDescending)
161
+ }
162
+
163
+ public func localizedCaseInsensitiveCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
164
+ lowercased().localizedCompare(other.lowercased())
165
+ }
166
+
167
+ public func localizedStandardCompare<T: StringProtocol>(_ other: T) -> ComparisonResult {
168
+ localizedCaseInsensitiveCompare(other)
169
+ }
170
+ }
171
+ #endif