Skip to content

refactor: build CSS and HTML by interpolation instead of String(format:) - #86

Merged
NuPlay merged 1 commit into
mainfrom
refactor/interpolated-css-builders
Aug 29, 2026
Merged

refactor: build CSS and HTML by interpolation instead of String(format:)#86
NuPlay merged 1 commit into
mainfrom
refactor/interpolated-css-builders

Conversation

@NuPlay

@NuPlay NuPlay commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Stacked on #85. Review the last commit only; base retargets to main automatically once that merges.

Problem

The CSS and HTML templates are printf format strings rendered with String(format:), which treats % as the start of a conversion. CSS is full of literal percent signs, so every one of them has to be written %% - and forgetting one is silent.

That single choice has now produced three separate defects:

defect cause
images not constrained to the web view width (#76) imageCSS missing its escapes, so max-width: 100% shipped as max-width: 100 - an invalid length the browser drops. Reported December 2025
iframeCSS reading 32 bits of a 64-bit Int %d instead of %ld
a test needed just to pin down argument order htmlTemplate took seven positional arguments and nothing enforced their order

The regression test added in #81 exists only to police a footgun the code creates for itself.

Changes

Interpolated builders replace the format strings:

// before
public static let imageCSS = "img{... max-width: 100%%; ... border-radius: %@px;}"
String(format: RichTextConstants.imageCSS, "\(imageRadius)")

// after
public static func imageCSS(radius: CGFloat) -> String {
    "img{height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: \(radius)px;}"
}

All three classes disappear at once: percent signs are literal, the compiler checks the types, and arguments are named. The container id and the two script message handler names are constants inside htmlDocument(css:body:), so five of those seven positional arguments no longer exist.

The seven format strings stay as deprecated public API and still work for anyone using them directly.

Verification

Each builder was rendered and diffed against the format string it replaces:

imageCSS identical
textCSS identical
iframeCSS identical
linkCSS identical
cssTemplate identical
mediaCSSTemplate identical
htmlTemplate identical (3376 vs 3376 chars)

All seven produce byte-identical output. The generated document was additionally re-checked in a browser for JS syntax, and the <details> height resync from #78 was re-run against it: 91px collapsed, 165px open, matching the measured element height.

The format-string scanner test is removed along with its subject - it policed a footgun that no longer has a live call site. Assertions on the generated output are kept.

Copilot AI lite review requested due to automatic review settings August 29, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@NuPlay NuPlay added the bug Something isn't working label Aug 29, 2026
@NuPlay NuPlay self-assigned this Aug 29, 2026
@NuPlay
NuPlay changed the base branch from chore/remove-dead-css-and-noop-api to main August 29, 2026 09:26
The templates were printf format strings rendered with `String(format:)`, which
treats `%` as the start of a conversion. CSS is full of literal percent signs, so
every one of them had to be written `%%`, and forgetting one was silent. That
single choice produced three separate defects:

- `imageCSS` was missing its escapes, so `max-width: 100%` shipped as
  `max-width: 100` - an invalid length the browser drops - and images were not
  constrained to the web view width. Reported in December 2025, fixed in #76.
- `iframeCSS` used `%d` for a Swift `Int`, reading 32 bits of a 64-bit value.
- `htmlTemplate` took seven positional arguments whose order nothing enforced,
  which needed a dedicated test to pin down.

Interpolation removes all three classes at once: percent signs are literal, the
compiler checks the types, and arguments are named. The container id and the two
script message handler names are constants inside `htmlDocument(css:body:)`, so
five of those seven positional arguments no longer exist at all.

The seven format strings stay as deprecated public API and still work for anyone
using them directly. Output is unchanged: each builder was diffed against the
format string it replaces and all seven render byte-identical results, and the
generated document was re-checked in a browser for JS syntax and for the
`<details>` height resync.

The regression test that scanned the format strings for unescaped percent signs
is removed with them; it existed only to police a footgun that no longer has a
live call site. Assertions on the generated output are kept.
@NuPlay
NuPlay force-pushed the refactor/interpolated-css-builders branch from b42e345 to 3227a69 Compare August 29, 2026 09:27
@NuPlay
NuPlay merged commit facb4c6 into main Aug 29, 2026
1 check passed
@NuPlay
NuPlay deleted the refactor/interpolated-css-builders branch August 29, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants