Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions docs/specs/components/Item/Item.swift.spec.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/native/apple/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ apple/
│ │ ├── Localization/
│ │ ├── Icon/ # Icon
│ │ ├── Avatar/ # Avatar
│ │ ├── Item/ # Item
│ │ ├── Text/ # Text
│ │ ├── Heading/ # Heading
│ │ ├── Button/ # Button
Expand Down
11 changes: 2 additions & 9 deletions packages/native/apple/Sources/Astryx/Avatar/Avatar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,7 @@ public struct Avatar<Status: View>: View {
/// the build with its value, and an unnamed link or button is flagged.
private func checkInDevelopment() {
#if DEBUG
if Avatar.isUnusableHref(props.href) {
assertionFailure(
"Avatar href \"\(props.href ?? "")\" is not an absolute URL an app can open; it draws without a link.")
}
astryxCheckHref(props.href, in: "Avatar")
// The avatar's own name: a status label alone does not name a person.
let ownName = Avatar.meaningful(props.alt) ?? Avatar.meaningful(props.name)
if Avatar.needsNameWarning(tap: tap, isNamed: ownName != nil) {
Expand Down Expand Up @@ -108,11 +105,7 @@ public struct Avatar<Status: View>: View {
}

/// A non-empty `href` that is not an absolute URL, such as a web path.
nonisolated static func isUnusableHref(_ href: String?) -> Bool {
guard let href, !href.isEmpty else { return false }
guard let url = URL(string: href) else { return true }
return url.scheme == nil
}
nonisolated static func isUnusableHref(_ href: String?) -> Bool { astryxIsUnusableHref(href) }

/// The picture as a link with `href`, a button with only `onClick`, or a
/// static image (Swift component API, A4). All three draw the same.
Expand Down
48 changes: 48 additions & 0 deletions packages/native/apple/Sources/Astryx/Base/SafeDestination.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

import Foundation

/// An `href` as a link reads it: control characters removed and surrounding
/// whitespace trimmed, as core's shared destination rule does.
func astryxNormalizedHref(_ href: String) -> String {
String(String.UnicodeScalarView(
href.unicodeScalars.filter { $0.value > 0x1F && $0.value != 0x7F }
)).trimmingCharacters(in: .whitespacesAndNewlines)
}

/// A destination a link may open, normalized as core's shared rule does, or
/// nil for a script URL or an HTML document in a `data:` URL, which core's
/// navigation destinations refuse (`family:navigation-destinations` FR4).
/// The scheme is compared in lowercase without the spaces after its colon,
/// which a data URL's media type ignores (`data: text/html` is HTML).
func astryxSafeDestination(_ href: String) -> String? {
let normalized = astryxNormalizedHref(href)
var comparable = normalized.lowercased()
if let colon = comparable.firstIndex(of: ":"),
comparable[..<colon].range(of: "^[a-z][a-z0-9+.-]*$", options: .regularExpression) != nil {
let rest = comparable[comparable.index(after: colon)...].drop { $0 == " " }
comparable = String(comparable[...colon]) + rest
}
for blocked in ["javascript:", "vbscript:", "data:text/html"] where comparable.hasPrefix(blocked) {
return nil
}
return normalized
}

/// A non-empty `href` that is not an absolute URL, such as a web path. Pass
/// the `href` as the view's link reads it, so the check and the link agree.
func astryxIsUnusableHref(_ href: String?) -> Bool {
guard let href, !href.isEmpty else { return false }
guard let url = URL(string: href) else { return true }
return url.scheme == nil
}

/// Stops a development build on an `href` an app cannot open, naming the
/// component and the value; a release build draws the view without a link.
func astryxCheckHref(_ href: String?, in component: String) {
#if DEBUG
if astryxIsUnusableHref(href) {
assertionFailure("\(component) href \"\(href ?? "")\" is not an absolute URL an app can open; it draws without a link.")
}
#endif
}
24 changes: 3 additions & 21 deletions packages/native/apple/Sources/Astryx/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,27 +82,9 @@ public struct Button: View {
/// disabled button, as core draws a disabled `<button>`.
var link: URL? { isDisabled ? nil : destination }

/// A destination a link may open, normalized as core's shared rule does, or
/// nil for a script URL or an HTML document in a `data:` URL, which core's
/// navigation destinations refuse (`family:navigation-destinations` FR4).
/// Control characters are removed and whitespace trimmed; the scheme is
/// compared in lowercase without the spaces after its colon, which a data
/// URL's media type ignores (`data: text/html` is HTML).
nonisolated static func safeDestination(_ href: String) -> String? {
let normalized = String(String.UnicodeScalarView(
href.unicodeScalars.filter { $0.value > 0x1F && $0.value != 0x7F }
)).trimmingCharacters(in: .whitespacesAndNewlines)
var comparable = normalized.lowercased()
if let colon = comparable.firstIndex(of: ":"),
comparable[..<colon].range(of: "^[a-z][a-z0-9+.-]*$", options: .regularExpression) != nil {
let rest = comparable[comparable.index(after: colon)...].drop { $0 == " " }
comparable = String(comparable[...colon]) + rest
}
for blocked in ["javascript:", "vbscript:", "data:text/html"] where comparable.hasPrefix(blocked) {
return nil
}
return normalized
}
/// A destination a link may open, or nil when core's navigation
/// destinations refuse it (`astryxSafeDestination`).
nonisolated static func safeDestination(_ href: String) -> String? { astryxSafeDestination(href) }

public var body: some View {
control
Expand Down
Loading
Loading