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
34 changes: 34 additions & 0 deletions .github/workflows/swiftformat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: swiftformat

on:
push:
branches: ["**"]
paths:
- "**/*.swift"
- ".github/workflows/swiftformat.yml"
- ".swiftformat"
- "Package.resolved"
- "bin/format"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
format:
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- uses: actions/checkout@v7

- run: bin/format

- uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: format
file_pattern: '*.swift'
25 changes: 25 additions & 0 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: swiftlint

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-24.04
timeout-minutes: 15

steps:
- uses: actions/checkout@v7

- run: bin/lint

68 changes: 68 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: tests

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
macos:
name: Test macOS
# The checked-in macOS snapshots were recorded on macOS 27.0.1.
runs-on: xcode-27
timeout-minutes: 30
env:
DEVELOPER_DIR: /Applications/Xcode_27.0.app/Contents/Developer

steps:
- uses: actions/checkout@v7

- name: Run tests
env:
SNAPSHOT_ARTIFACTS: ${{ runner.temp }}
run: swift test

- name: Upload failed snapshots
if: failure()
uses: actions/upload-artifact@v7
with:
name: failed-snapshots-macos
path: |
${{ runner.temp }}/BlockTextViewSnapshotTests
${{ runner.temp }}/DocumentationSnapshotTests

ios:
name: Test iOS
runs-on: macos-26
timeout-minutes: 30
env:
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer

steps:
- uses: actions/checkout@v7

- name: Run tests
# Snapshot cases use SwiftUI view modifiers introduced in iOS 16.
run: |
set -o pipefail
xcodebuild -scheme SimpleHTML \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' \
-resultBundlePath .build/TestResults-iOS.xcresult \
IPHONEOS_DEPLOYMENT_TARGET=16.0 \
test | xcbeautify --renderer github-actions

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v7
with:
name: test-results-ios
path: .build/TestResults-iOS.xcresult

127 changes: 127 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
--language-mode 6.2
--acronyms ID,URL,UUID
--allman false
--allow-partial-wrapping true
--anonymous-for-each convert
--asset-literals visual-width
--async-capturing
--before-marks
--binary-grouping 4,8
--blank-line-after-switch-case multiline-only
--call-site-paren default
--category-mark "MARK: %c"
--class-threshold 0
--closing-paren balanced
--closure-void remove
--complex-attributes preserve
--computed-var-attributes preserve
--conditional-assignment after-property
--conflict-markers reject
--date-format system
--decimal-grouping 3,6
--default-test-suite-attributes
--doc-comments before-declarations
--else-position same-line
--empty-braces no-space
--enum-namespaces always
--enum-threshold 0
--equatable-macro none
--exponent-case lowercase
--exponent-grouping disabled
--extension-acl on-extension
--extension-mark "MARK: - %t + %c"
--extension-threshold 0
--file-macro "#file"
--fraction-grouping disabled
--fragment false
--func-attributes preserve
--generic-types
--group-blank-lines true
--grouped-extension "MARK: %c"
--guard-else auto
--header strip
--hex-grouping 4,8
--hex-literal-case uppercase
--ifdef indent
--import-grouping testable-last
--indent tab
--indent-case false
--indent-strings false
--inferred-types always
--init-coder-nil false
--lifecycle
--line-after-marks true
--line-between-guards false
--linebreaks lf
--mark-categories true
--mark-class-threshold 0
--mark-enum-threshold 0
--mark-extension-threshold 0
--mark-extensions always
--mark-struct-threshold 0
--mark-types always
--markdown-files ignore
--max-width none
--modifier-order
--never-trailing
--nil-init remove
--no-space-operators
--no-wrap-operators
--non-complex-attributes
--octal-grouping 4,8
--operator-func spaced
--organization-mode visibility
--organize-types actor,class,enum,struct
--pattern-let hoist
--preserve-acronyms
--preserve-decls
--preserved-property-types Package
--property-types infer-locals-only
--ranges spaced
--redundant-async tests-only
--redundant-throws tests-only
--self remove
--self-required
--semicolons inline-only
--short-optionals except-properties
--single-line-for-each ignore
--smart-tabs enabled
--some-any true
--sort-swiftui-properties none
--sorted-patterns
--stored-var-attributes preserve
--strip-unused-args always
--struct-threshold 0
--swift-version 6.2
--tab-width unspecified
--throw-capturing
--timezone system
--trailing-closures
--trailing-commas always
--trim-whitespace always
--type-attributes preserve
--type-blank-lines remove
--type-body-marks preserve
--type-delimiter space-after
--type-mark "MARK: - %t"
--type-marks
--type-order
--url-macro none
--visibility-marks
--visibility-order
--void-type Void
--wrap-arguments preserve
--wrap-collections preserve
--wrap-conditions preserve
--wrap-effects preserve
--wrap-enum-cases always
--wrap-parameters default
--wrap-return-type preserve
--wrap-string-interpolation default
--wrap-ternary default
--wrap-type-aliases preserve
--xcode-indentation disabled
--xctest-symbols
--yoda-swap always
--disable andOperator,blankLinesBetweenChainedFunctions,conditionalAssignment,noForceUnwrapInTests,redundantRawValues,redundantSelf,swiftTestingTestCaseNames,unusedArguments
--enable isEmpty
25 changes: 25 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
included:
- Package.swift
- Sources

only_rules:
- custom_rules
- force_cast
- force_try
- force_unwrapping

force_cast: error
force_try: error
force_unwrapping: error

custom_rules:
unchecked_sendable:
name: "Unchecked Sendable"
regex: "@unchecked"
message: "Do not bypass Sendable checking with @unchecked."
severity: error
unsafe_nonisolated:
name: "Unsafe nonisolated"
regex: "nonisolated\\s*\\(\\s*unsafe\\s*\\)"
message: "Do not bypass actor isolation with nonisolated(unsafe)."
severity: error
13 changes: 10 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Use Xcode with a Swift 6.2 or newer toolchain. Open `Package.swift` in Xcode or

Keep parsing, attributed-string construction, and view rendering separate. Changes to supported HTML should include tests of the generated attributes; layout changes should be checked against HTML and Markdown inputs. Use Swift Testing for new tests. Do not use `.serialized` to work around flaky tests.

Run `bin/format` and `bin/lint` before committing. Both use package plugins with pinned tool versions, so no separate formatter or linter installation is needed. SwiftFormat covers Swift sources and tests; SwiftLint checks the package manifest and production sources for forced casts, forced tries, forced unwraps, and unsafe concurrency overrides.

GitHub Actions runs the full macOS test suite on the `xcode-27` preview runner, matching the macOS 27 snapshot baseline, and the full iOS suite on an iPhone 17 simulator running iOS 26.5 with Xcode 26.6. Tests and lint run for pull requests and pushes to `main`. A separate workflow formats branch pushes and commits any Swift formatting changes using the repository's default token. All three workflows also support manual runs.

## Running tests

Run the macOS tests with:
Expand All @@ -22,16 +26,17 @@ Run the iOS tests with:

```sh
xcodebuild -scheme SimpleHTML \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' test
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' \
IPHONEOS_DEPLOYMENT_TARGET=16.0 test
```

The test suite includes parsing and whitespace tests, block-layout tests, and image snapshots. Snapshot rendering depends on OS fonts, so use the baseline platform versions when comparing images.
The test suite includes parsing and whitespace tests, block-layout tests, and image snapshots. The iOS test build targets iOS 16 because the snapshot cases exercise SwiftUI's view-level bold and italic modifiers. The library continues to support iOS 15. Snapshot rendering depends on OS fonts, so use the baseline platform versions when comparing images.

## Snapshot tests

`BlockTextViewSnapshotTests` records separate macOS and iOS images for both HTML and Markdown. One light/dark pair covers appearance; the remaining cases run in light mode. Cases cover small, large, XXXL, and accessibility Dynamic Type sizes; font, bold/italic, monospaced text, foreground color, tint, line spacing, and multiline alignment; and a narrow layout. Fixtures include wrapped text, links, three levels of lists, continuation paragraphs, and repeated content.

The iOS references were recorded on an iPhone 17 simulator running iOS 26.5 at a fixed 2× display scale. The macOS references were recorded on macOS 27.0.1. Dynamic Type sizes affect the iOS references; macOS currently keeps the same text size.
The iOS references were recorded on an iPhone 17 simulator running iOS 26.5 at a fixed 2× display scale. The macOS references were recorded on macOS 27.0.1, also at 2×. The macOS helper creates a bitmap at that scale so tests do not depend on whether the machine has a Retina display. Dynamic Type sizes affect the iOS references; macOS currently keeps the same text size.

To intentionally update the baselines:

Expand All @@ -48,6 +53,8 @@ Do not regenerate snapshots just to make an unexplained failure pass. Investigat

The suite renders content-sized canvases rendered directly by SwiftUI at 4× pixel density with a 16-point margin, a white background, a thin gray border, and fixed light appearance and locale. References were recorded on macOS 27.0.1. The suite's PNGs in `Sources/SimpleHTML/SimpleHTML.docc/Resources/` are both the snapshot baselines and the published assets; README links to the same files.

Documentation images use a perceptual color precision of 0.99 while still requiring every pixel to match. Local and hosted rendering can differ by one value in a few 8-bit color channels during conversion to sRGB. Behavior snapshots retain exact pixel comparisons.

To regenerate them, temporarily set this suite's trait to `.snapshots(record: .all)`, then run:

```sh
Expand Down
20 changes: 19 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.0"),
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins.git", exact: "0.65.0"),
.package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.62.1"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
6 changes: 4 additions & 2 deletions Sources/SimpleHTML/BlockTextLine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ extension PresentationIntent {

var listItem: IntentType? {
components.first {
if case .listItem = $0.kind { return true }
if case .listItem = $0.kind {
return true
}
return false
}
}
Expand Down Expand Up @@ -71,7 +73,7 @@ struct BlockTextLine: View {
// size based on font from underlying text
.frame(
width: proxy.size.height / 3,
height: proxy.size.height / 3
height: proxy.size.height / 3,
)
// center in geometry reader
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
Expand Down
Loading
Loading