SwiftSoup Documentation

SwiftSoup: Pure Swift HTML Parser, with best of DOM, CSS, and jquery (Supports Linux, iOS, Mac, tvOS, watchOS)

5,127 stars Swift Swift 1 file Β· ~22,237 tokens #dom#extract#html#html-document#parse#selector#swift#swiftsoup
RAW Doc

File: README.md





SwiftSoup is a pure Swift library designed for seamless HTML parsing and manipulation across multiple platforms, including macOS, iOS, tvOS, watchOS, and Linux. It offers an intuitive API that leverages the best aspects of DOM traversal, CSS selectors, and jQuery-like methods for effortless data extraction and transformation. Built to conform to the WHATWG HTML5 specification, SwiftSoup ensures that parsed HTML is structured just like modern browsers do.

Key Features:

  • Parse and scrape HTML from a URL, file, or string.
  • Find and extract data using DOM traversal or CSS selectors.
  • Modify HTML elements, attributes, and text dynamically.
  • Sanitize user-submitted content using a safe whitelist to prevent XSS attacks.
  • Generate clean and well-structured HTML output.

SwiftSoup is designed to handle all types of HTMLβ€”whether perfectly structured or messy tag soupβ€”ensuring a logical and reliable parse tree in every scenario.


Swift

Swift 5 >=2.0.0

Swift 4.2 1.7.4

Installation

Cocoapods

SwiftSoup is available through CocoaPods. To install
it, simply add the following line to your Podfile:

ruby
pod 'SwiftSoup'

Carthage

SwiftSoup is also available through Carthage. To install
it, simply add the following line to your Cartfile:

ruby
github "scinfu/SwiftSoup"

Swift Package Manager

SwiftSoup is also available through Swift Package Manager.
To install it, simply add the dependency to your Package.Swift file:

swift
...
dependencies: [
    .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
],
targets: [
    .target( name: "YourTarget", dependencies: ["SwiftSoup"]),
]
...

Usage Examples

Parse an HTML Document

swift
import SwiftSoup

let html = """
<html><head><title>Example</title></head>
<body><p>Hello, SwiftSoup!</p></body></html>
"""

let document: Document = try SwiftSoup.parse(html)
print(try document.title()) // Output: Example

Automatic Format Detection

SwiftSoup.parse(...) automatically detects XML input by looking for an <?xml declaration at the start of the
content. When detected, the XML parser is used; otherwise the HTML parser is applied. This means feeds, OPML, and
other XML documents with a standard XML declaration "just work":

swift
import SwiftSoup

let xml = """
<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
  <body>
    <link>I'm link</link>
    <img>I'm img</img>
  </body>
</opml>
"""

let document = try SwiftSoup.parse(xml) // auto-detects XML
print(try document.select("link").first()?.text()) // Output: I'm link
print(try document.select("body > img").first()?.text()) // Output: I'm img

Explicit Parse Modes

Use parseXML(...) or parseHTML(...) when you want to force a specific parser regardless of the content:

swift
// Force XML parsing (no HTML5 tag normalization)
let xmlDoc = try SwiftSoup.parseXML(xmlString)

// Force HTML parsing (always applies HTML5 rules, even if input has <?xml>)
let htmlDoc = try SwiftSoup.parseHTML(htmlString)

// Explicit parser argument (unchanged from before)
let doc = try SwiftSoup.parse(input, baseUri, Parser.xmlParser())

Parse HTML from a URL

If Foundation cannot determine a page's text encoding, avoid String(contentsOf:) and parse the raw response bytes
instead:

swift
import SwiftSoup

let url = URL(string: "https://example.com")!
let document = try SwiftSoup.parse(url)
print(try document.title())

Profiling

SwiftSoup includes a lightweight profiler (gated by a compile-time flag) and a small CLI harness for parsing benchmarks.

CLI parse benchmark

This uses the SwiftSoupProfile executable target to parse a fixture corpus and report wall time:

bash
swift run -c release SwiftSoupProfile --fixtures /path/to/fixtures

Add --text to include Document.text() in the workload.

In-code profiler

The Profiler type is only compiled when the PROFILE flag is set. Build with:

bash
swift run -c release -Xswiftc -DPROFILE SwiftSoupProfile --fixtures /path/to/fixtures

Then the CLI will print the profiler summary at the end of the run.


Select Elements with CSS Query

swift
let html = """
<html><body>
<p class='message'>SwiftSoup is powerful!</p>
<p class='message'>Parsing HTML in Swift</p>
</body></html>
"""

let document = try SwiftSoup.parse(html)
let messages = try document.select("p.message")

for message in messages {
    print(try message.text())
}
// Output:
// SwiftSoup is powerful!
// Parsing HTML in Swift

Extract Text and Attributes

swift
let html = "<a href='https://example.com'>Visit the site</a>"
let document = try SwiftSoup.parse(html)
let link = try document.select("a").first()

if let link = link {
    print(try link.text()) // Output: Visit the site
    print(try link.attr("href")) // Output: https://example.com
}

Modify the DOM

swift
var document = try SwiftSoup.parse("<div id='content'></div>")
let div = try document.select("#content").first()
try div?.append("<p>New content added!</p>")
print(try document.html())
// Output:
// <html><head></head><body><div id="content"><p>New content added!</p></div></body></html>

Clean HTML for Security (Whitelist)

swift
let dirtyHtml = "<script>alert('Hacked!')</script><b>Important text</b>"
let cleanHtml = try SwiftSoup.clean(dirtyHtml, Whitelist.basic())
print(cleanHtml) // Output: <b>Important text</b>
swift
let dirtyHtml = #"<p style="color:red; position:absolute">Styled text</p>"#
let whitelist = try Whitelist()
    .addTags("p")
    .addAttributes("p", "style")
    .addCSSProperties("p", "color")
let cleanHtml = try SwiftSoup.clean(dirtyHtml, whitelist)
print(cleanHtml) // Output: <p style="color:red">Styled text</p>

Use CSS selectors to find elements

(from jsoup)

Selector overview

  • tagname: find elements by tag, e.g. div
  • #id: find elements by ID, e.g. #logo
  • .class: find elements by class name, e.g. .masthead
  • [attribute]: elements with attribute, e.g. [href]
  • [^attrPrefix]: elements with an attribute name prefix, e.g. [^data-] finds elements with HTML5 dataset attributes
  • [attr=value]: elements with attribute value, e.g. [width=500] (also quotable, like [data-name='launch sequence'])
  • [attr^=value], [attr$=value], [attr*=value]: elements with attributes that start with, end with, or contain the value, e.g. [href*=/path/]
  • [attr~=regex]: elements with attribute values that match the regular expression; e.g. img[src~=(?i)\.(png|jpe?g)]
  • *: all elements, e.g. *
  • [*] selects elements that have any attribute. e.g. p[*] finds paragraphs with at least one attribute, and p:not([*]) finds those with no attributes.
  • ns|tag: find elements by tag in a namespace prefix, e.g. dc|name finds <dc:name> elements
  • *|tag: find elements by tag in any namespace prefix, e.g. *|name finds <dc:name> and <name> elements
  • :empty: selects elements that have no children (ignoring zero-length text nodes, comments, etc.; whitespace-only text still counts as content); e.g. li:empty

Selector combinations

  • el#id: elements with ID, e.g. div#logo
  • el.class: elements with class, e.g. div.masthead
  • el[attr]: elements with attribute, e.g. a[href]
  • Any combination, e.g. a[href].highlight
  • ancestor child: child elements that descend from ancestor, e.g. .body p finds p elements anywhere under a block with class "body"
  • parent > child: child elements that descend directly from parent, e.g. div.content > p finds p elements; and body > * finds the direct children of the body tag
  • siblingA + siblingB: finds sibling B element immediately preceded by sibling A, e.g. div.head + div
  • siblingA ~ siblingX: finds sibling X element preceded by sibling A, e.g. h1 ~ p
  • el, el, el: group multiple selectors, find unique elements that match any of the selectors; e.g. div.masthead, div.logo

Pseudo selectors

  • :has(selector): find elements that contain elements matching the selector; e.g. div:has(p)
  • :is(selector): find elements that match any of the selectors in the selector list; e.g. :is(h1, h2, h3, h4, h5, h6) finds any heading element
  • :not(selector): find elements that do not match the selector; e.g. div:not(.logo)
  • :lt(n): find elements whose sibling index (i.e. its position in the DOM tree relative to its parent) is less than n; e.g. td:lt(3)
  • :gt(n): find elements whose sibling index is greater than n; e.g. div p:gt(2)
  • :eq(n): find elements whose sibling index is equal to n; e.g. form input:eq(1)
  • Note that the above indexed pseudo-selectors are 0-based, that is, the first element is at index 0, the second at 1, etc

Text content pseudo selectors

  • :contains(text): find elements that contain (directly or via children) the given normalized text. The search is case-insensitive; e.g. div:contains(jsoup)
  • :containsOwn(text): find elements whose own text directly contains the given text. e.g. p:containsOwn(jsoup)
  • :containsData(text): selects elements that contain the specified data (e.g. within <script>, <style>, or comments); e.g. script:containsData(jsoup)
  • :containsWholeText(text): selects elements that contain the exact, non-normalized whole text (case sensitive, preserving whitespace/newlines); e.g. p:containsWholeText(jsoup The Java HTML Parser)
  • :containsWholeOwnText(text): selects elements whose own text exactly matches the given non-normalized text (case sensitive); e.g. p:containsWholeOwnText(jsoup The Java HTML Parser)
  • :matches(regex): find elements whose text matches the specified regular expression; e.g. div:matches((?i)login)
  • :matchesOwn(regex): find elements whose own text matches the specified regular expression
  • :matchesWholeText(regex): selects elements whose entire, non-normalized text matches the specified regex; e.g. div:matchesWholeText(\d{3}-\d{2}-\d{4})
  • :matchesWholeOwnText(regex): selects elements whose own non-normalized text matches the regex; e.g. span:matchesWholeOwnText(\w+)

Structural pseudo selectors

  • :root: selects the root element of the document (in HTML, the <html> element); e.g. :root
  • :nth-child(an+b): selects elements with an+b–1 preceding siblings; supports expressions like 2n+1 for odd elements; e.g. tr:nth-child(2n+1)
  • :nth-last-child(an+b): selects elements with an+b–1 following siblings; e.g. tr:nth-last-child(-n+2)
  • :nth-of-type(an+b): selects elements based on their position among siblings of the same type; e.g. img:nth-of-type(2n+1)
  • :nth-last-of-type(an+b): selects elements based on their position among siblings of the same type, counting from the end; e.g. img:nth-last-of-type(2n+1)
  • :first-child: selects elements that are the first child of their parent; e.g. div > p:first-child
  • :last-child: selects elements that are the last child of their parent; e.g. ol > li:last-child
  • :first-of-type: selects the first element of its type among its siblings; e.g. dl dt:first-of-type
  • :last-of-type: selects the last element of its type among its siblings; e.g. tr > td:last-of-type
  • :only-child: selects elements that are the only child of their parent; e.g. div:only-child
  • :only-of-type: selects elements that are the only element of their type among their siblings; e.g. span:only-of-type

Optimize repeated queries

SwiftSoup provides automatic caching of parsed CSS queries to speed up repeated queries, and also to speed up parsing related queries.

The cache is controlled through the static property QueryParser.cache. By default, it is initialized with a reasonable size limit.
You may replace the cache at any time; however, assigning a new cache instance will discard all previously cached values.

swift
// Remove any cache limits.
QueryParser.cache = QueryParser.DefaultCache(limit: .unlimited)
// Limit to 1000 items. See also documentation for ``QueryParserCache/set(_:_:)``.
QueryParser.cache = QueryParser.DefaultCache(limit: .count(1000))

An alternative is to parse the query upfront and passing an Evaluator instead of query string.
Since Evaluator instances are immutable they are safe to store in (static) properties or pass across isolation boundaries.

swift
let elements: Elements = …
let eval = try QueryParser.parse("div > p")
for element in elements {
    print(try element.select(eval).text())
}

Author

Nabil Chatbi, [email protected]

Current maintainer: Alex Ehlke, available for hire for SwiftSoup related work or other iOS projects: alex dot ehlke at gmail

Note

SwiftSoup was ported to Swift from Java Jsoup library.

License

SwiftSoup is available under the MIT license. See the LICENSE file for more info.


File: _config.yml

theme: jekyll-theme-cayman

author:
twitter: scinfu

gems:

  • jekyll-seo-tag

File: AGENTS.md

Benchmarking & profiling protocol (SwiftSoup)

This repo’s current performance harness is the BenchmarkProfileTest/testParseBenchmarkProfile test. It is driven by
environment variables and runs in a release test build.

Optional mise shortcuts (macOS/Linux)

The tasks use your existing Swift toolchain and do not install tools:

text
mise run test:release
mise run bench
SWIFTSOUP_BENCHMARK_ITERATIONS=100 mise run bench
SWIFTSOUP_BENCHMARK_SET=attribute-heavy mise run bench

bench defaults to base,large, two in-process warmups, and ten measured iterations.
Existing SWIFTSOUP_BENCHMARK_* environment overrides are preserved. Increase the
iteration count for measurements; these modest defaults are only a starting point.
The direct SwiftPM commands below remain supported without mise.

Compare the benchmark's elapsed-time line, not build or task wall time. The default
workload includes Data and String parsing plus selector/text operations; it is not a
parse-only measurement. Record swift --version and any local source changes with
results. Repeat runs with alternating baseline/candidate order, run an unchanged
A/A comparison to estimate noise, and validate behavior with regression tests before
interpreting an improvement. Keep profiling runs separate from timing runs.

A/B comparison rules

  • Always compare release builds on the same machine.
  • If an optimization has a flag: run OFF vs ON.
  • If no flag: compare HEAD to a baseline commit in a worktree.
  • Test each optimization in isolation before any combined run.
  • Record commit SHAs, flags, iterations, and the exact command lines used.

Common benchmark knobs (env vars)

  • SWIFTSOUP_BENCHMARK=1 (enable the benchmark test)
  • SWIFTSOUP_BENCHMARK_SET=... (comma-separated workload sets)
  • SWIFTSOUP_BENCHMARK_WARMUP=2
  • SWIFTSOUP_BENCHMARK_ITERATIONS=500
  • SWIFTSOUP_BENCHMARK_ITERATIONS_MULTIPLIER=1
  • SWIFTSOUP_BENCHMARK_REPEAT=200 (base set)
  • SWIFTSOUP_BENCHMARK_LARGE_REPEAT=60 (large set)
  • SWIFTSOUP_BENCHMARK_SELECTOR_REPEAT=1
  • SWIFTSOUP_BENCHMARK_SELECTOR_STRESS_REPEAT=1
  • SWIFTSOUP_BENCHMARK_ATTRIBUTE_SELECTOR_STRESS_REPEAT=1
  • SWIFTSOUP_BENCHMARK_SERIALIZER=source-patched|without-source-reuse|reuse-source-outside-body
  • SWIFTSOUP_BENCHMARK_DENSE_BODY_MUTATIONS=1

Regression suite (broad coverage)

Run in both baseline and current (exact command line):

text
SWIFTSOUP_BENCHMARK=1 \
SWIFTSOUP_BENCHMARK_SET=base,large \
SWIFTSOUP_BENCHMARK_REPEAT=200 \
SWIFTSOUP_BENCHMARK_LARGE_REPEAT=60 \
SWIFTSOUP_BENCHMARK_WARMUP=2 \
SWIFTSOUP_BENCHMARK_ITERATIONS=500 \
SWIFTSOUP_BENCHMARK_ITERATIONS_MULTIPLIER=1 \
SWIFTSOUP_BENCHMARK_SELECTOR_REPEAT=1 \
SWIFTSOUP_BENCHMARK_SELECTOR_STRESS_REPEAT=1 \
SWIFTSOUP_BENCHMARK_ATTRIBUTE_SELECTOR_STRESS_REPEAT=1 \
swift test -c release --filter BenchmarkProfileTest/testParseBenchmarkProfile

Targeted suite (optimization-specific)

Pick the workload set that stresses the change and run with the same flags:

text
SWIFTSOUP_BENCHMARK=1 \
SWIFTSOUP_BENCHMARK_SET=attribute-heavy,attribute-mega,attribute-storm,data-heavy,data-storm,tag-heavy,custom-tag,dense-text,querystring \
SWIFTSOUP_BENCHMARK_WARMUP=2 \
SWIFTSOUP_BENCHMARK_ITERATIONS=500 \
SWIFTSOUP_BENCHMARK_ITERATIONS_MULTIPLIER=1 \
SWIFTSOUP_BENCHMARK_SELECTOR_REPEAT=1 \
SWIFTSOUP_BENCHMARK_SELECTOR_STRESS_REPEAT=1 \
SWIFTSOUP_BENCHMARK_ATTRIBUTE_SELECTOR_STRESS_REPEAT=1 \
swift test -c release --filter BenchmarkProfileTest/testParseBenchmarkProfile

Serializer A/B

For Reader-style dense body mutation, run the same release benchmark once per serializer:

text
for serializer in source-patched without-source-reuse reuse-source-outside-body; do
  SWIFTSOUP_BENCHMARK=1 \
  SWIFTSOUP_BENCHMARK_SET=manabi-reader \
  SWIFTSOUP_BENCHMARK_MANABI_REPEAT=40 \
  SWIFTSOUP_BENCHMARK_WARMUP=2 \
  SWIFTSOUP_BENCHMARK_ITERATIONS=30 \
  SWIFTSOUP_BENCHMARK_ITERATIONS_MULTIPLIER=1 \
  SWIFTSOUP_BENCHMARK_SKIP_SELECTORS=1 \
  SWIFTSOUP_BENCHMARK_SKIP_TEXT=1 \
  SWIFTSOUP_BENCHMARK_DENSE_BODY_MUTATIONS=1 \
  SWIFTSOUP_BENCHMARK_SERIALIZER="$serializer" \
  swift test -c release --filter BenchmarkProfileTest/testParseBenchmarkProfile
done

Baseline worktree helper

  • git worktree add ../SwiftSoup-bench-base
  • Run the same commands in both trees; compare the β€œBenchmark elapsed: … ms over N iterations” line.
  • Use git rev-parse HEAD to log the commit for each side.

Profiling

  • Use Instruments (Time Profiler) on the SwiftSoupPackageTests.xctest process while running the same benchmark test
    filter and env vars.
  • Always profile release builds and keep the workload set identical to the benchmark run you are comparing against.

File: CHANGELOG.md

Change Log

All notable changes to this project will be documented in this file.

Unreleased

  • Parse each :has() selector-list branch independently and support leading adjacent/general sibling combinators, including sibling chains and descendants. Preserve escaped and nested commas, candidate scoping, and cached evaluator isolation; avoid stale subtree result snapshots when sibling attributes change outside the selection root.
  • Isolate mutable parser-built OR evaluator graphs in the default query cache so editing a parsed selector cannot change later queries. Preserve established AND error handling in indexed selection and honor same-module evaluator overrides instead of substituting built-in index semantics.
  • Check OrderedSet.isSubset(of:) without restarting single-pass inputs. Such sequences no longer lose earlier matches or depend on hash iteration order; successful checks stop once all required members are found. Collections retain their specialized membership checks (including Set and Range), and singleton receivers need only one check without temporary membership storage. Empty receivers do not consume the input. Temporary membership storage for multi-member, non-Collection inputs is bounded by receiver size.
  • Preserve stored representatives in OrderedSet.swapObject(_:with:), including canonically equivalent String bytes. Equal lookup arguments no longer replace or remain retained as members; swapping the same member is a no-op. Explicit equal-value replacement through append remains unchanged.
  • Decode paired text escapes correctly in TokenQueue.unescape and :contains / :containsOwn / :containsData. Runs of backslashes now quote one scalar at a time, including inside graphemes, rather than duplicating escapes. A lone trailing backslash is still dropped. Regex arguments and CSS hexadecimal identifier decoding remain separate and unchanged.
  • Preserve the exact consumed spelling of balanced expressions while splitting compound selectors. Missing numeric closing parentheses now fail consistently after combinators and within partial outer expressions, instead of being synthesized. Partial literal text/regex input is no longer changed by an invented closing delimiter. Public TokenQueue.chompBalanced partial-result behavior and existing nonnumeric EOF tolerance remain unchanged; this is not a new browser-equivalent EOF policy.
  • Parse complete numeric selectors with ASCII digits, CSS whitespace and checked Int conversion. Correct implicit negative nth coefficients and reject malformed or out-of-range input with a normal parse error rather than trapping. Match all supported Int coefficients/offsets without arithmetic overflow and preserve An+B semantics in evaluator serialization. This intentionally stops accepting trailing garbage and non-CSS whitespace; no broader CSS token/comment/escape or of S support is added.
  • Keep validated absent attribute reads deferred and reject empty case-insensitive value queries before changing storage. Retain the existing bounded 32-name prefilter and exact comparisons. Validate both UTF-8 split partitions without recreating the entire text.
  • Fix regex capture extraction to use Foundation UTF-16 ranges, preventing Unicode-offset crashes and preserving captures within combining, emoji, and CRLF graphemes. Matcher.group now returns nil for invalid group indices or without a current match; participating empty captures remain empty strings. Exhausted matchers stop advancing their cursor. Pattern compilation reuse and legacy option handling are unchanged.
  • Empty child-list insertions and clearing an already empty Element no longer invalidate source/query/text caches or advance mutation tokens. Indexed insertions still validate bounds before returning. Empty TextNode insertion is still a real structural mutation.
  • Make virtual abs: attribute presence case-insensitive, matching value lookup. Invalid virtual suffixes fall back to physical attribute presence. Fresh TextNode byte-based presence checks now initialize the same deferred storage as String checks, without marking content edited. These correct previously false results; URL resolution and text-splitting rules remain unchanged.
  • Define TextNode.splitText(_:) offsets as Swift Characters and splitText(utf8Offset:) offsets as exact Unicode scalar-aligned UTF-8 byte positions. End offsets produce empty tails. Invalid ranges, partial UTF-8 scalars, and malformed byte storage throw before mutation; byte splits no longer silently round to a grapheme boundary.
  • Keep text-dependent selector caches current after child appends, detach retained children during empty() / content replacement, and preserve original insertion gaps when moving existing siblings. Self replacement is a no-op; invalid offsets and cyclic insertions/replacements are rejected before detaching any inputs.
  • Resolve node URLs through the public base-URI byte getter, including absent base URIs, instead of force-unwrapping storage.
  • Preserve byte-array TextNode presence initialization and the complete earlier public-behavior regression suites while integrating the alternative canonical-name refinement. Ambiguous deferred attributes now materialize; their reads still preserve source reuse and mutation versions.
  • Reduce cold deferred-name validation work with a bounded mask prefilter and exact collision checks. Keep ambiguous batches and byte-distinct Unicode names on the same canonical materialization contract.
  • Make deferred attribute lookups and serialization agree with materialized storage. Preserve the existing first-position/last-value rule for exact duplicate names, first matching case-variant lookup, trimmed valid keys, and byte-name precedence. Ordinary unique attribute batches remain deferred; ambiguous batches materialize before reads rather than changing their answers later.
  • Use wrapping integer arithmetic in Attribute.hashCode() so hash mixing cannot trap on overflow.
  • Avoid repeated membership scans when exposing singly owned attribute lists or iterators. Mutation notifications still validate every owner by identity, and multi-owner cleanup remains unchanged.
  • Include comment contents in Element.data() and :containsData, as documented, in descendant document order. Traverse iteratively without intermediate subtree strings or materializing single-slice data nodes. Custom DataNode getter overrides remain respected. Queries previously ignoring comment contents may now match.
  • Observe direct Attribute.setKey / setValue edits through every live owning collection and node, including references shared by put / addAll. Removed or replaced references no longer invalidate former owners. Reads that materialize deferred text/data do not count as DOM mutations.
  • Clone mutable attribute objects independently, including implicit boolean attributes, while retaining immutable byte storage sharing. Cloned DOM mutations no longer change the original. Attribute keys that are empty after trimming are rejected before mutation.
  • Keep attribute indexes consistent with case-insensitive getters when case variants or direct renames introduce duplicate names: the first matching attribute wins and elements appear once.
  • Make deferred text and script-data attributes authoritative before public attribute access or edits. Subsequent reads/appends cannot overwrite edits or restore removed content. Direct non-element attribute edits invalidate text/selector caches and serialized source reuse.
  • Return each element once from class indexes even when class repeats a token or case variant. Preserve traversal order in both combined and class-only rebuilds.
  • Distinguish missing physical attributes from present empty values in equality, inequality, and regex predicates. Empty prefix/suffix/substring operands now match nothing, as required by CSS; existing SwiftSoup case/whitespace normalization is otherwise unchanged.
  • Keep attribute fast plans consistent with the parser by delegating non-ASCII attribute selectors to it. Preserve parsed virtual abs: predicates with quoted empty operands, and recognize padded abs: keys in the public value getter.
  • Honor case-preserving attribute updates in byte-slice predicates without dropping the lowercase-key fast path.
  • Preserve value and regex predicates when an attribute-presence index seeds a compound selector. Presence alone does not prove a prefix, suffix, substring, or regex match. Keep exact-index shortcuts and check single-predicate And evaluators consistently.
  • Resolve virtual abs: attribute presence through hasAttr, not the physical attribute-name index, and invalidate affected ancestor/subtree selector results when the base URI changes. Physical attribute indexes and text caches remain intact.
  • Route unescaped punctuation in bare ID selectors through the parser, matching compound-selector validation. Literal punctuation in an ID must be escaped (for example #x\] for ID x]) or supplied through getElementById.
  • Treat getElementById arguments as literal IDs, retaining leading/trailing whitespace. Callers that intentionally accepted padded input must now trim it themselves. Recognize form feed as a descendant combinator rather than an ID byte in the selector fast path.
  • Preserve escaped trailing spaces and non-ASCII identifier content while removing CSS query padding, including direct parser and multi-root selection paths.
  • Scan balanced and compound selectors at code-point boundaries; handle backslash parity and paired quote delimiters correctly. Unicode prepend characters no longer absorb closing parentheses or combinators. The public balanced scanner still supports multi-scalar delimiters.
  • Match class tokens, never an entire whitespace-separated class list. Use HTML's five ASCII whitespace separators consistently in class getters, predicates, and indexes; vertical tab (U+000B) remains part of a class token.
  • Preserve byte-distinct Unicode selector spellings in the parser, evaluator, fast-plan, and per-root result caches. Custom QueryParserCache implementations must also use code-point-exact key equality rather than Swift's canonical-equivalence equality.
  • Generate ID/class escapes by Unicode scalar, preserving combining marks (including immediately after #/.), emoji, leading digits, controls, and literal backslashes. Escape spaces as code points so query trimming does not remove significant trailing spaces. CSS represents U+0000 as U+FFFD, not as a literal NUL.
  • Preserve whitespace decoded from ID escapes during indexed selection. Anchor :has(> ...) against each candidate element and avoid the collect-once shortcut for root-dependent predicates.
  • Decode CSS hexadecimal escapes in ID and class selectors: one to six hex digits, one optional CSS whitespace terminator (including CRLF), and U+FFFD for zero, surrogate, or out-of-range code points. Preserve the complete escape when splitting compound selectors.
  • Compatibility: p#\61 now matches ID a, not ID 61. Clients that intended the old literal value should use p#61 or p#\36 1; an ID containing a literal backslash followed by 61 is selected by p#\\61. ID/class parsing also accepts non-ASCII identifier code points so combining marks following hex digits are retained. Existing non-hex escapes remain supported. This does not change tag/attribute escape parsing or the separate text/regex unescape behavior.

[2.3.2](https://github.com/scinfu/SwiftSoup/tree/2.3.2)

  • Renamed Selector Class to CssSelector

[1.7.4](https://github.com/scinfu/SwiftSoup/tree/1.7.4)

  • Removed Some warnings
  • Swift 4.2

[1.7.1](https://github.com/scinfu/SwiftSoup/tree/1.7.1)

  • Backward compatibility for Swift < 4.1

[1.7.0](https://github.com/scinfu/SwiftSoup/tree/1.7.0)

  • Removed StringBuilder from Element.cssSelector
  • Lint Code
  • Swift 4.1

[1.6.5](https://github.com/scinfu/SwiftSoup/tree/1.6.5)

  • Removed StringBuilder from Element.cssSelector
  • Lint Code

[1.6.4](https://github.com/scinfu/SwiftSoup/tree/1.6.4)

  • Add newer simulators to targeted devices to build with Carthage [tvOS]

[1.6.3](https://github.com/scinfu/SwiftSoup/tree/1.6.3)

  • Add newer tvOS simulators to targeted devices to build with Carthage.
  • Add newer watchOS simulators to targeted devices to build with Carthage.

File: .travis.yml

Travis CI

https://swift.org/download/#releases

whitelist (branches that should be built)

branches:
only:
- master
- develop

the matrix of builds should cover each combination of Swift version

and platform that is supported. The version of Swift used is specified

by .swift-version, unless SWIFT_SNAPSHOT is specified.

matrix:
include:
# Continue to test one permutation on Trusty (14.04)
- os: linux
dist: xenial
sudo: required
services: docker
env: DOCKER_IMAGE=swift:5.0.3-xenial SWIFT_SNAPSHOT=5.0.3 SWIFT_TEST_ARGS="--parallel"
- os: linux
dist: xenial
sudo: required
services: docker
env: DOCKER_IMAGE=swift:5.1.5-xenial SWIFT_SNAPSHOT=5.1.5 SWIFT_TEST_ARGS="--parallel"
- os: osx
osx_image: xcode10.2
sudo: required
env: SWIFT_SNAPSHOT=5.0.1 SWIFT_TEST_ARGS="--parallel"
- os: osx
osx_image: xcode11.3
sudo: required
env: SWIFT_SNAPSHOT=5.1.3 SWIFT_TEST_ARGS="--parallel"
- os: osx
osx_image: xcode13.3
sudo: required
env: SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT SWIFT_TEST_ARGS="--parallel"

before_install:

script:

  • ./Package-Builder/build-package.sh -projectDir $TRAVIS_BUILD_DIR

File: Benchmarks/entity-decoding-20260909.md

Use the existing dictionary for multipoint entity lookup

The ByteSlice fallback scanned every entry in the existing multipoint-entity dictionary, including misses before a long single-scalar lookup. It now uses the dictionary's subscript with the existing toArraySlice() conversion. This is one replacement expression, not another cache or lookup table. Array-backed names share slice storage; Data/pointer-backed names use a short-lived byte copy, which is included in the end-to-end Data and pointer-input measurements. Common packed-name lookup, case sensitivity, extended/base admission and entity tables are unchanged.

Six new regression methods independently parse all 2,125 names from the declarative entity table, check extended and base admission, unknown/case/truncated names, nonzero array and Data indices, pointer storage, COW snapshots, repeated lookup, semicolon rules, and public String/Data/borrowed-buffer parsing. The dictionary query slice is not retained after lookup.

Independent confirmation

Target 400 ms timed work per process, fixture size 64. unescape-multi uses two-scalar names such as NotEqualTilde; unescape-long uses long one-scalar names such as CounterClockwiseContourIntegral; unescape-unknown exercises misses. Parse cases contain 64 paragraphs and include parsing plus text() extraction. They are deliberately entity-heavy, not claims about normal web-page prevalence. Both Data and caller-borrowed-pointer input include the temporary conversion cost.

Workload Reference ms Candidate ms Less time 95% interval
unescape-multi 0.095792 0.056377 +41.15% +36.40 to +45.54%
unescape-long 0.124962 0.066814 +46.53% +45.25 to +47.78%
unescape-unknown 1.065441 0.987711 +7.30% +4.84 to +9.69%
unescape-common 0.026962 0.026970 -0.03% -1.88 to +1.79%
parse-multi-data 0.348443 0.311125 +10.71% +8.12 to +13.22%
parse-long-buffer 0.373229 0.312480 +16.28% +14.87 to +17.66%
parse-normal-string 0.268462 0.269777 -0.49% -3.65 to +2.57%

Scope and exact reference

Independent change on fork master cb57c9e160da093c7dd87eb91d335b6891d0efb7. The numerical study instead compares the already-optimized PR #3 at c9c37c80eb571aabba59a1f3ef31af9b072af4a8 with that exact production tree plus only this change. The reference Sources Git tree is 98e22c134afa5144fe64565f448260239545f90f. Both revisions have identical original Entities.swift, blob 9a8e292e58323cedb86e834cc24c8bf14af89739.

PRs #1–#7 were audited by scope and changed-file lists; none changes Entities. A later recheck included the new regex PR #8, which changes Pattern, not Entities. No existing PR head, master, dependency pin, source-range setting, or parser default was changed. This does not incorporate or supersede the selector/ownership correctness stack.

The two entity PRs change different helpers and are independently reviewable against master. Their two Tools files are intentionally byte-identical, so they share one portable harness after merging either order.

Measurement protocol

Swift 6.2.1, Linux x86_64, CPU 0 affinity, -O -g -whole-module-optimization -swift-version 6 shipping libraries without -enable-testing. One identical optimized public client dynamically loads each library. Fixture setup, compilation, full-output verification, sanitizer tests and profiling are outside timed intervals. Three in-process warmups precede each timed loop. Hashing is randomized normally; it is not made deterministic for timing.

Confirmation uses 12 balanced ABBA/BAAB blocks: 24 fresh processes per revision per workload. Each block averages the two log times per revision; the table reports geometric-mean milliseconds per operation and a paired-block Student t 95% interval for the log time ratio, transformed to percent less time. Positive means faster. These are exploratory intervals without multiple-comparison correction, not predictions for another machine or production traffic. Wall and process CPU values, calibrations, all raw process records and exact command/library/client/source hashes are retained. No sample/outlier is deleted.

The initial screen (six blocks, 12 processes per revision per workload) is retained separately rather than pooled selectively into confirmation. Targeted and ASCII A/A controls use eight blocks; Reader A/A uses six. All wall-clock A/A intervals include zero; the multipoint-entity A/A interval is especially wide because randomized dictionary ordering affects the baseline linear scan. This uncertainty is not hidden.

Reader controls: no general application claim

Both entity changes together were also compared with unchanged PR #3 in eight balanced blocks, 16 fresh processes per revision per workload, target 400 ms per process, retained medium/byte-input Reader fixtures. Proper deep-copy injection is measured; shallow-copy paths are only equality sentinels. These SwiftSoup-side replays exclude morphology, Realm/dictionary I/O, WebKit, and UI.

Workload Reference ms Candidate ms Less time 95% interval
manabi-parse 3.745240 3.801148 -1.49% -4.53 to +1.46%
manabi-injection-deep 23.271460 22.932565 +1.46% -1.36 to +4.19%
manabi-ruby 5.326387 5.349208 -0.43% -1.70 to +0.83%
manabi-inner 0.018072 0.017880 +1.07% -0.89 to +2.98%

Every Reader interval spans zero. No overall Reader speedup, universal parser gain, Apple-device performance result, or iPhone result is claimed. The two successful helper-specific results do not change that conclusion.

Behavioral verification

On the exact PR #3 build inputs, the unchanged reference plus both new test files passes the native release suite: 818 cases, 801 passed, 17 opt-in benchmarks skipped, zero failures. Each isolated candidate plus its own six new tests passes 812 cases, 795 passed, 17 skipped, zero failures. The new tests also pass against unchanged production source: these are behavior-preserving optimizations, not correctness fixes. No existing test was removed or weakened.

All four shipping variants (reference, escaping only, lookup only, both) match 9,408 full DOM records, 4,608 contextual-fragment records, and 72 nonempty Reader output sets byte-for-byte. DOM output SHA-256: c243b0307f2fbfaeb781bd5786ac51e9e66f659c165b9f33f1590345416b6ca1; fragment output SHA-256: 1c582c7608ddef757b25c57b270e4e63460a21a871025db35dcb60edf5585a98.

The inherited Reader parse-only observer emitted an empty array, so six of its nominal 72 sets did not observe a parsed result. A separate verification-only client now records regenerated HTML and text for parse-only (and ruby-parse), rejects empty output, and was run across all variants. The timed Reader client was not changed. This correction is evidence tooling, not a change to another worker's production PR or retrospective validation of its historical results.

The combined production candidate also passes the complete native AddressSanitizer release suite: 818 cases, 801 passed, 17 skipped, zero failures, with ASAN_OPTIONS=detect_leaks=0. This is not a leak check.

Native CI for the master-based PR is a separate check, not the source of the above PR #3 integration counts. See the PR's checks for the exact published head. ASan results, when reported, use detect_leaks=0 and do not establish leak freedom.

Portable reproduction

The committed public client is the exact source used for final entity confirmation. The committed Python runner applies the same comparison protocol with configurable paths; it is not a copy of the study's machine-specific orchestration paths.

To repeat the exact measured reference, make two detached worktrees at c9c37c80eb571aabba59a1f3ef31af9b072af4a8, then apply only this PR's Sources/Entities.swift diff from master to the candidate. Do not include the other entity change in an isolated comparison. To measure master itself instead, use two master-based worktrees and label that a new experiment.

With BASE and CANDIDATE pointing to those worktrees, and OUT an absolute new build directory:

sh
mkdir -p "$OUT/base" "$OUT/candidate"
ext=so
[ "$(uname -s)" = Darwin ] && ext=dylib
for variant in base candidate; do
  root="$BASE"
  [ "$variant" = candidate ] && root="$CANDIDATE"
  swiftc -swift-version 6 -O -g -whole-module-optimization \
    -emit-library -emit-module -module-name SwiftSoup "$root"/Sources/*.swift \
    -emit-module-path "$OUT/$variant/SwiftSoup.swiftmodule" \
    -o "$OUT/$variant/libSwiftSoup.$ext"
done
swiftc -O -g -I "$OUT/base" -L "$OUT/base" -lSwiftSoup \
  Tools/benchmark_entities.swift -o "$OUT/benchmark-entities"

Run the command below, then repeat with --aa and a different output prefix. The runner pins an allowed CPU on Linux and records lack of affinity on macOS. It compares complete public outputs before timing, checks timed checksums, and refuses to overwrite an existing run. Never time concurrently with compilation, sanitizer runs or a profiler. Host-specific path/binary hashes will necessarily differ from this study.

sh
python3 Tools/compare_entity_benchmarks.py \
  --baseline-library "$OUT/base" --candidate-library "$OUT/candidate" \
  --client "$OUT/benchmark-entities" --blocks 12 --ms 400 --size 64 \
  --workloads unescape-multi unescape-long unescape-unknown unescape-common parse-multi-data parse-long-buffer parse-normal-string \
  --output "$OUT/lookup-ab"
# Repeat with --aa and --output "$OUT/lookup-aa".

Normal project correctness checks remain swift test -c release and, where supported, ASAN_OPTIONS=detect_leaks=0 swift test -c release --sanitize address. The benchmark does not change Package.swift or add dependencies.

Measured artifact identities

  • Reference library SHA-256: 97486712c374feabf95d22d9ae8564ee427e80e210388bee7bf963231ada16f9
  • Candidate library SHA-256: da8fbbb62f5d243fe7250b6a8b0d114dd3382ca31bd03b0071bbeb2994517b5f
  • Identical final client SHA-256: 22672ec15224612f36814f41a8ac792c7d24c550f903f66da8c2b15cb9e51154
  • Candidate Entities Git blob: 61c723bfd79b7bdadf8a6f79e0af6045b4e5de21
  • Runtime patch SHA-256: 2d67086d42310be5f12a2a04557ed2667309016296975ce619235f3363f44132

Complete raw screen/confirmation/A/A records, CPU results, calibration records, fixture and source manifests, failed/neutral observations, exact source snapshots, test logs and complete output sets are retained in the companion delivery bundle. Do not substitute an earlier other-worker report for this experiment.


File: Benchmarks/entity-escaping-20260909.md

Batch pass-through Unicode runs during entity escaping

The UTF escaping helper used to invoke the internal buffer writer for every non-ASCII sequence. It now copies adjacent pass-through sequences together. ASCII replacements, NBSP replacements, custom public replacement callbacks and their order are unchanged. It stops at the same byte boundaries and retains the historical permissive lead-byte stepping for malformed/truncated bytes; it does not silently replace that with a validating UTF-8 decoder. No new cache, state, public API, or normalization policy.

Six new regression methods include an independent byte oracle, 2,048 seeded malformed byte strings, every byte lead at truncation boundaries, combining marks and emoji, base/extended/XHTML, UTF-8/UTF-16 output settings, attribute/text contexts, array/offset-array/Data/nonzero-index-Data/pointer storage, builder aliases and retained snapshots, custom append callbacks, normalization controls, and source-less regenerated HTML.

Independent confirmation

Target 500 ms timed work per process, fixture size 128. escape-japanese contains long continuous Japanese runs plus a required escape. escape-mixed interleaves Japanese, ASCII, emoji, combining marks, NBSP and markup. serialize-japanese regenerates a source-less 128-paragraph document with text and title attributes; it does not reuse original HTML source. These synthetic inputs expose the helper, not a measured traffic distribution.

Workload Reference ms Candidate ms Less time 95% interval
escape-japanese 0.020657 0.011167 +45.94% +44.59 to +47.26%
escape-mixed 0.036759 0.030078 +18.17% +14.68 to +21.52%
serialize-japanese 0.242221 0.150415 +37.90% +36.56 to +39.22%
escape-plain 0.004125 0.004108 +0.41% -2.43 to +3.17%

Small/ASCII control confirmation

Twelve blocks at target 500 ms; an initial median suggested a small ASCII slowdown, but neither the longer repeat nor its A/A control establishes one. Already-safe strings keep their pre-existing no-escaping fast path.

Workload Reference ms Candidate ms Less time 95% interval
escape-ascii 0.015277 0.015022 +1.67% -0.75 to +4.02%
escape-short 0.000313 0.000310 +1.10% -0.85 to +3.01%

Scope and exact reference

Independent change on fork master cb57c9e160da093c7dd87eb91d335b6891d0efb7. The numerical study instead compares the already-optimized PR #3 at c9c37c80eb571aabba59a1f3ef31af9b072af4a8 with that exact production tree plus only this change. The reference Sources Git tree is 98e22c134afa5144fe64565f448260239545f90f. Both revisions have identical original Entities.swift, blob 9a8e292e58323cedb86e834cc24c8bf14af89739.

PRs #1–#7 were audited by scope and changed-file lists; none changes Entities. A later recheck included the new regex PR #8, which changes Pattern, not Entities. No existing PR head, master, dependency pin, source-range setting, or parser default was changed. This does not incorporate or supersede the selector/ownership correctness stack.

The two entity PRs change different helpers and are independently reviewable against master. Their two Tools files are intentionally byte-identical, so they share one portable harness after merging either order.

Measurement protocol

Swift 6.2.1, Linux x86_64, CPU 0 affinity, -O -g -whole-module-optimization -swift-version 6 shipping libraries without -enable-testing. One identical optimized public client dynamically loads each library. Fixture setup, compilation, full-output verification, sanitizer tests and profiling are outside timed intervals. Three in-process warmups precede each timed loop. Hashing is randomized normally; it is not made deterministic for timing.

Confirmation uses 12 balanced ABBA/BAAB blocks: 24 fresh processes per revision per workload. Each block averages the two log times per revision; the table reports geometric-mean milliseconds per operation and a paired-block Student t 95% interval for the log time ratio, transformed to percent less time. Positive means faster. These are exploratory intervals without multiple-comparison correction, not predictions for another machine or production traffic. Wall and process CPU values, calibrations, all raw process records and exact command/library/client/source hashes are retained. No sample/outlier is deleted.

The initial screen (six blocks, 12 processes per revision per workload) is retained separately rather than pooled selectively into confirmation. Targeted and ASCII A/A controls use eight blocks; Reader A/A uses six. All wall-clock A/A intervals include zero; the multipoint-entity A/A interval is especially wide because randomized dictionary ordering affects the baseline linear scan. This uncertainty is not hidden.

Reader controls: no general application claim

Both entity changes together were also compared with unchanged PR #3 in eight balanced blocks, 16 fresh processes per revision per workload, target 400 ms per process, retained medium/byte-input Reader fixtures. Proper deep-copy injection is measured; shallow-copy paths are only equality sentinels. These SwiftSoup-side replays exclude morphology, Realm/dictionary I/O, WebKit, and UI.

Workload Reference ms Candidate ms Less time 95% interval
manabi-parse 3.745240 3.801148 -1.49% -4.53 to +1.46%
manabi-injection-deep 23.271460 22.932565 +1.46% -1.36 to +4.19%
manabi-ruby 5.326387 5.349208 -0.43% -1.70 to +0.83%
manabi-inner 0.018072 0.017880 +1.07% -0.89 to +2.98%

Every Reader interval spans zero. No overall Reader speedup, universal parser gain, Apple-device performance result, or iPhone result is claimed. The two successful helper-specific results do not change that conclusion.

Behavioral verification

On the exact PR #3 build inputs, the unchanged reference plus both new test files passes the native release suite: 818 cases, 801 passed, 17 opt-in benchmarks skipped, zero failures. Each isolated candidate plus its own six new tests passes 812 cases, 795 passed, 17 skipped, zero failures. The new tests also pass against unchanged production source: these are behavior-preserving optimizations, not correctness fixes. No existing test was removed or weakened.

All four shipping variants (reference, escaping only, lookup only, both) match 9,408 full DOM records, 4,608 contextual-fragment records, and 72 nonempty Reader output sets byte-for-byte. DOM output SHA-256: c243b0307f2fbfaeb781bd5786ac51e9e66f659c165b9f33f1590345416b6ca1; fragment output SHA-256: 1c582c7608ddef757b25c57b270e4e63460a21a871025db35dcb60edf5585a98.

The inherited Reader parse-only observer emitted an empty array, so six of its nominal 72 sets did not observe a parsed result. A separate verification-only client now records regenerated HTML and text for parse-only (and ruby-parse), rejects empty output, and was run across all variants. The timed Reader client was not changed. This correction is evidence tooling, not a change to another worker's production PR or retrospective validation of its historical results.

The combined production candidate also passes the complete native AddressSanitizer release suite: 818 cases, 801 passed, 17 skipped, zero failures, with ASAN_OPTIONS=detect_leaks=0. This is not a leak check.

Native CI for the master-based PR is a separate check, not the source of the above PR #3 integration counts. See the PR's checks for the exact published head. ASan results, when reported, use detect_leaks=0 and do not establish leak freedom.

Portable reproduction

The committed public client is the exact source used for final entity confirmation. The committed Python runner applies the same comparison protocol with configurable paths; it is not a copy of the study's machine-specific orchestration paths.

To repeat the exact measured reference, make two detached worktrees at c9c37c80eb571aabba59a1f3ef31af9b072af4a8, then apply only this PR's Sources/Entities.swift diff from master to the candidate. Do not include the other entity change in an isolated comparison. To measure master itself instead, use two master-based worktrees and label that a new experiment.

With BASE and CANDIDATE pointing to those worktrees, and OUT an absolute new build directory:

sh
mkdir -p "$OUT/base" "$OUT/candidate"
ext=so
[ "$(uname -s)" = Darwin ] && ext=dylib
for variant in base candidate; do
  root="$BASE"
  [ "$variant" = candidate ] && root="$CANDIDATE"
  swiftc -swift-version 6 -O -g -whole-module-optimization \
    -emit-library -emit-module -module-name SwiftSoup "$root"/Sources/*.swift \
    -emit-module-path "$OUT/$variant/SwiftSoup.swiftmodule" \
    -o "$OUT/$variant/libSwiftSoup.$ext"
done
swiftc -O -g -I "$OUT/base" -L "$OUT/base" -lSwiftSoup \
  Tools/benchmark_entities.swift -o "$OUT/benchmark-entities"

Run the command below, then repeat with --aa and a different output prefix. The runner pins an allowed CPU on Linux and records lack of affinity on macOS. It compares complete public outputs before timing, checks timed checksums, and refuses to overwrite an existing run. Never time concurrently with compilation, sanitizer runs or a profiler. Host-specific path/binary hashes will necessarily differ from this study.

sh
python3 Tools/compare_entity_benchmarks.py \
  --baseline-library "$OUT/base" --candidate-library "$OUT/candidate" \
  --client "$OUT/benchmark-entities" --blocks 12 --ms 500 --size 128 \
  --workloads escape-japanese escape-mixed serialize-japanese escape-plain escape-ascii escape-short \
  --output "$OUT/escape-ab"
# Repeat with --aa and --output "$OUT/escape-aa".

Normal project correctness checks remain swift test -c release and, where supported, ASAN_OPTIONS=detect_leaks=0 swift test -c release --sanitize address. The benchmark does not change Package.swift or add dependencies.

Measured artifact identities

  • Reference library SHA-256: 97486712c374feabf95d22d9ae8564ee427e80e210388bee7bf963231ada16f9
  • Candidate library SHA-256: 8224610433f55df4fa9d408d02a750dcbc5de43f8e0cc503d3826e42aa06c99f
  • Identical final client SHA-256: 22672ec15224612f36814f41a8ac792c7d24c550f903f66da8c2b15cb9e51154
  • Candidate Entities Git blob: 8dc30c59cabd7d9156a5bab3e92539e0682ec93d
  • Runtime patch SHA-256: e7d0ee7f5df9441a37397ed089ee0bcbc54504b8f061a17a2f519b2c27928e71

Complete raw screen/confirmation/A/A records, CPU results, calibration records, fixture and source manifests, failed/neutral observations, exact source snapshots, test logs and complete output sets are retained in the companion delivery bundle. Do not substitute an earlier other-worker report for this experiment.


File: Benchmarks/fragment-append-confirmation-20260908.md

Fragment-append confirmation β€” September 8, 2026

This is a fresh confirmation of the published changes, not a replacement for the earlier study or a claim that its entire raw archive is reproduced here.

Exact revisions

  • Baseline: 79c79cc99249e45e51f36f574eab28241c40197c (already contains the preceding DOM primitive optimizations).
  • Upstream-ready candidate: 37f5f85d45e594b96b55fea279b90161a90968a0 on perf/dom-primitives-upstream-20260908.
  • Fork integration: 0bd3cb4ad29d79820ffe6c2dbe9d0706359962dc on perf/dom-primitives-20260908, PR #3.

The incremental runtime change is six additions and nine deletions across Attribute, TextNode and DataNode: append directly to the optional fragment array instead of making an additional array value, appending, and assigning it back. Cache invalidation, mutation tokens, accumulated lengths, and materialization order remain unchanged. The fork integration preserves the older runtime stack and upstream's escaped-ID fix. No parser option is changed; in particular, source-range tracking is not disabled by these optimizations.

Portable A/B repeat

Swift 6.2.1, Linux x86_64; optimized standalone XCTest clients and release libraries (-O -g -whole-module-optimization -enable-testing for libraries). CPU affinity pinned to 0. Compilation, behavioral checks and timing were separate. Each operation uses the exact committed FragmentAppendBenchmarkTest source, 256 fragments, 1,000 timed iterations, three warmups, six balanced ABBA/BAAB blocks, and 12 fresh processes per revision. No outliers were removed.

Geometric-mean milliseconds per operation; positive percentages mean less time. Intervals are exploratory paired-block 95% t intervals on log time ratios, without multiple-comparison adjustment. They do not represent other machines or production traffic.

Operation Baseline ms Candidate ms Less time 95% interval
attribute 0.260973 0.025893 +90.1% +89.6 to +90.5%
data 0.261975 0.021965 +91.6% +91.2 to +92.0%
parse-attribute 0.101143 0.102085 -0.9% -5.7 to +3.7%
parse-coalesced 0.701111 0.419226 +40.2% +37.7 to +42.6%
parse-discarded 0.559111 0.553148 +1.1% -0.8 to +2.9%
parse-script 0.212531 0.209720 +1.3% -4.2 to +6.6%
parse-text 0.088264 0.081062 +8.2% +2.6 to +13.4%
text 0.276857 0.029196 +89.5% +89.1 to +89.8%

text, data and attribute measure construction, repeated accumulation and materialization, not a whole parser. parse-coalesced is a deliberately interrupted-text parser stress test with source ranges disabled. parse-discarded uses the same interrupted input with source ranges enabled. This is not a reason to disable source ranges in Manabi. The other parser cases use entity-rich text, an entity-rich attribute and script content respectively. These synthetic cases do not establish workload prevalence.

Reader controls, including the negative results

The external deterministic Manabi-style replays exclude morphology, dictionary/Realm I/O, WebKit and UI. Publication uses proper deep copies rather than the application's previously identified shallow-copy behavior. The confirmation and independent control repeat use 12 and 24 fresh process runs per revision respectively.

Pass Operation Baseline ms Candidate ms Less time 95% interval
initial manabi-parse 8.3302 8.4541 -1.5% -6.1 to +2.9%
initial manabi-injection-deep 55.2623 54.7782 +0.9% -4.3 to +5.7%
initial manabi-publish-deep 7.1998 7.7678 -7.9% -18.0 to +1.4%
initial manabi-highlight 2.9011 2.7696 +4.5% +0.4 to +8.5%
initial manabi-ruby 11.0913 11.1444 -0.5% -2.5 to +1.5%
repeat manabi-publish-deep 7.4747 7.2548 +2.9% -0.2 to +6.0%
repeat manabi-injection-deep 57.0095 57.2986 -0.5% -3.4 to +2.3%
repeat manabi-highlight 2.8719 2.8845 -0.4% -2.9 to +2.0%

The initial publication slowdown did not reproduce. The initial highlighting improvement also did not reproduce. The defensible conclusion remains no established general reader-pipeline speedup from this incremental patch. Both passes are retained rather than selecting whichever estimate looks best. All three unchanged A/A intervals include zero.

The repeat was interrupted by an execution-tool timeout after 80 completed records, exactly at a balanced-block boundary. It resumed with the same recorded iteration counts and remaining schedule. All 144 planned repeat records are retained; none were discarded or duplicated. Together there are 312 external timing/A/A processes plus 24 portable processes containing 192 individual operation measurements.

Fresh behavioral verification

  • All three exact source variants passed the archived standalone suite: 708 passed, 14 opt-in benchmarks skipped, zero failures per variant (722 total). The archived suite does not contain all current upstream/fork tests and is not a full SwiftPM/Xcode run.
  • All 11 newly committed fragment regression tests pass on all three variants. All eight portable fragment benchmarks also pass enabled on baseline and upstream-ready candidate.
  • All 9,408 complete DOM-observation records match across the three variants (1,344 cases Γ— seven parsing configurations).
  • All 72 complete reader replay output sets match across the three variants.
  • The production source trees and both new test-file blob hashes were checked against the published objects. This confirmation did not repeat the previously recorded AddressSanitizer checks. No Apple-device or Instruments measurement is claimed.

Complete DOM-output SHA-256: c243b0307f2fbfaeb781bd5786ac51e9e66f659c165b9f33f1590345416b6ca1.

Sources Git tree hashes:

text
baseline       697f0424d5df9ee2a674c6cfb65c65ec534715ee
candidate      324d663ff0b1f4451fcaeb812a833dd2ffff78e8
fork integrated 10b7b422369c40a5679ae9590ec15826975eb329

Contributor reproduction

Place the identical committed benchmark file in each revision and use the same release toolchain, count and iteration settings. Alternate execution order and run A/A controls. The standard SwiftPM entry point is:

sh
SWIFTSOUP_FRAGMENT_BENCHMARK=1 \
SWIFTSOUP_FRAGMENT_COUNT=256 \
SWIFTSOUP_FRAGMENT_ITERATIONS=1000 \
swift test -c release --filter FragmentAppendBenchmarkTest

The reported measurements used standalone XCTest clients, not that full SwiftPM invocation. The companion confirmation bundle includes the actual build/test scripts, source snapshots, fixture data, complete raw timings and commands, test logs and observation manifests. It does not include the unrecovered original 1,196-run raw archive mentioned in the earlier PR description.

Raw portable batches

Each numeric entry below is the total timed elapsed milliseconds for 1,000 operations. Warmups are excluded. The CSV retains every portable result, including neutral and negative estimates.

csv
block,position,variant,attribute,data,parse-attribute,parse-coalesced,parse-discarded,parse-script,parse-text,text
0,0,baseline,255.411678,275.178052,107.175217,728.168826,560.229020,208.627683,82.587664,262.206516
0,1,published,25.579163,21.365444,101.130164,413.614961,556.322533,203.740599,79.057976,28.301915
0,2,published,25.644611,20.979342,96.723614,406.059186,565.558030,214.578360,77.157391,29.627253
0,3,baseline,289.042623,246.126677,101.098971,658.845932,543.085952,208.748039,81.293368,279.905957
1,0,published,24.883302,21.160754,100.450664,399.310970,534.994820,206.467113,77.740345,29.729945
1,1,baseline,251.566747,258.307253,97.242089,703.919597,571.169321,232.412511,85.424644,294.300435
1,2,baseline,268.804440,261.395035,100.629603,672.653627,558.200816,233.519556,105.852963,280.200172
1,3,published,26.441173,21.566988,97.776195,434.288291,585.710713,209.278208,85.207817,29.266781
2,0,baseline,264.216566,255.137535,107.969436,787.111024,580.278386,200.133102,92.523179,280.276366
2,1,published,25.137188,23.336245,105.524856,460.846221,557.167632,206.959538,85.258122,28.968211
2,2,published,25.255797,21.607769,102.604941,418.373179,561.411036,209.794466,83.369706,29.363366
2,3,baseline,267.514963,265.354769,98.689241,684.140524,540.209326,202.845952,81.171710,276.012317
3,0,published,27.143916,23.656504,104.258134,451.549749,533.544661,211.159182,76.863917,28.390149
3,1,baseline,255.839353,256.408821,98.505994,673.736603,542.258787,206.778982,79.286809,262.138950
3,2,baseline,256.300924,250.297928,102.714925,689.346923,579.523664,208.207956,81.701791,280.083016
3,3,published,26.415296,22.209248,97.398262,422.525679,555.234212,210.426538,78.624652,28.875083
4,0,baseline,265.393020,267.721289,103.903555,698.540515,549.591108,207.191875,82.143195,271.288905
4,1,published,25.831877,22.073273,101.341915,404.993149,548.581006,211.146804,80.607280,30.409425
4,2,published,25.841071,21.943026,99.314581,408.470259,552.156854,210.552480,78.400084,29.967669
4,3,baseline,250.405694,268.493969,95.348208,698.641974,565.410751,217.872818,101.259105,270.276685
5,0,published,25.482749,21.348690,99.822770,411.785482,541.486304,207.406126,79.989474,28.336279
5,1,baseline,252.938428,264.736596,101.550344,710.588512,568.305538,208.021643,101.830145,303.943747
5,2,baseline,256.615419,276.373265,99.654738,716.039535,552.997105,218.940135,89.365609,264.718292
5,3,published,27.179504,22.507033,120.702768,403.784745,547.587184,215.412720,91.747703,29.199427

File: Benchmarks/independent-css-20260909.md

Slash-free inline CSS cleaning

Scope

Independent master-based change on 5d593583469c49d3c84449f28251b4cf201042a8. Only Sources/Whitelist.swift changes at runtime: three added lines at the start of private stripCSSComments. Existing regex compilation, entity escaping, and whitespace-normalization work is not duplicated. No public API, cache, persistent state, sanitizer policy, dependency pin, or other worker's branch changes.

A CSS comment cannot start without an ASCII slash. When the UTF-8 view contains none, return the existing String rather than rebuilding it Character by Character. Any slash, including a quoted slash or a late slash that is not a comment, retains the entire original quote/escape-aware scanner. Property filtering and unsafe-value checks still run. Unicode slash lookalikes do not become comment delimiters.

Tradeoff: slash-containing input incurs an additional scan up to its first slash. A long late-slash control is included explicitly. This is not a claim that all CSS or all cleaning becomes faster.

Isolated release confirmation

Reference: already-optimized PR #3 source c9c37c80eb571aabba59a1f3ef31af9b072af4a8. Candidate: that exact source plus only this runtime change. These first-table numbers are not measurements against master.

Linux x86_64, Swift 6.2.1; shipping -O -g -whole-module-optimization libraries without -enable-testing; one identical separately compiled public client per comparison pair. CPU affinity 0, three warmups, fresh processes, randomized workload order, eight balanced ABBA/BAAB blocks: 16 processes per revision per workload. Target 250 ms in the faster revision, using equal iteration counts. Compilation, tests and profiling were excluded from timing. Geometric-mean milliseconds per operation; exploratory paired-block 95% t intervals on log ratios, without multiple-comparison correction. Every observation retained.

Public workload Reference ms Candidate ms Less time 95% interval
Sanitize 64 short style values 3.117449 2.844915 8.74% 6.59–10.84%
Sanitize 64 long Unicode style values 18.401358 13.572643 26.24% 24.77–27.69%
Long values with a late slash 12.041063 12.105335 -0.53% -2.55–1.44%
Actual-comment control 2.460368 2.431834 1.16% -4.63–6.63%
Clean and regenerate 64 styled paragraphs 4.050292 3.720846 8.13% 5.71–10.50%
Parse, clean and regenerate styled document 4.643244 4.359176 6.12% 0.51–11.41%
Clean document without styles 0.615912 0.596120 3.21% -2.34–8.47%

The earlier independent four-block, 120 ms screen is retained separately, not pooled. Long Unicode strings deliberately expose reconstruction work; their prevalence in production traffic was not measured. Cleanup uses public Cleaner and regenerated HTML, not a private helper-only benchmark or shallow-copy substitution.

Confirmation against newly advanced master

Reference: 5d593583469c49d3c84449f28251b4cf201042a8. Candidate: that exact source plus BOTH this patch and the independent sibling-position patch. This is a combined confirmation, not isolated attribution. A new client was compiled once against the current reference, then kept identical for the pair. Same eight-block/250 ms protocol.

Public workload Reference ms Combined ms Less time 95% interval
Short styles 3.064962 2.827195 7.76% 5.24–10.21%
Long Unicode styles 18.382911 13.342432 27.42% 26.83–28.00%
Late-slash control 11.672791 11.894930 -1.90% -6.30–2.31%
Actual-comment control 2.402561 2.392114 0.43% -1.34–2.17%
Clean styled document 4.004747 3.718265 7.15% 5.20–9.06%
Parse and clean styled document 4.604337 4.281462 7.01% 3.88–10.05%
Clean document without styles 0.633303 0.623906 1.48% -3.42–6.16%

Noise and limits

The initial six-block, 200 ms A/A matrix had one false positive: identical binaries made style-free cleaning appear 2.89% faster, interval 1.23–4.52%. The independent 12-block, 400 ms recheck gives -0.40%, interval -2.64–1.80%; its late-slash control is -0.83%, interval -2.49–0.80%. All intervals in the separate current-master A/A matrix span zero. All initial and repeat records are retained.

Do not interpret small control differences as speedups. This shared host is noisy and the intervals do not establish device-level precision. No overall Manabi, universal parser, Apple-device, or iPhone performance claim. No allocation counts or profiler-derived timing claim.

Correctness and publication

Six new regression methods cover unchanged property filtering/normalization, byte-preserved Japanese/combining/emoji/quoted content, slash-free unsafe values, comments and unterminated comments, 512 seeded cases compared with the original scanner forced by a removable comment prefix, and repeated rule changes without input mutation. No existing tests were weakened.

  • Original PR #3 release: 806 cases, 17 optional benchmarks skipped, zero failures.
  • Unchanged PR #3 plus both new test files: 821 cases, 17 skipped, zero failures. Combined candidate: the same in local debug and release.
  • Current master plus both test files but unchanged production: native Swift 6.1.3 release, 1,080 cases, 17 skipped, zero failures.
  • This independent master-based production/test tree: native Swift 6.1.3 release, 1,071 cases, 17 skipped, zero failures (1,054 passed).
  • Master plus both runtime patches: local Swift 6.2.1 full DEBUG AddressSanitizer suite, 1,080 cases, 17 skipped, zero failures. ASAN_OPTIONS=detect_leaks=0; not leak checking or a release-ASan claim.

Native publication run: https://github.com/lake-of-fire/SwiftSoup/actions/runs/34335351288 . An earlier attempt stopped before testing because the container lacked Python; the successful retry installed it. Runtime/test SHA-256 values were checked before testing and non-force branch publication. All 217 files in the initial published tree were independently checked against local inputs and Git blob hashes. Subsequent additions are only this report and the two already-used benchmark tools.

The old reference, both isolated candidates, old combined candidate, current master and current combined candidate all match 2,108 complete public-observation records byte-for-byte, including CSS results, unchanged inputs, cleaned documents, structural selectors, mutation and regenerated HTML/text. SHA-256: 204a9bfdfa9f50a4789b7ce2f0946be5ae482269a6ec9abec7ec2b7ab853c775. These records are not independent bugs or browser-equivalence checks.

The complete two-candidate study retains 1,760 timed fresh processes across 11 stages, excluding calibration/correctness launches. Every timed checksum and aggregate was independently recomputed. Aggregate JSON SHA-256: 067a43e72434b422f505444267777a594bb8aa9b66e18f259a798d920915218b.

Reproduction

Use swift test -c release for native tests. For isolated timing, apply only this runtime diff to the stated PR #3 reference; for current-master confirmation, apply both runtime diffs to the stated master. Keep clients and source revisions matched within each comparison pair.

Example Linux shipping-library build in each separate source directory:

sh
mkdir -p shipping
swiftc -swift-version 6 -O -g -whole-module-optimization \
  -emit-library -emit-module -module-name SwiftSoup \
  -emit-module-path shipping/SwiftSoup.swiftmodule \
  -o shipping/libSwiftSoup.so Sources/*.swift

Compile one client against the reference and compare:

sh
swiftc -swift-version 6 -O -I /reference/shipping -L /reference/shipping \
  -lSwiftSoup Tools/benchmark_independent_hotspots.swift -o /tmp/hotspots
python3 Tools/compare_independent_hotspots.py --client /tmp/hotspots \
  --baseline-library /reference/shipping --candidate-library /candidate/shipping \
  --suite css --output /tmp/new-css-results --blocks 8 --ms 250

Python requires NumPy/SciPy. Repeat with --aa into another fresh directory. The runner compares complete nonempty outputs before timing, checks every checksum, controls Linux affinity/library loading, and saves calibration, raw trials, binary identities and intervals. Never compile or profile during timing. Exact sources, patches, raw timing records and test logs accompany the delivery archive.


File: Benchmarks/independent-position-20260909.md

Element sibling-position lookup

Scope

Independent master-based change on 5d593583469c49d3c84449f28251b4cf201042a8. Only Sources/Element.swift changes at runtime: 19 additions and one deletion in elementSiblingIndex(). This is distinct from the existing adjacent-sibling navigation and sibling reindexing work. No cache, persistent state, public API, parser default, dependency pin, or other worker's branch changes.

Built-in parents previously materialized the complete filtered Elements list before finding one element's index. Count directly through their child nodes and stop at the target instead. Exact Element, Document and FormElement parents take this path. Custom parent/children/array projections keep the old path. The original two parent() calls, nil-second-result error, zero for absent membership, and independence from stale public siblingIndex values are preserved.

This remains O(n) for one lookup and O(nΒ²) for all positions in a wide parent. It removes unnecessary allocations and scanning, not the asymptotic cost of every structural selector.

Isolated release confirmation

Reference: already-optimized PR #3 source c9c37c80eb571aabba59a1f3ef31af9b072af4a8. Candidate: that exact source plus only this runtime change. These first-table numbers are not measurements against master.

Linux x86_64, Swift 6.2.1, shipping libraries built with -O -g -whole-module-optimization, without -enable-testing. One identical separately compiled public client per comparison pair. CPU affinity 0, three warmups, fresh processes, randomized workload order, eight balanced ABBA/BAAB blocks: 16 processes per revision per workload, targeting 250 ms of work in the faster revision with equal iteration counts. No compilation, tests, or profiling during timing. Geometric-mean milliseconds per operation; exploratory paired-block 95% t intervals on log ratios, without multiple-comparison adjustment. No observations discarded.

Public workload Reference ms Candidate ms Less time 95% interval
All positions of 256 sibling elements 1.996053 0.103813 94.80% 94.49–95.09%
Positions with intervening text/comments 2.279398 0.297094 86.97% 86.51–87.40%
Uncached nth-child selection, 256 paragraphs 2.452187 0.409174 83.31% 82.62–83.98%
Parse 128 paragraphs plus nth-child selection 1.685411 1.153848 31.54% 29.58–33.44%
Custom-parent fallback control 0.927815 0.916840 1.18% -5.01–7.01%
Parse/text control 1.141513 1.128082 1.18% -3.26–5.42%

An earlier independent four-block, 120 ms screening run is retained separately, not pooled into this confirmation. Selection uses public Collector/evaluator traversal, not warmed string-query results. The parsing row includes parsing, selection and normal lifetime costs; it is not a parser-only gain.

Confirmation against newly advanced master

Reference: 5d593583469c49d3c84449f28251b4cf201042a8. Candidate: that exact source plus BOTH this patch and the separate slash-free CSS cleaning patch. This is a combined confirmation, not an isolated attribution. A new public client was compiled once against the current baseline, then held identical across this pair. Same eight-block/250 ms protocol.

Workload Reference ms Combined ms Less time 95% interval
All positions of 256 sibling elements 2.111733 0.110034 94.79% 93.99–95.49%
Mixed sibling positions 2.252929 0.306312 86.40% 86.07–86.73%
Uncached nth-child selection 2.375669 0.392831 83.46% 82.98–83.93%
Parse plus nth-child selection 1.794774 1.276510 28.88% 25.83–31.80%
Custom-parent fallback control 0.922212 0.913415 0.95% -0.86–2.74%
Parse/text control 1.240496 1.219517 1.69% -3.12–6.28%

Every interval in both six-block, 200 ms position A/A matrices spans zero. The companion CSS study did have a false-positive identical-binary control; its longer recheck spans zero. This is a noisy shared host. Do not infer device-level precision, a universal parser speedup, or whole-Manabi performance. Fixtures are synthetic, not measured production traffic; no Apple/iPhone performance was measured.

Correctness and publication

Nine new regression methods cover mixed nodes/widths, stale indexes, detached and missing membership, built-in document/form parents, custom children and array projections, stateful parent callbacks, reparenting/deep copies, and warmed selectors after mutation. They pass against unchanged production as compatibility guards. No existing tests were weakened.

  • Original PR #3 release: 806 cases, 17 optional benchmarks skipped, zero failures.
  • Unchanged PR #3 plus both new test files: 821 cases, 17 skipped, zero failures. Combined candidate: the same in local debug and release.
  • Newly advanced master plus both test files but unchanged production: native Swift 6.1.3 release, 1,080 cases, 17 skipped, zero failures.
  • This independent master-based production/test tree: native Swift 6.1.3 release, 1,074 cases, 17 skipped, zero failures (1,057 passed).
  • Master plus both runtime patches: local Swift 6.2.1 full DEBUG AddressSanitizer suite, 1,080 cases, 17 skipped, zero failures. ASAN_OPTIONS=detect_leaks=0; not leak checking and not a release-ASan claim.

Native publication run: https://github.com/lake-of-fire/SwiftSoup/actions/runs/34335351288 . Its first attempt stopped before testing because Python was missing from the container; the successful retry installed it. Source preparation verified exact runtime/test SHA-256 values before tests and non-force publication. All 217 files in this initial published tree were independently checked against local inputs and Git blob hashes. Subsequent additions are only this report and the two already-used benchmark tools.

The old reference, its two isolated candidates, its combined candidate, current master and current combined candidate all match 2,108 complete public-observation records byte-for-byte: positions, selections, later mutation, HTML/text, CSS filtering and cleaned-document outputs. SHA-256: 204a9bfdfa9f50a4789b7ce2f0946be5ae482269a6ec9abec7ec2b7ab853c775. These are records, not 2,108 independent bugs or a claim of browser equivalence.

The full two-candidate study retains 1,760 timed fresh processes across 11 stages, excluding calibration and correctness launches. Every checksum and aggregate was independently recomputed. Aggregate JSON SHA-256: 067a43e72434b422f505444267777a594bb8aa9b66e18f259a798d920915218b.

Reproduction

Run native project tests with swift test -c release. For the isolated timing study, apply only this runtime diff to the PR #3 reference above. For the current-master confirmation, apply both runtime diffs to the stated current master. Keep source revisions and clients matched within each pair.

Example Linux shipping-library build, executed separately in each source directory:

sh
mkdir -p shipping
swiftc -swift-version 6 -O -g -whole-module-optimization \
  -emit-library -emit-module -module-name SwiftSoup \
  -emit-module-path shipping/SwiftSoup.swiftmodule \
  -o shipping/libSwiftSoup.so Sources/*.swift

Compile one client against the reference library, then compare:

sh
swiftc -swift-version 6 -O -I /reference/shipping -L /reference/shipping \
  -lSwiftSoup Tools/benchmark_independent_hotspots.swift -o /tmp/hotspots
python3 Tools/compare_independent_hotspots.py --client /tmp/hotspots \
  --baseline-library /reference/shipping --candidate-library /candidate/shipping \
  --suite position --output /tmp/new-position-results --blocks 8 --ms 250

Python requires NumPy and SciPy. Repeat with --aa and a different output directory. The runner verifies complete nonempty outputs before timing, controls Linux library loading/CPU affinity, checks all timed checksums, and retains calibration, every trial, binary identities and intervals. Never build or profile concurrently. Raw evidence, exact source snapshots, patches and complete test logs accompany the delivery archive.


File: Benchmarks/pattern-reuse-audit-20260909.md

Pattern reuse: reconciled-source confirmation, September 9, 2026

Source and scope

Baseline is reviewed master 7d2d58c6cb6dd1d2251eb9563a844b3d01718afe, not the older optimization baseline. Candidate 9ee782e56aa9cfae51b24c3e0a3da02f8a8832ed (tree b4d6f44665f69fef3f4b8104304d96dc59bf0678) merges the unchanged Pattern runtime/test change with that master. Only Pattern.swift changes at runtime. Portable Swift/Python tools are committed. Original PR #8 ancestry is retained; no forced update.

Pattern construction now compiles and retains its immutable regex/result. Reused matchers and repeated validation avoid recompilation. Invalid patterns still have nonthrowing construction, throwing validation and the old invalid-matcher diagnostic. Every Matcher retains independent eager matches and its own cursor. Ignored legacy options and capture-string semantics are unchanged.

Native correctness

The source/test-identical predecessor 925a8e2b3848dbfd7ad024807acb8f697ae02b90 passed 1,065 total XCTest cases: 1,048 passed, 17 opt-in benchmarks skipped, zero failures, in each of debug, release and AddressSanitizer on both Ubuntu and macOS, in run 34330228389. ASan used detect_leaks=0; this is not leak checking. The later candidate changes only the benchmark runner, and the corrected workflow explicitly verifies Sources/Tests are identical.

The first run's overall status is red because its benchmark used an invalid library/client pairing. Do not call the whole first run green. Its test logs and failed benchmark are retained.

Corrected measurement protocol

Run 34331467924 completed on both runners. Ubuntu uses Swift 6.3.3/x86_64; macOS uses Swift 6.1.2/arm64. Shipping libraries and public clients use optimization; libraries do not enable testing or library evolution. The exact same public client source is compiled separately against each matching module/library. Pattern's non-resilient struct layout changed, so loading the new library underneath an old-layout client was invalid. That first harness mistake was fixed, not represented as a product crash or accepted timing.

Each operation covers 256 inputs/patterns, except the parse control (64 paragraphs). Three in-process warmups. For each workload/host: eight alternating same-binary A/A pairs and twelve alternating A/B pairs in fresh processes. Common iteration counts are calibrated using both arms, targeting approximately 150 ms for the faster arm, capped at 4,096 iterations. Unused construction uses eight fixed iterations because its baseline is extremely short. Compilation and correctness suites finish before timing. Linux pins one available CPU; macOS has no affinity. No samples are discarded or pooled across hosts.

Percentages below are medians of within-pair ratios, not ratios of separate duration medians. Negative means less time. Checksums must match every pair. The benchmark checksum checks are narrower than the native behavioral suites and do not replace them.

Workload Ubuntu paired change Faster pairs macOS paired change Faster pairs
Reused Pattern -77.01% 12/12 -71.56% 12/12
Reused Pattern plus repeated validate -86.64% 12/12 -84.28% 12/12
Fresh Pattern with one match +0.46% 2/12 -0.17% 6/12
Parse/text control +1.96% 4/12 +0.37% 5/12

No general parse, Reader or one-use speedup is established. A/A reused-Pattern medians were +0.32% on Ubuntu and +7.40% on macOS, 0/8 faster. Other A/A medians range from -1.75% to +2.25%. The macOS control shows measurable host/order drift; the large reuse improvements exceed it, but these are not precision device estimates or confidence intervals.

Real construction tradeoff

For 256 patterns constructed only to read their original string, baseline/candidate separate median durations were 0.0142/0.9541 ms on Ubuntu and 0.0242/0.9759 ms on macOS. Eager compilation is materially more expensive when never used. These very short baseline timings are not suitable for a precise multiplier claim. The compiled expression also remains alive with its Pattern. This tradeoff is intentional, not a universal optimization guarantee.

Evidence integrity

Each host retains 210 process records: 200 A/A and A/B records plus ten calibrations; warmup operations are inside each process and excluded from elapsed timing. Across both hosts: 420 retained records. Library and both client SHA-256 values appear in each summary; raw records retain logical A/A role and actual loaded variant separately.

Downloaded artifact ZIP hashes, independently checked:

  • audit-pattern-matched-ubuntu-22.04: 850f8ca61879faa4482f1836945734e869726b53e0bf647d818ced8de5d144f3.
  • audit-pattern-matched-macos-15: c9d7d3ab18432c912bd8272238aef8f9552fc8e9fe54362df946a9e6ccd87666.

Reproduce by building matching baseline/candidate libraries and matching clients from Tools/benchmark_pattern_reuse.swift, then invoking Tools/compare_pattern_reuse.py with --baseline-library, --candidate-library, --baseline-client, --candidate-client and a new --output directory. The driver refuses an existing output directory. Run normal SwiftPM tests separately. Final PR CI is a separate exact-head gate. No iOS device, full Reader, allocation-count or leak-freedom claim.


File: Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json

{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}


File: .github/FUNDING.yml

These are supported funding model platforms

github: [scinfu]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


File: .github/workflows/macos.yml

name: macOS Swift

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master", "fix/**" ]

jobs:
build:
name: Swift on ${{ matrix.os }}
strategy:
matrix:
os: [macos-15]

text
runs-on: ${{ matrix.os }}

steps:
- name: Print Swift version
  run: swift --version
- uses: actions/checkout@v4
- name: Build
  run: swift build -v
- name: Run tests
  run: swift test -v
- name: Build Xcode framework
  run: xcodebuild -project SwiftSoup.xcodeproj -target SwiftSoup -configuration Release -sdk macosx CODE_SIGNING_ALLOWED=NO build

File: .github/workflows/ubuntu.yml

name: Ubuntu Swift

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master", "fix/**" ]

jobs:
build:
name: Swift ${{ matrix.swift }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04]
swift: ["6.1", "6.0"]

text
runs-on: ${{ matrix.os }}

container: 
  image: swift:${{ matrix.swift }}

steps:
- uses: actions/checkout@v4
- name: Build
  run: swift build -v
- name: Run tests
  run: swift test -v