From 19255dad44575296c1fffd44f1989885b81e7438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 10:15:46 +0200 Subject: [PATCH 1/7] Fix DocC linked inline code and colorless syntax highlighting A linked inline code span (a class-less inside an ) inherited the neutral inline-code pill, and on themes carrying a bare `code { color: ... }` rule it also lost the link color outright, so a linked symbol looked identical to plain code. Add a descendant rule (specificity 0,2,2) that gives the linked pill the accent color, a visible underline, and an accent-dominant border and tint, so it reads as a link in every scheme while keeping the pill shape. The syntax-highlight token colors were all derived from var(--color-accent) via color-mix, which collapsed to near-monochrome under a strongly saturated accent: keyword, type, string and number all read as one hue and only comments stood apart. Decouple the palette into curated, light/dark-aware hues per token (green strings, teal types, violet numbers, magenta attributes); the keyword stays the accent as the one brand-tied signal and comments stay muted. Dark variants flip under [data-theme="dark"], matching the generated token layer. Every fixed hue is checked for WCAG AA on the code surface in both modes. --- Sources/SiteKit/Resources/DocC/docc.css | 87 ++++++++++++++++++++----- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/Sources/SiteKit/Resources/DocC/docc.css b/Sources/SiteKit/Resources/DocC/docc.css index deb0698..803a6db 100644 --- a/Sources/SiteKit/Resources/DocC/docc.css +++ b/Sources/SiteKit/Resources/DocC/docc.css @@ -4039,6 +4039,33 @@ body.sk-docc-shell-body { border-radius: 4px; } +/* A linked inline-code pill must read as interactive, not as just another code pill. The base link + affordance (accent color + prose underline) is otherwise lost: the neutral pill above already + carries an accent-leaning border, and a host theme that sets a bare `code { color: ... }` (legacy + sites often do) flattens the inherited link color outright. The DESCENDANT selector with + :not([class]) lands at specificity (0,2,2), so it out-ranks both the neutral pill (0,2,1) for the + border/tint and any bare `code` color rule (0,0,1) for the text color. Give the pill an explicit + accent text color, a visible underline, and an accent-dominant border + tint so it is unmistakably + a link in every scheme while keeping the redesign's pill shape. `code a` covers the reverse nesting + (a link wrapped inside a code span). */ +.sk-article-body a code:not([class]) { + color: var(--color-accent); + border-color: color-mix(in srgb, var(--color-accent) 60%, var(--color-border)); + background: color-mix(in srgb, var(--color-accent) 14%, var(--color-bg-code-inline)); + text-decoration: underline; + text-underline-offset: 2px; +} +.sk-article-body code a { + color: var(--color-accent); + text-decoration: underline; + text-underline-offset: 2px; +} +.sk-article-body a:hover code:not([class]), +.sk-article-body code a:hover { + color: var(--color-accent-hover, var(--color-accent)); + border-color: var(--color-accent); +} + /* Code inside any fenced block fills the block and drops the inline treatment: block display, no padding/border, inherited size. Covers both highlighted () and the class-less of an un-highlighted block. */ @@ -4062,11 +4089,21 @@ body.sk-docc-shell-body { } /* ── Syntax-highlight token colors ─────────────────────────────── - Token spans emitted by CodeHighlighter during build time. All colors are - theme-token-driven via color-mix so they adapt to every color scheme and - both light/dark modes without per-scheme overrides. - The .sk-docc-highlight scope gate ensures these rules never affect non-DocC - or un-highlighted code blocks. */ + Token spans emitted by CodeHighlighter during build time. + + The palette is INDEPENDENT, not accent-derived. An earlier version mixed every token from + var(--color-accent), which collapsed to near-monochrome under a strongly saturated accent: the + accent-dominant mix swamped each token's secondary hue, so keyword/type/string/number all read as + one colour (e.g. all-blue or all-orange) and only comments stood apart. Syntax highlighting works + on stable, learned colours, so code legibility outranks per-scheme tinting here. Each token gets a + curated, distinct hue (Xcode/DocC-like): green strings, teal types, violet numbers, magenta + attributes. Only the KEYWORD stays var(--color-accent) – it is the strongest signal and keeps a + thread of brand/scheme cohesion. Comments stay muted + italic. + + Light values live here at the default scope; dark values flip under [data-theme="dark"] below, + matching the same mechanism the generated tokens.css uses to switch modes. Hexes are hand-checked + for WCAG AA (>= 4.5:1) against --color-bg-code in both modes. The .sk-docc-highlight scope gate + ensures these rules never affect non-DocC or un-highlighted code blocks. */ /* Comments: muted, italicised – conventional across editors. */ .sk-docc-highlight .sk-tok-comment { @@ -4074,31 +4111,47 @@ body.sk-docc-shell-body { font-style: italic; } -/* String literals: accent-adjacent warm tone derived from the scheme accent. - color-mix shifts toward a green-adjacent hue on most schemes while keeping - contrast – works in both light and dark because the accent itself is always - legible against the code surface. */ +/* String literals: green – the most widely learned string colour. */ .sk-docc-highlight .sk-tok-string { - color: color-mix(in srgb, var(--color-accent) 80%, #22c55e); + color: #166534; } -/* Numeric literals: a complementary warm tone. */ +/* Numeric literals: violet, distinct from a blue or warm accent in either mode. */ .sk-docc-highlight .sk-tok-number { - color: color-mix(in srgb, var(--color-accent) 60%, #f59e0b); + color: #6d28d9; } -/* Keywords: accent color (the strongest semantic signal). */ +/* Keywords: accent colour (the strongest semantic signal, and the one brand-tied token). */ .sk-docc-highlight .sk-tok-keyword { color: var(--color-accent); font-weight: 600; } -/* Type names: a slightly lighter or shifted accent-adjacent tone. */ +/* Type names: teal – the conventional DocC type colour, and the most frequent token, so it must + stay clearly separate from the accent-coloured keyword. */ .sk-docc-highlight .sk-tok-type { - color: color-mix(in srgb, var(--color-accent) 75%, #818cf8); + color: #115e59; } -/* Attributes (@MainActor, @State, …): a purple-shifted tone distinct from keywords. */ +/* Attributes (@MainActor, @State, …): magenta, distinct from the violet numbers. */ .sk-docc-highlight .sk-tok-attribute { - color: color-mix(in srgb, var(--color-accent) 55%, #a78bfa); + color: #a21caf; +} + +/* Dark mode: the same hue families, lifted to lighter tints so each token clears WCAG AA against the + darker --color-bg-code. Driven by [data-theme="dark"] (the toggle the head script sets and the + generated tokens.css switches on), so it tracks the active theme. Keyword and comment need no flip + – keyword rides var(--color-accent) and comment rides var(--color-text-secondary), both of which + the token layer already swaps per mode. */ +[data-theme="dark"] .sk-docc-highlight .sk-tok-string { + color: #4ade80; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-number { + color: #c4b5fd; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-type { + color: #2dd4bf; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-attribute { + color: #f0abfc; } From 88f3102e2c688f37f003bbac561409fa00389c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 14:19:14 +0200 Subject: [PATCH 2/7] Add optional SwiftSyntax-based Swift code highlighter for DocC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regex highlighter only recognizes keywords, strings, comments, numbers, attributes, and capitalized type names, so DocC code blocks read flat: value references, calls, members, and parameters are all left uncolored. This adds a semantic-near alternative that classifies Swift tokens by their syntactic role, most visibly turning variable references green, for an Xcode-like palette. Introduce a `CodeHighlighting` protocol seam. The zero-dependency regex `CodeHighlighter` remains the default conformer, and the shared `applyToBodyHTML` block-rewriting plumbing moves onto the protocol so every conformer reuses it. `DocCLoader` now holds an injected highlighter (default regex), and `SiteBuilder.docc(…, highlighter:)` exposes the injection point. The SwiftSyntax highlighter lives in its own `SiteKitSyntaxHighlighting` product and target, depending on only `SwiftParser`, `SwiftSyntax`, and `SwiftIDEUtils` (not the macro/compiler-plugin modules). Target-based dependency resolution keeps swift-syntax out of any build that uses only the base `SiteKit` product, so non-DocC sites pay no added build cost. It uses the syntactic classification as a base and refines generic identifiers via a syntax-tree visitor into type, call, variable, member, param, boolean, and label roles; non-Swift snippets fall back to the regex highlighter. Classification is purely syntactic, so all type references share one class (no framework-vs-project split). Seed core `docc.css` with a fixed Apple/Xcode palette for every role class in light and dark, and pin the highlighted code-block surface so the fixed colors keep their WCAG contrast regardless of the site accent. --- Package.resolved | 11 +- Package.swift | 32 ++++ Sources/SiteKit/Pipeline/SiteBuilder.swift | 10 +- Sources/SiteKit/Plugins/DocC/DocCLoader.swift | 24 ++- Sources/SiteKit/Resources/DocC/docc.css | 136 ++++++++----- .../SiteKit/Utilities/CodeHighlighter.swift | 96 +++------- .../SiteKit/Utilities/CodeHighlighting.swift | 135 +++++++++++++ .../SwiftSyntaxHighlighter.swift | 136 +++++++++++++ .../SwiftTokenRoleClassifier.swift | 146 ++++++++++++++ .../HighlighterPreviewGenerator.swift | 168 ++++++++++++++++ .../SwiftSyntaxHighlighterTests.swift | 180 ++++++++++++++++++ 11 files changed, 948 insertions(+), 126 deletions(-) create mode 100644 Sources/SiteKit/Utilities/CodeHighlighting.swift create mode 100644 Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift create mode 100644 Sources/SiteKitSyntaxHighlighting/SwiftTokenRoleClassifier.swift create mode 100644 Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift create mode 100644 Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift diff --git a/Package.resolved b/Package.resolved index 4247bca..e426ad1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "228703345ba4471e3c0fc29c7b71f4486d20782e43b972784dc1b3b010895fbd", + "originHash" : "8ed9a9d9e721da71dfc2e44baf106c5f006158b5665518d22d352e32f671eb79", "pins" : [ { "identity" : "swift-argument-parser", @@ -55,6 +55,15 @@ "version" : "0.7.3" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax.git", + "state" : { + "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", + "version" : "603.0.2" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 987513a..1e45e29 100644 --- a/Package.swift +++ b/Package.swift @@ -6,6 +6,12 @@ let package = Package( platforms: [.macOS(.v26)], products: [ .library(name: "SiteKit", targets: ["SiteKit"]), + // Optional add-on library: a SwiftSyntax-based Swift code highlighter for DocC sites that want + // semantic-near token roles (variable/call/member/param/…) instead of the regex highlighter's + // capitalized-only type detection. It lives in its own product+target so it pulls swift-syntax + // ONLY into builds that actually use it. A consumer depending only on the `SiteKit` product never + // compiles swift-syntax (SE-0226 target-based dependency resolution prunes it). + .library(name: "SiteKitSyntaxHighlighting", targets: ["SiteKitSyntaxHighlighting"]), // The executable *product* is `sitekit` (the durable public command name); its *target* // is `SiteKitCLI` because a target literally named `sitekit` collides with the `SiteKit` // library target on a case-insensitive filesystem – at both the `Sources/` directory and @@ -19,6 +25,12 @@ let package = Package( .package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"), .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + + // swift-syntax powers ONLY the optional SiteKitSyntaxHighlighting target. The 6xx.x line tracks + // the Swift toolchain (603.x = Swift 6.3, the toolchain SiteKit builds with). Only the parser + + // tree + syntactic-classification modules are used; the macro/compiler-plugin modules (the heavy, + // slow-to-compile part of swift-syntax) are deliberately NOT depended upon. + .package(url: "https://github.com/swiftlang/swift-syntax.git", "603.0.0"..<"604.0.0"), ], targets: [ .target( @@ -38,10 +50,30 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), ] ), + // Optional SwiftSyntax-based highlighter. Depends on the base `SiteKit` library (for the + // `CodeHighlighting` seam) plus exactly three swift-syntax modules: SwiftParser (error-tolerant + // parsing of code fragments), SwiftSyntax (the tree + visitor), and SwiftIDEUtils (the syntactic + // `classifications` API that SourceKit uses). Deliberately excludes SwiftSyntaxMacros, + // SwiftCompilerPlugin, SwiftSyntaxMacroExpansion, SwiftOperators, SwiftSyntaxBuilder and + // SwiftParserDiagnostics – none are needed to classify tokens, and they are the bulk of the + // swift-syntax build cost. + .target( + name: "SiteKitSyntaxHighlighting", + dependencies: [ + "SiteKit", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftParser", package: "swift-syntax"), + .product(name: "SwiftIDEUtils", package: "swift-syntax"), + ] + ), .testTarget( name: "SiteKitTests", dependencies: ["SiteKit"] ), + .testTarget( + name: "SiteKitSyntaxHighlightingTests", + dependencies: ["SiteKitSyntaxHighlighting"] + ), .testTarget( name: "SiteKitCLITests", dependencies: ["SiteKitCLI"] diff --git a/Sources/SiteKit/Pipeline/SiteBuilder.swift b/Sources/SiteKit/Pipeline/SiteBuilder.swift index cdc0f88..17c9760 100644 --- a/Sources/SiteKit/Pipeline/SiteBuilder.swift +++ b/Sources/SiteKit/Pipeline/SiteBuilder.swift @@ -496,7 +496,8 @@ public struct SiteBuilder { public static func docc( config: SiteConfig, projectDirectory: URL, - cleanBeforeBuild: Bool = true + cleanBeforeBuild: Bool = true, + highlighter: (any CodeHighlighting)? = nil ) -> SiteBuilder { let urlPrefix = config.effectiveSections.first?.urlPrefix ?? "documentation" @@ -505,7 +506,7 @@ public struct SiteBuilder { var builder = SiteBuilder(config: config, projectDirectory: projectDirectory) .cleanBeforeBuild(cleanBeforeBuild) .contentDiscovery(DocCCatalogDiscovery()) - .articleLoader(DocCLoader(language: config.language, defaultCodeLanguage: config.docc?.defaultCodeLanguage)) + .articleLoader(DocCLoader(language: config.language, defaultCodeLanguage: config.docc?.defaultCodeLanguage, highlighter: highlighter)) // Emit every *.docc/Images/ asset into output /assets/ so @PageImage icon // URLs like /assets/WWDC25.svg resolve to real files in production. .additionalTeleporter(DocCCatalogImageTeleporter(contentDirectory: contentDirectory)) @@ -698,11 +699,12 @@ extension SiteBuilder { /// ``` public static func docc( configPath: String, - cleanBeforeBuild: Bool = true + cleanBeforeBuild: Bool = true, + highlighter: (any CodeHighlighting)? = nil ) throws -> SiteBuilder { let projectDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) let config = SiteBuilder.loadConfigOrExit(at: configPath, in: projectDirectory) - return docc(config: config, projectDirectory: projectDirectory, cleanBeforeBuild: cleanBeforeBuild) + return docc(config: config, projectDirectory: projectDirectory, cleanBeforeBuild: cleanBeforeBuild, highlighter: highlighter) } /// Loads the site configuration for a `configPath:` convenience factory, reporting diff --git a/Sources/SiteKit/Plugins/DocC/DocCLoader.swift b/Sources/SiteKit/Plugins/DocC/DocCLoader.swift index 809c039..57465ef 100644 --- a/Sources/SiteKit/Plugins/DocC/DocCLoader.swift +++ b/Sources/SiteKit/Plugins/DocC/DocCLoader.swift @@ -27,10 +27,28 @@ public struct DocCLoader: Loader { /// untagged blocks are highlighted as if they were tagged with this language. Nil means /// plain escaped text. Does not override an explicitly tagged block's language. private let defaultCodeLanguage: String? + /// The code highlighter applied to fenced code blocks. Defaults to the zero-dependency + /// regex `CodeHighlighter`; a DocC site can inject `SwiftSyntaxHighlighter` (from the + /// `SiteKitSyntaxHighlighting` product) for semantic-near Swift token roles. + private let highlighter: any CodeHighlighting - public init(language: String = "en", defaultCodeLanguage: String? = nil) { + /// Creates a DocC loader. + /// + /// - Parameters: + /// - language: The locale this loader produces pages for. + /// - defaultCodeLanguage: Fallback language for untagged fenced code blocks. + /// - highlighter: The code highlighter for fenced blocks. Pass nil (the default) to use the + /// zero-dependency regex `CodeHighlighter`; inject `SwiftSyntaxHighlighter` for the richer + /// SwiftSyntax-based Swift roles. The default is resolved internally so callers depending + /// only on `SiteKit` never reference the swift-syntax product. + public init( + language: String = "en", + defaultCodeLanguage: String? = nil, + highlighter: (any CodeHighlighting)? = nil + ) { self.language = language self.defaultCodeLanguage = defaultCodeLanguage + self.highlighter = highlighter ?? CodeHighlighter() } public func load(source: MarkdownSource) throws -> PageModel { @@ -119,7 +137,7 @@ public struct DocCLoader: Loader { extensions["doccAIOnly"] = true } else if let aiBody = self.aiVariantBodyHTML(communityPath: source.filePath) { // Apply syntax highlighting to the AI-variant body as well. - let highlightedAI = CodeHighlighter.applyToBodyHTML(aiBody, defaultLanguage: self.defaultCodeLanguage) + let highlightedAI = self.highlighter.applyToBodyHTML(aiBody, defaultLanguage: self.defaultCodeLanguage) extensions["doccAIVariant"] = highlightedAI } @@ -127,7 +145,7 @@ public struct DocCLoader: Loader { // DocC notes only: the MarkdownRenderer is shared across all site types and must // not be changed. Highlighting is applied here (post-rendering, pre-PageModel) so // it is cleanly isolated and does not affect non-DocC pages. - let highlightedBodyHTML = CodeHighlighter.applyToBodyHTML( + let highlightedBodyHTML = self.highlighter.applyToBodyHTML( parsed.bodyHTML, defaultLanguage: self.defaultCodeLanguage ) diff --git a/Sources/SiteKit/Resources/DocC/docc.css b/Sources/SiteKit/Resources/DocC/docc.css index 803a6db..11abbb1 100644 --- a/Sources/SiteKit/Resources/DocC/docc.css +++ b/Sources/SiteKit/Resources/DocC/docc.css @@ -4089,69 +4089,115 @@ body.sk-docc-shell-body { } /* ── Syntax-highlight token colors ─────────────────────────────── - Token spans emitted by CodeHighlighter during build time. - - The palette is INDEPENDENT, not accent-derived. An earlier version mixed every token from - var(--color-accent), which collapsed to near-monochrome under a strongly saturated accent: the - accent-dominant mix swamped each token's secondary hue, so keyword/type/string/number all read as - one colour (e.g. all-blue or all-orange) and only comments stood apart. Syntax highlighting works - on stable, learned colours, so code legibility outranks per-scheme tinting here. Each token gets a - curated, distinct hue (Xcode/DocC-like): green strings, teal types, violet numbers, magenta - attributes. Only the KEYWORD stays var(--color-accent) – it is the strongest signal and keeps a - thread of brand/scheme cohesion. Comments stay muted + italic. - - Light values live here at the default scope; dark values flip under [data-theme="dark"] below, - matching the same mechanism the generated tokens.css uses to switch modes. Hexes are hand-checked - for WCAG AA (>= 4.5:1) against --color-bg-code in both modes. The .sk-docc-highlight scope gate - ensures these rules never affect non-DocC or un-highlighted code blocks. */ - -/* Comments: muted, italicised – conventional across editors. */ -.sk-docc-highlight .sk-tok-comment { - color: color-mix(in srgb, var(--color-text-secondary, var(--color-text)) 70%, transparent); - font-style: italic; + Token spans emitted by a code highlighter during build time. Two highlighters feed these classes: + the zero-dependency regex CodeHighlighter (keyword/type/string/number/attribute/comment) and the + optional SwiftSyntax highlighter, which additionally classifies call/variable/member/param/boolean/ + operator/label so a DocC site gets an Xcode-like, semantic-near palette – notably GREEN variable + references (the `stickers` in `ForEach(stickers) { … }`). + + The palette is a FIXED Apple/Xcode set, not accent-derived: syntax highlighting reads best with + stable, learned colours, so code legibility outranks per-scheme tinting here (an earlier + accent-derived mix collapsed to near-monochrome under a saturated accent). Because the colours are + fixed, the highlighted code-block surface is pinned too (below) so every token keeps a known WCAG + contrast regardless of the site's accent or colour scheme. Roles Xcode leaves in the default text + colour – member, param, operator, label – get no rule and simply inherit, matching the reference + screenshot where `.swipeActions` and the `sticker` binding are default-coloured. + + Light values live at the default scope; dark values flip under [data-theme="dark"], the same toggle + the generated tokens.css uses. Values are seeded from the SiteKit DocC palette spec (Apple + swift-docc-render / Xcode); a site can override any of them in its own CSS with no Swift rebuild. + Measured WCAG contrast on the pinned surfaces: light tokens >= 4.5 except attribute 4.17 and comment + 3.78 (both AA-large, muted-by-design / tuning candidates); dark tokens all >= 4.5. */ + +/* Pin the highlighted code-block surface so the fixed token palette keeps its measured contrast in + both modes (light #f5f5f7 ≈ Apple near-white, which also lifts the whole palette; dark #1f1f24 ≈ + Xcode editor). Scoped to highlighted blocks only, so inline code pills and un-highlighted blocks + keep their scheme-adaptive surface. */ +.sk-article-body pre.sk-docc-highlight { + background: #f5f5f7; } - -/* String literals: green – the most widely learned string colour. */ -.sk-docc-highlight .sk-tok-string { - color: #166534; +[data-theme="dark"] .sk-article-body pre.sk-docc-highlight { + background: #1f1f24; } -/* Numeric literals: violet, distinct from a blue or warm accent in either mode. */ -.sk-docc-highlight .sk-tok-number { - color: #6d28d9; +/* Comments: muted grey, italicised – conventional across editors. */ +.sk-docc-highlight .sk-tok-comment { + color: #707F8C; + font-style: italic; } -/* Keywords: accent colour (the strongest semantic signal, and the one brand-tied token). */ +/* Keywords, and #if/#else directives: Apple pink – the strongest semantic signal. */ .sk-docc-highlight .sk-tok-keyword { - color: var(--color-accent); + color: #AD3DA4; + font-weight: 600; +} + +/* Booleans and nil: the same pink as keywords. */ +.sk-docc-highlight .sk-tok-boolean { + color: #AD3DA4; font-weight: 600; } -/* Type names: teal – the conventional DocC type colour, and the most frequent token, so it must - stay clearly separate from the accent-coloured keyword. */ +/* Type references: purple. All type references share this one colour – a syntactic highlighter cannot + reproduce Xcode's framework-vs-project split, so `ScrollView` and a project's own type look alike. */ .sk-docc-highlight .sk-tok-type { - color: #115e59; + color: #703DAA; +} + +/* Calls: seeded green for "more colour" (it greens project initializers like `StickerListItemView`); + flip to the type purple for the cleaner look that keeps framework `ForEach` from going green – a + CSS-only change, no Swift rebuild. */ +.sk-docc-highlight .sk-tok-call { + color: #3C7D3C; +} + +/* Variable references: green – the headline of the semantic-near palette. */ +.sk-docc-highlight .sk-tok-variable { + color: #3C7D3C; } -/* Attributes (@MainActor, @State, …): magenta, distinct from the violet numbers. */ +/* String literals: Apple red. */ +.sk-docc-highlight .sk-tok-string { + color: #D12F1B; +} + +/* Numeric literals: Apple blue. */ +.sk-docc-highlight .sk-tok-number { + color: #272AD8; +} + +/* Attributes (@State, @MainActor, …): ocher. */ .sk-docc-highlight .sk-tok-attribute { - color: #a21caf; + color: #947100; } -/* Dark mode: the same hue families, lifted to lighter tints so each token clears WCAG AA against the - darker --color-bg-code. Driven by [data-theme="dark"] (the toggle the head script sets and the - generated tokens.css switches on), so it tracks the active theme. Keyword and comment need no flip - – keyword rides var(--color-accent) and comment rides var(--color-text-secondary), both of which - the token layer already swaps per mode. */ -[data-theme="dark"] .sk-docc-highlight .sk-tok-string { - color: #4ade80; +/* Dark mode: the same hue families, lifted to lighter tints that clear WCAG AA on the dark surface. + Driven by [data-theme="dark"] (the toggle the head-init script sets and the generated tokens.css + switches on), so it tracks the active theme. member/param/operator/label inherit in both modes. */ +[data-theme="dark"] .sk-docc-highlight .sk-tok-comment { + color: #7F8C98; } -[data-theme="dark"] .sk-docc-highlight .sk-tok-number { - color: #c4b5fd; +[data-theme="dark"] .sk-docc-highlight .sk-tok-keyword { + color: #FF7AB2; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-boolean { + color: #FF7AB2; } [data-theme="dark"] .sk-docc-highlight .sk-tok-type { - color: #2dd4bf; + color: #DABAFF; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-call { + color: #7FD98A; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-variable { + color: #7FD98A; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-string { + color: #FF8170; +} +[data-theme="dark"] .sk-docc-highlight .sk-tok-number { + color: #D9C97C; } [data-theme="dark"] .sk-docc-highlight .sk-tok-attribute { - color: #f0abfc; + color: #CC9768; } diff --git a/Sources/SiteKit/Utilities/CodeHighlighter.swift b/Sources/SiteKit/Utilities/CodeHighlighter.swift index c158a25..08bd210 100644 --- a/Sources/SiteKit/Utilities/CodeHighlighter.swift +++ b/Sources/SiteKit/Utilities/CodeHighlighter.swift @@ -19,9 +19,21 @@ import Foundation /// ```swift /// let html = CodeHighlighter.highlight(code: rawCode, language: "swift") /// ``` -struct CodeHighlighter { +public struct CodeHighlighter: CodeHighlighting { - // MARK: - Public API + /// Creates the zero-dependency regex highlighter – the default DocC code highlighter. + public init() {} + + // MARK: - CodeHighlighting + + /// Highlights `code` for `language`, satisfying the `CodeHighlighting` seam. Forwards to the + /// static `highlight(code:language:)` entry point that carries the regex implementation, so + /// the regex logic has a single home while `DocCLoader` can hold this as `any CodeHighlighting`. + public func highlight(code: String, language: String?) -> String { + Self.highlight(code: code, language: language) + } + + // MARK: - Static API /// Highlights `code` for the given `language` and returns an HTML fragment /// containing `` tokens. The returned string is @@ -53,92 +65,30 @@ struct CodeHighlighter { /// This method is DocC-only: it is called from `DocCLoader` after the body /// HTML is produced and is not referenced by any shared renderer. /// + /// The block-extraction plumbing now lives on the `CodeHighlighting` protocol so every + /// conformer shares it; this static entry point is a thin shim over the regex conformer + /// for callers (and tests) that reach `CodeHighlighter` directly. + /// /// - Parameters: /// - html: The rendered article body HTML (code content is already HTML-escaped /// by `MarkdownRenderer`). /// - defaultLanguage: Optional fallback language for untagged fences. static func applyToBodyHTML(_ html: String, defaultLanguage: String?) -> String { - // Match:
...content...
- // or
...content...
(no language class) - // The content may span multiple lines, so .dotMatchesLineSeparators is required. - guard let regex = try? NSRegularExpression( - pattern: #"
(.*?)
"#, - options: [.dotMatchesLineSeparators] - ) else { return html } - - let ns = html as NSString - var result = "" - var cursor = 0 - - let matches = regex.matches(in: html, range: NSRange(location: 0, length: ns.length)) - for match in matches { - // Append verbatim text before this match. - let wholeRange = match.range - result += ns.substring(with: NSRange(location: cursor, length: wholeRange.location - cursor)) - - // Extract language from `class="language-X"` (group 2). When the class - // attribute is entirely absent group 1 and group 2 are both empty. - let language: String? - if match.range(at: 2).location != NSNotFound { - let langRaw = ns.substring(with: match.range(at: 2)) - language = langRaw.isEmpty ? defaultLanguage : langRaw - } else { - language = defaultLanguage - } - - // The already-escaped code content (group 3). - let escapedContent: String - if match.range(at: 3).location != NSNotFound { - escapedContent = ns.substring(with: match.range(at: 3)) - } else { - escapedContent = "" - } - - // Unescape then re-highlight so the tokenizer works on plain text. - let rawContent = unescapeHTML(escapedContent) - let highlighted: String - let classAttr: String - if let lang = language?.lowercased().trimmingCharacters(in: .whitespaces), !lang.isEmpty { - highlighted = highlight(code: rawContent, language: lang) - classAttr = " class=\"language-\(escapeAttribute(lang))\"" - } else { - // No language and no default: just re-escape the raw content. - highlighted = escapeHTML(rawContent) - classAttr = "" - } - - result += "
\(highlighted)
" - cursor = wholeRange.location + wholeRange.length - } - - result += ns.substring(with: NSRange(location: cursor, length: ns.length - cursor)) - return result + CodeHighlighter().applyToBodyHTML(html, defaultLanguage: defaultLanguage) } // MARK: - HTML helpers - /// Escapes `<`, `>`, `&`, and `"` so the text is safe in HTML content. + /// Escapes `<`, `>`, `&`, and `"` so the text is safe in HTML content. Forwards to the + /// shared `HTMLEscaping` helper so every highlighter escapes identically. static func escapeHTML(_ string: String) -> String { - string - .replacing("&", with: "&") - .replacing("<", with: "<") - .replacing(">", with: ">") - .replacing("\"", with: """) + HTMLEscaping.escape(string) } /// Reverses the four basic HTML entities used by `escapeHTML`. /// Used to recover raw source text from already-escaped HTML before re-tokenizing. static func unescapeHTML(_ string: String) -> String { - string - .replacing("<", with: "<") - .replacing(">", with: ">") - .replacing(""", with: "\"") - .replacing("&", with: "&") - } - - /// Escapes a string for use in an HTML attribute value (double-quote context). - private static func escapeAttribute(_ string: String) -> String { - escapeHTML(string) + HTMLEscaping.unescape(string) } // MARK: - Tokenization engine diff --git a/Sources/SiteKit/Utilities/CodeHighlighting.swift b/Sources/SiteKit/Utilities/CodeHighlighting.swift new file mode 100644 index 0000000..f819b65 --- /dev/null +++ b/Sources/SiteKit/Utilities/CodeHighlighting.swift @@ -0,0 +1,135 @@ +import Foundation + +/// HTML entity escaping shared by every code highlighter and the body-rewriting plumbing. +/// +/// Kept in one place so the regex `CodeHighlighter`, the `CodeHighlighting` body-rewriter, +/// and any out-of-module conformer (e.g. the SwiftSyntax highlighter) all escape token text +/// identically. Only the four entities a build-time highlighter ever needs are handled. +public enum HTMLEscaping { + /// Escapes `&`, `<`, `>`, and `"` so the text is safe in HTML content. The `&` rule runs + /// first so the ampersands introduced by the other rules are not re-escaped. + public static func escape(_ string: String) -> String { + string + .replacing("&", with: "&") + .replacing("<", with: "<") + .replacing(">", with: ">") + .replacing("\"", with: """) + } + + /// Reverses the four entities `escape(_:)` produces. Used to recover raw source text from + /// already-escaped rendered HTML before re-tokenizing it. + public static func unescape(_ string: String) -> String { + string + .replacing("<", with: "<") + .replacing(">", with: ">") + .replacing(""", with: "\"") + .replacing("&", with: "&") + } +} + +/// A build-time syntax highlighter for DocC code blocks. +/// +/// A conformer turns one raw (unescaped) code snippet into an HTML fragment of +/// `` tokens whose contents are HTML-escaped, so the output is safe +/// to embed inside a `` element. The shared `applyToBodyHTML(_:defaultLanguage:)` +/// default walks a rendered article body and re-highlights every fenced `
` block
+/// through `highlight(code:language:)`, so a conformer only implements the per-snippet step.
+///
+/// SiteKit ships two conformers:
+/// - `CodeHighlighter` – the zero-dependency regex highlighter, the default in the base
+///   `SiteKit` library. It recognizes keywords, strings, comments, numbers, attributes, and
+///   capitalized type names.
+/// - `SwiftSyntaxHighlighter` – a SwiftSyntax-based highlighter in the optional
+///   `SiteKitSyntaxHighlighting` product. It classifies Swift tokens by syntactic role
+///   (variable, call, member, parameter, …) for a semantic-near, Xcode-like palette.
+///
+/// DocC sites opt into the richer highlighter by injecting it (`SiteBuilder.docc(…, highlighter:)`
+/// or `DocCLoader(…, highlighter:)`); sites that stay on the default never compile swift-syntax.
+public protocol CodeHighlighting: Sendable {
+   /// Highlights `code` for the given `language` and returns an HTML fragment containing
+   /// `` tokens. The returned string is safe to embed inside a ``
+   /// element without further escaping. A nil, empty, or unknown language returns plain escaped
+   /// text with no spans.
+   ///
+   /// - Parameters:
+   ///   - code: The raw, unescaped source code to highlight.
+   ///   - language: A language identifier (e.g. "swift", "python"). Nil or empty falls back to
+   ///     plain escaped text.
+   func highlight(code: String, language: String?) -> String
+}
+
+extension CodeHighlighting {
+   /// Post-processes rendered article body HTML and applies syntax highlighting to every
+   /// `
` block found. + /// + /// Adds `sk-docc-highlight` to the `
` class list so the token CSS rules are scoped to
+   /// highlighted blocks. Blocks whose language class is absent or empty use `defaultLanguage`
+   /// when provided; blocks with no language and no default are left as plain escaped text.
+   ///
+   /// This is the shared block-level plumbing for every conformer: it locates the fenced blocks
+   /// and delegates the per-snippet work to `highlight(code:language:)`. It is DocC-only – it is
+   /// called from `DocCLoader` after the body HTML is produced and is not referenced by any
+   /// shared renderer.
+   ///
+   /// - Parameters:
+   ///   - html: The rendered article body HTML (code content is already HTML-escaped by
+   ///     `MarkdownRenderer`).
+   ///   - defaultLanguage: Optional fallback language for untagged fences.
+   public func applyToBodyHTML(_ html: String, defaultLanguage: String?) -> String {
+      // Match: 
...content...
+ // or
...content...
(no language class) + // The content may span multiple lines, so .dotMatchesLineSeparators is required. + guard let regex = try? NSRegularExpression( + pattern: #"
(.*?)
"#, + options: [.dotMatchesLineSeparators] + ) else { return html } + + let ns = html as NSString + var result = "" + var cursor = 0 + + let matches = regex.matches(in: html, range: NSRange(location: 0, length: ns.length)) + for match in matches { + // Append verbatim text before this match. + let wholeRange = match.range + result += ns.substring(with: NSRange(location: cursor, length: wholeRange.location - cursor)) + + // Extract language from `class="language-X"` (group 2). When the class attribute is + // entirely absent group 1 and group 2 are both empty. + let language: String? + if match.range(at: 2).location != NSNotFound { + let langRaw = ns.substring(with: match.range(at: 2)) + language = langRaw.isEmpty ? defaultLanguage : langRaw + } else { + language = defaultLanguage + } + + // The already-escaped code content (group 3). + let escapedContent: String + if match.range(at: 3).location != NSNotFound { + escapedContent = ns.substring(with: match.range(at: 3)) + } else { + escapedContent = "" + } + + // Unescape then re-highlight so the tokenizer works on plain text. + let rawContent = HTMLEscaping.unescape(escapedContent) + let highlighted: String + let classAttr: String + if let lang = language?.lowercased().trimmingCharacters(in: .whitespaces), !lang.isEmpty { + highlighted = self.highlight(code: rawContent, language: lang) + classAttr = " class=\"language-\(HTMLEscaping.escape(lang))\"" + } else { + // No language and no default: just re-escape the raw content. + highlighted = HTMLEscaping.escape(rawContent) + classAttr = "" + } + + result += "
\(highlighted)
" + cursor = wholeRange.location + wholeRange.length + } + + result += ns.substring(with: NSRange(location: cursor, length: ns.length - cursor)) + return result + } +} diff --git a/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift new file mode 100644 index 0000000..80b5746 --- /dev/null +++ b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift @@ -0,0 +1,136 @@ +import SiteKit +import SwiftIDEUtils +import SwiftParser +import SwiftSyntax + +/// A SwiftSyntax-based code highlighter that classifies Swift tokens by their syntactic role and +/// emits a distinct `sk-tok-` span per role, for an Xcode-like, semantic-near palette. +/// +/// Compared with the regex `CodeHighlighter` (which only recognizes keywords, strings, comments, +/// numbers, attributes, and capitalized type names), this highlighter additionally distinguishes +/// value references (`stickers` → `variable`, rendered green), function calls (`call`), member +/// accesses (`member`), parameter bindings (`param`), booleans/`nil` (`boolean`), and argument +/// labels (`label`). It does so purely from the parsed syntax tree – no type-checker or symbol +/// graph – so it is fast and error-tolerant on the partial code fragments common in DocC notes. +/// +/// Known limit: the classification is SYNTACTIC, not semantic. It cannot reproduce Xcode's +/// framework-vs-project split (e.g. `ScrollView` vs a project's `StickerListItemView` are both +/// just capitalized type references), so every type reference shares the one `type` role. +/// +/// Non-Swift snippets (and nil/empty languages) are delegated to a fallback highlighter – by +/// default the zero-dependency regex `CodeHighlighter` – so a DocC site can inject this single +/// highlighter and still get reasonable coloring for its Python, shell, or YAML blocks. +public struct SwiftSyntaxHighlighter: CodeHighlighting { + private let fallback: any CodeHighlighting + + /// Creates a SwiftSyntax-based highlighter. + /// + /// - Parameter fallback: The highlighter used for non-Swift snippets and nil/empty languages. + /// Pass nil (the default) to use the zero-dependency regex `CodeHighlighter`. Resolved + /// internally so the default does not reference an internal type across the module boundary. + public init(fallback: (any CodeHighlighting)? = nil) { + self.fallback = fallback ?? CodeHighlighter() + } + + public func highlight(code: String, language: String?) -> String { + guard let language = language?.lowercased().trimmingCharacters(in: .whitespaces), + language == "swift" else { + return self.fallback.highlight(code: code, language: language) + } + return Self.highlightSwift(code) + } + + // MARK: - Swift highlighting + + /// Parses `code`, merges the base syntactic classification with the per-token role refinement, + /// and emits one HTML fragment of escaped, role-tagged spans. + static func highlightSwift(_ code: String) -> String { + let bytes = Array(code.utf8) + let count = bytes.count + guard count > 0 else { return "" } + + let tree = Parser.parse(source: code) + let roleMap = SwiftTokenRoleClassifier.classify(tree) + + var output = "" + var cursor = 0 + + // The classification stream is ordered and non-overlapping. Any byte range it does not cover + // (whitespace, punctuation classified `.none`) is emitted as plain escaped text via the gap + // fill below, so the full source is always reproduced exactly once. + for classified in tree.classifications { + let lower = classified.range.lowerBound.utf8Offset + let upper = classified.range.upperBound.utf8Offset + guard lower < upper, lower >= cursor, upper <= count else { continue } + + if lower > cursor { + output += Self.escapedSlice(bytes, from: cursor, to: lower) + } + + let text = Self.escapedSlice(bytes, from: lower, to: upper) + if let role = Self.role(forKind: classified.kind, offset: lower, bytes: bytes, from: lower, to: upper, roleMap: roleMap) { + output += "\(text)" + } else { + output += text + } + cursor = upper + } + + if cursor < count { + output += Self.escapedSlice(bytes, from: cursor, to: count) + } + return output + } + + /// Resolves the final `sk-tok-*` role class for one classified range: a visitor refinement when + /// present, otherwise a direct mapping of the base `SyntaxClassification`. + private static func role( + forKind kind: SyntaxClassification, + offset: Int, + bytes: [UInt8], + from lower: Int, + to upper: Int, + roleMap: [Int: String] + ) -> String? { + if let refined = roleMap[offset] { + return refined + } + switch kind { + case .keyword, .ifConfigDirective: + return "keyword" + case .type: + return "type" + case .stringLiteral, .regexLiteral: + return "string" + case .integerLiteral, .floatLiteral: + return "number" + case .attribute: + return "attribute" + case .lineComment, .blockComment, .docLineComment, .docBlockComment: + return "comment" + case .operator: + return "operator" + case .argumentLabel: + return "label" + case .dollarIdentifier: + // `$0`, `$1` – anonymous closure arguments, i.e. value references. + return "variable" + case .identifier: + // An identifier the role visitor did not refine. Mirror the regex highlighter's only + // heuristic – a capitalized identifier is a type – and leave anything else uncolored + // rather than guess. + let text = String(decoding: bytes[lower..` span. + private static func escapedSlice(_ bytes: [UInt8], from lower: Int, to upper: Int) -> String { + HTMLEscaping.escape(String(decoding: bytes[lower.. [Int: String] { + let visitor = SwiftTokenRoleClassifier(viewMode: .sourceAccurate) + visitor.walk(tree) + return visitor.roles + } + + // MARK: - Helpers + + private func byteOffset(of token: TokenSyntax) -> Int { + token.positionAfterSkippingLeadingTrivia.utf8Offset + } + + /// Records `role` for `token`. `overwrite: false` only fills an offset the visitor has not yet + /// classified, so a specific parent assignment is never clobbered by the generic fallback. + private func set(_ role: String, at token: TokenSyntax, overwrite: Bool = true) { + let offset = self.byteOffset(of: token) + if overwrite || self.roles[offset] == nil { + self.roles[offset] = role + } + } + + private func isIdentifierToken(_ token: TokenSyntax) -> Bool { + if case .identifier = token.tokenKind { return true } + return false + } + + private func startsUppercased(_ text: String) -> Bool { + guard let first = text.first else { return false } + return first.isUppercase + } + + // MARK: - Calls + + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + // The callee identifies what kind of call this is. A capitalized callee is a type + // initializer (`ScrollView { … }`, `ForEach(…)`, `StickerListItemView(…)`) – we cannot + // tell a framework type from a project type syntactically, so every type reference shares + // the one `type` role. A lowercase callee is a free-function call (`print(…)`). A member + // callee (`view.swipeActions(…)`) is left to the member-access visitor so the member keeps + // its `member` role whether or not it is called. + if let reference = node.calledExpression.as(DeclReferenceExprSyntax.self), + self.isIdentifierToken(reference.baseName) { + let role = self.startsUppercased(reference.baseName.text) ? "type" : "call" + self.set(role, at: reference.baseName) + } + return .visitChildren + } + + // MARK: - Member access + + override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind { + // The member name in `base.member` (or a leading-dot member like `.trailing`). Classified + // `member` regardless of whether it is then called, matching Xcode where `.swipeActions` + // stays the default member color. + let name = node.declName.baseName + if self.isIdentifierToken(name) { + self.set("member", at: name) + } + return .visitChildren + } + + // MARK: - Generic value references + + override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind { + // Reached for every declaration reference. Callees and member names were already assigned + // by their parent above, so only fill the ones still unset: a lowercase reference is a value + // (`stickers`, `sticker`) → the headline green `variable`; a capitalized one is a bare type + // reference (`Color.red`'s `Color`, a metatype) → `type`. + let token = node.baseName + guard self.isIdentifierToken(token) else { return .visitChildren } + let offset = self.byteOffset(of: token) + guard self.roles[offset] == nil else { return .visitChildren } + self.roles[offset] = self.startsUppercased(token.text) ? "type" : "variable" + return .visitChildren + } + + // MARK: - Parameter and binding declarations + + override func visit(_ node: ClosureShorthandParameterSyntax) -> SyntaxVisitorContinueKind { + // The `sticker` in `{ sticker in … }`. + self.set("param", at: node.name) + return .visitChildren + } + + override func visit(_ node: ClosureParameterSyntax) -> SyntaxVisitorContinueKind { + // The name in a typed closure parameter `{ (sticker: Sticker) in … }`. + self.set("param", at: node.firstName) + return .visitChildren + } + + override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { + // The internal binding name of a function parameter (`func add(to list: …)` → `list`). The + // external label (`firstName`) is already classified `argumentLabel` by the base pass. + if let secondName = node.secondName { + self.set("param", at: secondName) + } + return .visitChildren + } + + override func visit(_ node: IdentifierPatternSyntax) -> SyntaxVisitorContinueKind { + // A value binding name: `let name = …`, `for sticker in …`, `case let .some(value)`. Colored + // like a value reference (green) since it names a value the reader will then refer to. + self.set("variable", at: node.identifier) + return .visitChildren + } + + // MARK: - Boolean and nil literals + + override func visit(_ node: BooleanLiteralExprSyntax) -> SyntaxVisitorContinueKind { + // `true` / `false` arrive as keyword tokens; lift them into their own `boolean` role. + self.set("boolean", at: node.literal) + return .visitChildren + } + + override func visit(_ node: NilLiteralExprSyntax) -> SyntaxVisitorContinueKind { + // `nil` is grouped with the booleans per the palette spec. + self.set("boolean", at: node.nilKeyword) + return .visitChildren + } +} diff --git a/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift b/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift new file mode 100644 index 0000000..61d6066 --- /dev/null +++ b/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift @@ -0,0 +1,168 @@ +import Testing +import Foundation +import SiteKit +@testable import SiteKitSyntaxHighlighting + +/// Generates a self-contained HTML preview of the SwiftSyntax highlighter, for eyeballing the token +/// classification and the palette. It renders a representative SwiftUI snippet three ways: BEFORE +/// (the regex highlighter), AFTER with one DISTINCT debug color per role (proves the classification +/// independent of the palette), and AFTER with the shipped Apple/Xcode palette (light + dark, the +/// look that core `docc.css` applies). +/// +/// Not an assertion test – it only writes a file, and only when `SITEKIT_HIGHLIGHTER_PREVIEW_OUT` +/// points at an output path, so a plain `swift test` stays fast. Run it with: +/// +/// ``` +/// SITEKIT_HIGHLIGHTER_PREVIEW_OUT=/tmp/highlighter-preview.html swift test --filter HighlighterPreview +/// ``` +@Suite("HighlighterPreview") +struct HighlighterPreviewGenerator { + /// A representative SwiftUI example exercising every role: types, a green variable reference, a + /// parameter binding, member accesses, argument labels, a boolean, a string, a number, an + /// attribute, an operator, and a comment. + static let sample = """ + struct StickerList: View { + @State private var stickers: [Sticker] = [] + + var body: some View { + ScrollView { + LazyVStack(spacing: 12) { + // Each row keeps its own swipe actions. + ForEach(stickers) { sticker in + StickerListItemView(sticker: sticker) + .swipeActions(edge: .trailing) { + DeleteButton(title: "Delete", isDestructive: true) { + stickers.removeAll { $0.id == sticker.id } + } + } + } + } + .swipeActionsContainer() + } + } + } + """ + + @Test("generate the highlighter preview HTML") + func generate() throws { + guard let outPath = ProcessInfo.processInfo.environment["SITEKIT_HIGHLIGHTER_PREVIEW_OUT"] else { + return + } + let before = CodeHighlighter().highlight(code: Self.sample, language: "swift") + let after = SwiftSyntaxHighlighter().highlight(code: Self.sample, language: "swift") + try Self.document(before: before, after: after).write(toFile: outPath, atomically: true, encoding: .utf8) + print("HIGHLIGHTER_PREVIEW_WROTE \(outPath)") + } + + // MARK: - HTML assembly + + private static func document(before: String, after: String) -> String { + """ + + + + + + SiteKit SwiftSyntax highlighter preview + + + +

SwiftSyntax Swift highlighter preview

+

Each syntactic role is a separate sk-tok-* class, so the palette is a + pure-CSS concern. Variable references (stickers, the sticker usage, + count) classify as variable and render green.

+ +

1 · Classification – each role in a distinct debug color

+

Arbitrary, deliberately-distinct colors so every role is visible; proves the + classification, not the final look. BEFORE is the regex highlighter (capitalized = type only).

+ \(Self.legend) +
+
BEFORE – regex highlighter
+
\(before)
+
AFTER – SwiftSyntax roles
+
\(after)
+
+ +

2 · Shipped Apple/Xcode palette (call = green; alternative is call = type)

+
+
AFTER – light
+
\(after)
+
AFTER – dark
+
\(after)
+
+ + + """ + } + + private static let legend: String = { + let roles: [(String, String)] = [ + ("keyword", "struct, var, in"), ("type", "ScrollView, Sticker"), ("call", "lowercase callee"), + ("variable", "stickers, sticker (use)"), ("member", ".swipeActions, .id"), ("param", "sticker binding"), + ("string", "\"Delete\""), ("number", "12"), ("boolean", "true / nil"), + ("attribute", "@State"), ("comment", "// …"), ("operator", "=="), ("label", "spacing:, edge:"), + ] + let items = roles.map { role, example in + "\(role) \(example)" + }.joined(separator: "\n") + return "
\(items)
" + }() + + private static let css = """ + body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; color: #1d1d1f; background: #fff; } + h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; } + .note { color: #555; max-width: 70ch; line-height: 1.5; } + .legend-var { color: #3C7D3C; font-weight: 700; } + .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; } + @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } } + figure { margin: 0; } figcaption { font-size: .8rem; color: #666; margin-bottom: .3rem; } + pre { margin: 0; padding: 1rem 1.2rem; border-radius: 10px; overflow-x: auto; + font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace; font-size: 13px; line-height: 1.6; } + pre code { display: block; background: none; border: 0; padding: 0; } + .light { background: #f5f5f7; color: #1d1d1f; border: 1px solid #e2e2e6; } + .dark { background: #1f1f24; color: #e6e6ea; border: 1px solid #34343a; } + .legend { display: flex; flex-wrap: wrap; gap: .5rem; padding: .8rem; border-radius: 10px; margin: .6rem 0 1rem; } + .chip { display: inline-flex; align-items: center; gap: .3rem; background: #fff; border: 1px solid #e2e2e6; border-radius: 6px; padding: .15rem .45rem; } + .chip .sw { font-family: ui-monospace, monospace; font-weight: 700; font-size: 12px; } + .chip small { color: #777; } + + /* Panel 1 – distinct debug colors (one per role). */ + .debug .sk-tok-keyword { color: #C026D3; font-weight: 700; } + .debug .sk-tok-type { color: #2563EB; } + .debug .sk-tok-call { color: #EA580C; } + .debug .sk-tok-variable { color: #16A34A; font-weight: 600; } + .debug .sk-tok-member { color: #0D9488; } + .debug .sk-tok-param { color: #CA8A04; } + .debug .sk-tok-string { color: #DC2626; } + .debug .sk-tok-number { color: #7C3AED; } + .debug .sk-tok-boolean { color: #DB2777; } + .debug .sk-tok-attribute { color: #65A30D; } + .debug .sk-tok-comment { color: #6B7280; font-style: italic; } + .debug .sk-tok-operator { color: #0891B2; } + .debug .sk-tok-label { color: #9333EA; } + + /* Panel 2 – the Apple/Xcode palette that core docc.css ships. call = green. */ + .palette.light .sk-tok-keyword { color: #AD3DA4; font-weight: 600; } + .palette.light .sk-tok-type { color: #703DAA; } + .palette.light .sk-tok-call { color: #3C7D3C; } + .palette.light .sk-tok-variable { color: #3C7D3C; } + .palette.light .sk-tok-string { color: #D12F1B; } + .palette.light .sk-tok-number { color: #272AD8; } + .palette.light .sk-tok-boolean { color: #AD3DA4; } + .palette.light .sk-tok-attribute { color: #947100; } + .palette.light .sk-tok-comment { color: #707F8C; font-style: italic; } + /* member / param / operator / label inherit the default text color. */ + + .palette.dark .sk-tok-keyword { color: #FF7AB2; font-weight: 600; } + .palette.dark .sk-tok-type { color: #DABAFF; } + .palette.dark .sk-tok-call { color: #7FD98A; } + .palette.dark .sk-tok-variable { color: #7FD98A; } + .palette.dark .sk-tok-string { color: #FF8170; } + .palette.dark .sk-tok-number { color: #D9C97C; } + .palette.dark .sk-tok-boolean { color: #FF7AB2; } + .palette.dark .sk-tok-attribute { color: #CC9768; } + .palette.dark .sk-tok-comment { color: #7F8C98; font-style: italic; } + """ +} diff --git a/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift new file mode 100644 index 0000000..aa3eb56 --- /dev/null +++ b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift @@ -0,0 +1,180 @@ +import Testing +import SiteKit +@testable import SiteKitSyntaxHighlighting + +@Suite("SwiftSyntaxHighlighter") +struct SwiftSyntaxHighlighterTests { + + private let highlighter = SwiftSyntaxHighlighter() + + /// Convenience: assert that `text` is wrapped in exactly the `sk-tok-` span. + private func expectSpan(_ html: String, role: String, text: String, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(html.contains("\(text)"), "expected \(text) as sk-tok-\(role)", sourceLocation: sourceLocation) + } + + // MARK: - Role classification (the semantic-near roles the regex highlighter cannot produce) + + @Test("A value reference is classified variable (the green-variables headline)") + func variableReferenceIsGreen() { + // `stickers` is a DeclReferenceExpr that is neither a callee nor a member base, so it must be + // classified `variable` (rendered green) – the headline requirement of this whole change. + let code = "ForEach(stickers) { sticker in row(sticker) }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "variable", text: "stickers") + } + + @Test("A capitalized callee is classified type (one class for all type refs)") + func capitalizedCalleeIsType() { + let code = "ScrollView { Text(title) }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "type", text: "ScrollView") + self.expectSpan(result, role: "type", text: "Text") + } + + @Test("A lowercase free-function callee is classified call") + func lowercaseCalleeIsCall() { + let code = "print(message)" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "call", text: "print") + } + + @Test("A member access name is classified member") + func memberAccessIsMember() { + let code = "view.swipeActions(edge: .trailing) { }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "member", text: "swipeActions") + self.expectSpan(result, role: "member", text: "trailing") + } + + @Test("A closure parameter binding is classified param") + func closureParameterIsParam() { + // The binding `sticker` (declaration) is `param`; its later use is `variable`. Both occur, at + // different offsets, so the same name carries two roles depending on position. + let code = "ForEach(stickers) { sticker in StickerRow(sticker) }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "param", text: "sticker") + self.expectSpan(result, role: "variable", text: "sticker") + } + + @Test("Boolean and nil literals are classified boolean") + func booleanAndNilAreBoolean() { + let code = "let flag = true\nlet value = nil" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "boolean", text: "true") + self.expectSpan(result, role: "boolean", text: "nil") + } + + @Test("An argument label is classified label") + func argumentLabelIsLabel() { + let code = "LazyVStack(spacing: 12) { }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "label", text: "spacing") + } + + @Test("A let-binding name is classified variable") + func letBindingIsVariable() { + let code = "let count = 3" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "variable", text: "count") + } + + // MARK: - Roles taken straight from the base SwiftIDEUtils classification + + @Test("Keywords, strings, numbers, comments, and attributes use the base classification") + func baseClassificationRoles() { + let code = """ + // a leading comment + @State private var amount = 42 + let label = "hello" + """ + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "keyword", text: "var") + self.expectSpan(result, role: "keyword", text: "let") + self.expectSpan(result, role: "number", text: "42") + self.expectSpan(result, role: "string", text: ""hello"") + self.expectSpan(result, role: "comment", text: "// a leading comment") + self.expectSpan(result, role: "attribute", text: "@State") + } + + // MARK: - Fragment tolerance + + @Test("A partial fragment (no enclosing type) is classified without crashing") + func fragmentToleranceDegradesGracefully() { + // DocC code blocks are routinely partial – a bare statement list, an elided body. SwiftParser + // is error-tolerant and still yields a tree, so classification must produce best-effort roles + // (here the green variable) rather than throwing or returning empty. + let code = "ForEach(stickers) { sticker in\n StickerListItemView(sticker)\n .swipeActions" + let result = self.highlighter.highlight(code: code, language: "swift") + #expect(!result.isEmpty) + self.expectSpan(result, role: "variable", text: "stickers") + self.expectSpan(result, role: "type", text: "StickerListItemView") + } + + @Test("Empty input returns empty output") + func emptyInput() { + #expect(self.highlighter.highlight(code: "", language: "swift") == "") + } + + // MARK: - HTML escaping safety + + @Test("Angle brackets and ampersands are escaped exactly once") + func htmlEscaping() { + let code = "let a: Array = []\nlet b = x && y" + let result = self.highlighter.highlight(code: code, language: "swift") + #expect(result.contains("<")) + #expect(result.contains(">")) + #expect(result.contains("&")) + #expect(!result.contains("&amp;")) + #expect(!result.contains("&lt;")) + } + + @Test("Reproduces the full source text once spans are stripped") + func reproducesSourceExactly() { + // No byte of the source may be dropped or duplicated: stripping every span tag and unescaping + // must return the original code verbatim (the gap-fill emits untouched regions as plain text). + let code = "struct S {\n var n = 1 // note\n func f() { print(n) }\n}" + let result = self.highlighter.highlight(code: code, language: "swift") + let stripped = result + .replacing(try! Regex(""), with: "") + .replacing("", with: "") + #expect(HTMLEscaping.unescape(stripped) == code) + } + + // MARK: - Non-Swift fallback + + @Test("Non-Swift languages delegate to the regex fallback") + func nonSwiftDelegatesToFallback() { + let code = "def greet():\n return None" + let viaSyntax = self.highlighter.highlight(code: code, language: "python") + let viaRegex = CodeHighlighter().highlight(code: code, language: "python") + #expect(viaSyntax == viaRegex) + } + + @Test("A nil language delegates to the fallback (plain escaped text)") + func nilLanguageDelegatesToFallback() { + let code = "let x = 1" + let viaSyntax = self.highlighter.highlight(code: code, language: nil) + let viaRegex = CodeHighlighter().highlight(code: code, language: nil) + #expect(viaSyntax == viaRegex) + #expect(!viaSyntax.contains("ForEach(stickers) { s in row(s) }
" + let result = self.highlighter.applyToBodyHTML(html, defaultLanguage: nil) + #expect(result.contains("sk-docc-highlight")) + self.expectSpan(result, role: "variable", text: "stickers") + } + + @Test("applyToBodyHTML keeps a // inside a pre-escaped string out of comments") + func applyToBodyHTMLStringWithSlashes() { + // DocC hands already-escaped content; the parser must treat the URL's // as part of the string. + let html = "
let u = "https://x"
" + let result = self.highlighter.applyToBodyHTML(html, defaultLanguage: nil) + #expect(result.contains("sk-tok-string")) + #expect(!result.contains("sk-tok-comment")) + } +} From 4e090752437d8221301a757ce1d27e4c02513382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 15:20:38 +0200 Subject: [PATCH 3/7] Split framework and project types in the Swift DocC highlighter The SwiftSyntax highlighter colored every capitalized type with the one purple `type` role, so an SDK type and a project's own type looked alike. Xcode instead reads SDK types (ScrollView, ForEach) purple and a project's own types (StickerListItemView, DeleteButton) green. Approximate that split with a committed framework-type allowlist, since isolated DocC snippets carry no symbol graph. A capitalized type whose name is in the allowlist keeps `sk-tok-type`; any other capitalized type becomes the new `sk-tok-projecttype` role (green). The split is applied at every point the type role is assigned (the expression visitors for initializers and bare references, and the base classification for type annotations), so type-position types are split too. The allowlist is a committed Swift literal (1755 names) parsed once into a Set, never re-extracted at build time, so the coloring is reproducible across machines, CI, and consumers regardless of the installed SDK. It covers SwiftUI, SwiftUICore, Foundation, Combine, Swift stdlib and Concurrency, plus curated UIKit, AppKit, and CoreGraphics types so SwiftUI-interop names like UIView and CGRect read as framework types. Sites can extend the set with a new non-breaking init parameter, SwiftSyntaxHighlighter(additionalFrameworkTypes:), to color their own umbrella or design-system types like the SDK. The core docc.css gains a placeholder green for the new role in light and dark. --- Sources/SiteKit/Resources/DocC/docc.css | 15 +- .../FrameworkTypeAllowlist.swift | 1799 +++++++++++++++++ .../SwiftSyntaxHighlighter.swift | 48 +- .../SwiftTokenRoleClassifier.swift | 42 +- .../HighlighterPreviewGenerator.swift | 38 +- .../SwiftSyntaxHighlighterTests.swift | 69 +- 6 files changed, 1966 insertions(+), 45 deletions(-) create mode 100644 Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift diff --git a/Sources/SiteKit/Resources/DocC/docc.css b/Sources/SiteKit/Resources/DocC/docc.css index 11abbb1..9b3a3d7 100644 --- a/Sources/SiteKit/Resources/DocC/docc.css +++ b/Sources/SiteKit/Resources/DocC/docc.css @@ -4138,12 +4138,20 @@ body.sk-docc-shell-body { font-weight: 600; } -/* Type references: purple. All type references share this one colour – a syntactic highlighter cannot - reproduce Xcode's framework-vs-project split, so `ScrollView` and a project's own type look alike. */ +/* Framework / known type references: purple. `sk-tok-type` now means a capitalized type whose name is + in the highlighter's committed framework allowlist (SDK + curated UIKit/AppKit/CoreGraphics/stdlib), + so `ScrollView` reads purple while a project's own type goes green via sk-tok-projecttype below. */ .sk-docc-highlight .sk-tok-type { color: #703DAA; } +/* Project (non-framework) type references: green – approximates Xcode, where a project's own + `StickerListItemView` reads green while SDK types read purple. Placeholder green (matches the + variable green for now); the exact, distinct-from-variable green is finalised in the colour pass. */ +.sk-docc-highlight .sk-tok-projecttype { + color: #3C7D3C; +} + /* Calls: seeded green for "more colour" (it greens project initializers like `StickerListItemView`); flip to the type purple for the cleaner look that keeps framework `ForEach` from going green – a CSS-only change, no Swift rebuild. */ @@ -4186,6 +4194,9 @@ body.sk-docc-shell-body { [data-theme="dark"] .sk-docc-highlight .sk-tok-type { color: #DABAFF; } +[data-theme="dark"] .sk-docc-highlight .sk-tok-projecttype { + color: #7FD98A; +} [data-theme="dark"] .sk-docc-highlight .sk-tok-call { color: #7FD98A; } diff --git a/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift b/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift new file mode 100644 index 0000000..31db577 --- /dev/null +++ b/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift @@ -0,0 +1,1799 @@ +import Foundation + +/// The committed framework-type allowlist that drives the highlighter's framework-vs-project split. +/// +/// A capitalized identifier the classifier would mark as a type is looked up here: a name in this set +/// is a framework / known type and keeps `sk-tok-type` (purple); any other capitalized type is a +/// project-defined type and becomes `sk-tok-projecttype` (green). This mirrors Xcode, where SDK types +/// like `ScrollView` read purple while a project's own `StickerListItemView` reads green. +/// +/// Provenance: extracted once OFFLINE from the installed SDK module `.swiftinterface` public +/// type-declarations (SwiftUI + SwiftUICore, where `View` / `Text` / `ForEach` actually live, plus +/// Foundation and Combine), unioned with a curated set of common Swift stdlib / Concurrency, UIKit, +/// AppKit, and CoreGraphics types (which the SwiftUI/Foundation extraction does not cover, so +/// `UIView` / `CGRect` would otherwise be misclassified as project types in SwiftUI-interop +/// snippets). The list is COMMITTED and never re-extracted at build time, so the coloring is +/// reproducible across machines, CI, and consumers regardless of the SDK installed there. Regenerate +/// it when bumping the supported SDK / toolchain. +enum FrameworkTypeAllowlist { + /// The framework / known-type names. A capitalized type token whose name is in this set renders as + /// `sk-tok-type`; every other capitalized type renders as `sk-tok-projecttype`. Built once from + /// `rawList`, so callers pay the parse cost a single time per process. + static let frameworkTypeNames: Set = Set( + Self.rawList + .split(whereSeparator: \.isNewline) + .lazy + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + ) + + /// The role class for a capitalized type `name`, given the effective framework set in play (the + /// committed `frameworkTypeNames`, optionally unioned with a highlighter's `additionalFrameworkTypes`): + /// `type` for a framework / known type (name in the set), `projecttype` for any other + /// (project-defined) type. The single decision point for the framework-vs-project split, so the two + /// assignment sites (the role classifier's expression visitors and the base-classification mapping) + /// can never disagree on the role strings. + static func role(forTypeName name: String, in frameworkTypes: Set) -> String { + frameworkTypes.contains(name) ? "type" : "projecttype" + } + + /// One framework type per line. Kept as a single multi-line string literal (parsed once into the + /// set above) to avoid both a 1700+-element array-literal compile cost and any resource bundling. + private static let rawList = """ +AccessibilityActionCategory +AccessibilityActionKind +AccessibilityAdjustmentDirection +AccessibilityAttachmentModifier +AccessibilityChildBehavior +AccessibilityCustomContentKey +AccessibilityDirectTouchOptions +AccessibilityFocusState +AccessibilityHeadingLevel +AccessibilityLabeledPairRole +AccessibilityQuickActionOutlineStyle +AccessibilityQuickActionPromptStyle +AccessibilityQuickActionStyle +AccessibilityRotorContent +AccessibilityRotorContentBuilder +AccessibilityRotorEntry +AccessibilitySystemRotor +AccessibilityTechnologies +AccessibilityTextContentType +AccessibilityTraits +AccessibilityZoomGestureAction +AccessoryBarActionButtonStyle +AccessoryBarButtonStyle +AccessoryCircularCapacityGaugeStyle +AccessoryCircularGaugeStyle +AccessoryLinearCapacityGaugeStyle +AccessoryLinearGaugeStyle +Actions +ActionSheet +Actor +ActorType +AdaptableTabBarPlacement +AdaptiveImageGlyphAttribute +AffineTransform +AgreementArgumentAttribute +AgreementConceptAttribute +Alert +AlertScene +AligningContentProviderLayout +Alignment +Alignment3D +AlignmentID +AlignmentKey +AlignmentStrategy +AllCases +AllSatisfy +AlternateDescriptionAttribute +AlternatingRowBackgroundBehavior +AMPMStyle +Anchor +AnchoredLayout +AnchoredRelativeFormatStyle +Angle +Angle2D +AngularGradient +Animatable +AnimatableData +AnimatableModifier +AnimatablePair +AnimatableValues +Animation +AnimationCompletionCriteria +AnimationContext +AnimationState +AnimationStateKey +AnimationTimelineSchedule +Any +AnyCancellable +AnyCompositorContent +AnyDefinition +AnyGesture +AnyGradient +AnyHashable +AnyKeyPath +AnyLayout +AnyLocation +AnyLocationBase +AnyObject +AnyPublisher +AnyScrollTargetBehavior +AnyShape +AnyShapeStyle +AnySubscriber +AnyTabContent +AnyTransition +AnyView +App +AppStorage +Argument +Arithmetic +ArithmeticOperator +Array +ArrayLiteralElement +ArraySlice +AssertNoFailure +Assign +AssistiveAccess +AsymmetricTransition +AsyncBytes +AsyncCharacterSequence +AsyncImage +AsyncImagePhase +AsyncIterator +AsyncIteratorProtocol +AsyncLineSequence +AsyncMessage +AsyncPublisher +AsyncSequence +AsyncStream +AsyncThrowingPublisher +AsyncThrowingStream +AsyncUnicodeScalarSequence +AttributeContainer +AttributeContainerProxy +Attributed +AttributedString +AttributedStringAttributeMutation +AttributedStringKey +AttributedStringProtocol +AttributedStyle +AttributedSubstring +AttributedTextFormatting +AttributedTextFormattingDefinition +AttributedTextSelection +AttributedTextValueConstraint +AttributeDynamicLookup +AttributeInvalidationCondition +AttributeMergePolicy +AttributeRunBoundaries +Attributes +AttributeScope +AttributeScopeCodableConfiguration +AttributeScopes +AttributesSlice1 +AttributesSlice2 +AttributesSlice3 +AttributesSlice4 +AttributesSlice5 +Attribution +Autoconnect +AutomaticControlGroupStyle +AutomaticDisclosureGroupStyle +AutomaticFormStyle +AutomaticHoverEffect +AutomaticImmersionStyle +AutomaticLabeledContentStyle +AutomaticMenuBarExtraStyle +AutomaticNavigationSplitViewStyle +AutomaticNavigationTransition +AutomaticPresentationSizing +AutomaticTableStyle +AutomaticTextEditorStyle +AXChartDescriptorRepresentable +Axis +BackgroundColorAttribute +BackgroundDisplayMode +BackgroundProminence +BackgroundStyle +BackgroundTask +BackgroundTaskCancelledReason +BadgeProminence +BalancedNavigationSplitViewStyle +Base64DecodingOptions +Base64EncodingOptions +BaselineOffsetAttribute +BaseMessageIdentifier +Behavior +BezierPoint +BidirectionalCollection +Bindable +Binding +BlendMode +BlockOperation +BlurOptions +BlurReplaceTransition +Body +BookmarkCreationOptions +BookmarkResolutionOptions +Bool +BooleanLiteralType +BorderedButtonMenuStyle +BorderedButtonStyle +BorderedListStyle +BorderedProminentButtonStyle +BorderedTableStyle +BorderlessButtonMenuButtonStyle +BorderlessButtonMenuStyle +BorderlessButtonStyle +BorderlessPullDownMenuButtonStyle +Breakpoint +Buffer +BufferingStrategy +Builder +Bundle +BundleDescription +Button +ButtonBorderShape +ButtonMenuStyle +ButtonRepeatBehavior +ButtonRole +ButtonSizing +ButtonStyle +ButtonStyleConfiguration +ButtonToggleStyle +ByteCount +ByteCountAttribute +ByteCountFormatStyle +ByteCountFormatter +Cache +CachePolicy +Cadence +CalculationError +Calendar +CalendarDayChangedMessage +Cancellable +Canvas +CapitalizationContext +Capsule +CardButtonStyle +CarouselListStyle +CarouselTabViewStyle +Case +CaseIterable +Catch +Category +CGAffineTransform +CGColor +CGContext +CGFloat +CGGradient +CGImage +CGMutablePath +CGPath +CGPoint +CGRect +CGSize +CGVector +Character +CharacterIndex +CharacterSet +CharacterView +CheckboxToggleStyle +CheckedContinuation +CheckpointMessage +Children +Chirality +Circle +CircularGaugeStyle +CircularProgressViewStyle +ClipOptions +Clock +ClosedRange +CocoaError +Codable +CodableAttributedStringKey +CodableConfiguration +CodableRepresentation +CodableWithConfiguration +Code +CodingKey +Collation +Collect +CollectByCount +CollectByTime +Collection +CollectionContainsCollection +CollectionIndexSubscript +CollectionRangeSubscript +Color +ColorMatrix +ColorPicker +ColorRenderingMode +Colors +ColorScheme +ColorSchemeContrast +ColorSpace +ColumnNavigationViewStyle +ColumnsFormStyle +CombineIdentifier +CombineLatest +CombineLatest3 +CombineLatest4 +CommandGroup +CommandGroupPlacement +CommandMenu +Commands +CommandsBuilder +CommonKeyPathKind +CompactDatePickerStyle +CompactMap +CompactMenuControlGroupStyle +Comparable +ComparableComparator +Comparator +Compared +CompareOptions +Comparison +ComparisonOperator +Completion +Component +ComponentDisplayOption +ComponentParseStrategy +Components +ComponentsFormatStyle +CompositorContent +CompositorContentBuilder +Concatenate +ConcentricRectangle +Conditional +ConditionalCast +Configuration +Conjunction +ConnectablePublisher +ConnectionAcceptedMessage +ContainerBackgroundPlacement +ContainerRelativeShape +ContainerSizingOptions +ContainerValueKey +ContainerValues +Contains +ContainsWhere +Content +ContentHoverEffect +ContentMarginPlacement +ContentMode +ContentOffset +ContentShapeKinds +ContentSizeCategory +ContentToolbarPlacement +ContentTransition +ContentUnavailableView +Context +ContextMenu +ContiguousArray +ContiguousBytes +ContinuousClock +ControlActiveState +ControlGroup +ControlGroupStyle +ControlGroupStyleConfiguration +ControlSize +ControlWidget +ControlWidgetConfiguration +ControlWidgetConfigurationBuilder +ControlWidgetTemplate +ControlWidgetTemplateBuilder +CookiesChangedMessage +CoordinateSpace +CoordinateSpaceProtocol +Corner +Count +CubicKeyframe +Currency +CurrencyFormatStyleConfiguration +CurrentLocaleDidChangeMessage +CurrentValueLabel +CurrentValueSubject +CustomAnimation +CustomCombineIdentifierConvertible +CustomDebugStringConvertible +CustomHoverEffect +CustomizableToolbarContent +CustomLocalizedStringResourceConvertible +CustomNSError +CustomPresentationDetent +CustomPronoun +CustomStringConvertible +CyclicYear +Data +DataAvailableMessage +DataDecodingStrategy +DataEncodingStrategy +DataProtocol +DataTaskPublisher +Date +DateComponents +DateComponentsFormatter +DateDecodingStrategy +DateEncodingStrategy +DateFieldAttribute +DateFormatter +DateInterval +DateOffset +DatePicker +DatePickerComponents +DatePickerStyle +DatePickerStyleConfiguration +DateReference +DateSeparator +DateStyle +DateTimeSeparator +Day +DayOfYear +DayPeriod +Deallocator +Debounce +DebugReplaceableView +Decimal +DecimalSeparatorDisplayStrategy +Decodable +DecodableAttributedStringKey +DecodableWithConfiguration +Decode +Decoder +DecodingConfiguration +DecodingConfigurationProviding +DefaultButtonLabel +DefaultButtonStyle +DefaultDatePickerStyle +DefaultDateProgressLabel +DefaultDocumentGroupLaunchActions +DefaultFocusEvaluationPriority +DefaultGaugeStyle +DefaultGlassEffectShape +DefaultGroupBoxStyle +DefaultLabelStyle +DefaultListStyle +DefaultMenuButtonStyle +DefaultMenuStyle +DefaultNavigationViewStyle +DefaultPickerStyle +DefaultProgressViewStyle +DefaultSettingsLinkLabel +DefaultShareLinkLabel +DefaultTabLabel +DefaultTabViewStyle +DefaultTextFieldStyle +DefaultToggleStyle +DefaultToolbarItem +DefaultWindowStyle +DefaultWindowToolbarStyle +DefaultWindowVisibilityToggleLabel +Deferred +Definiteness +DefinitionBuilder +Delay +Demand +DepthAlignment +DepthAlignmentID +DepthAlignmentKey +Description +DescriptiveNumberFormatConfiguration +Design +Determination +DialogSeverity +Dictionary +DictionaryKeyDefaultValueSubscript +DictionaryKeySubscript +DidBecomeActiveMessage +DidBecomeInvalidMessage +DidChangeMessage +DidCloseUndoGroupMessage +DidEnterBackgroundMessage +DidFinishGatheringMessage +DidLoadMessage +DidOpenUndoGroupMessage +DidRedoChangeMessage +DidStartGatheringMessage +DidTerminateMessage +DidUndoChangeMessage +DigitalCrownEvent +DigitalCrownRotationalSensitivity +Direction +DirectoryHint +DisabledTextSelectability +DisclosureGroup +DisclosureGroupStyle +DisclosureGroupStyleConfiguration +DisclosureTableRow +DiscontiguousAttributedSubstring +DiscreteFormatStyle +Disjunction +DismissAction +DismissBehavior +DismissImmersiveSpaceAction +DismissSearchAction +DismissWindowAction +DispatchGroup +DispatchQueue +DispatchSemaphore +DispatchTime +DispatchWorkItem +DisplayProxy +Divider +DocumentBaseBox +DocumentConfiguration +DocumentGroup +DocumentGroupLaunchScene +DocumentLaunchGeometryProxy +DocumentLaunchView +Double +DoubleColumnNavigationViewStyle +DragConfiguration +DragDropPreviewsFormation +DragGesture +DragSession +DrawingOptions +Drop +DropConfiguration +DropDelegate +DropInfo +DropOperation +DropProposal +DropSession +DropUntilOutput +DropWhile +Duration +DurationFieldAttribute +DynamicProperty +DynamicRange +DynamicTableRowContent +DynamicTypeSize +DynamicViewContent +Edge +EdgeInsets +EditableCollectionContent +EditActions +EditButton +EditMode +Element +Ellipse +EllipticalGradient +EllipticalListStyle +Empty +EmptyAnimatableData +EmptyCommands +EmptyControlWidgetConfiguration +EmptyControlWidgetTemplate +EmptyDefinition +EmptyHoverEffect +EmptyHoverEffectContent +EmptyMatchedTransitionSourceConfiguration +EmptyModifier +EmptyTableRowContent +EmptyView +EmptyVisualEffect +EmptyWidgetConfiguration +EnabledTextSelectability +Encodable +EncodableAttributedStringKey +EncodableWithConfiguration +Encode +Encoder +Encoding +EncodingConfiguration +EncodingConfigurationProviding +EncodingConversionOptions +End +Entries +EnumeratedSequence +EnumerationOptions +Environment +EnvironmentalModifier +EnvironmentKey +EnvironmentObject +EnvironmentValues +Equal +Equatable +EquatableView +Era +Error +ErrorPointer +ErrorUserInfoKey +Event +EventModifiers +EveryMinuteTimelineSchedule +ExclusiveGesture +ExpandedWindowToolbarStyle +ExplicitTimelineSchedule +ExpressibleByArrayLiteral +ExpressibleByDictionaryLiteral +ExpressibleByIntegerLiteral +ExpressibleByStringLiteral +Expression +ExpressionEvaluate +ExtendedGraphemeClusterLiteralType +Fail +Failure +FailurePolicy +FetchedResults +FetchRequest +Field +FieldDatePickerStyle +FileDialogBrowserOptions +FileDocument +FileDocumentConfiguration +FileDocumentReadConfiguration +FileDocumentWriteConfiguration +FileHandle +FileManager +FileWrapper +FillShapeStyle +FillShapeView +FillStyle +Filter +FilterOptions +FindContext +First +FirstWhere +FittedPresentationSizing +FixedTextVariant +FlatMap +Flexibility +Float +Float16 +FloatDivision +FloatingPointFormatStyle +FloatingPointParseStrategy +FloatLiteralType +FocusedBinding +FocusedObject +FocusedValue +FocusedValueKey +FocusedValues +FocusInteractions +FocusState +Font +FontAttribute +ForceCast +ForcedUnwrap +ForEach +ForEachSectionCollection +ForEachSubviewCollection +ForegroundColorAttribute +ForegroundStyle +Form +FormatInput +FormatOutput +FormatString +FormatStyle +FormatStyleCapitalizationContext +FormattingOptions +FormPresentationSizing +FormStyle +FormStyleConfiguration +FoundationAttributes +FractionalPartDisplayStrategy +FrameResizeDirection +FrameResizePosition +Frequency +FullImmersionStyle +Future +Gauge +GaugeStyle +GaugeStyleConfiguration +GeometryEffect +GeometryProxy +GeometryReader +GeometryReader3D +Gesture +GestureMask +GestureState +GestureStateGesture +Glass +GlassButtonStyle +GlassEffectContainer +GlassEffectTransition +GlassProminentButtonStyle +GlobalActor +GlobalCoordinateSpace +Gradient +GradientOptions +GrammaticalCase +GrammaticalGender +GrammaticalNumber +GrammaticalPerson +GraphicalDatePickerStyle +GraphicsContext +Grid +GridItem +GridLayout +GridRow +Group +GroupBox +GroupBoxStyle +GroupBoxStyleConfiguration +GroupedFormStyle +GroupedListStyle +GroupedTabViewStyle +GroupElementsOfContent +GroupHoverEffect +Grouping +GroupSectionsOfContent +HandGestureShortcut +HandleEvents +Hashable +Hasher +HelpLink +HiddenTitleBarWindowStyle +HierarchicalShapeStyle +HierarchicalShapeStyleModifier +HighlightHoverEffect +HorizontalAlignment +HorizontalDirection +HorizontalEdge +HostDisplayOption +HostingSheetRepresentation +Hour +HourCycle +HoverEffect +HoverEffectContent +HoverEffectGroup +HoverEffectPhaseOverride +HoverPhase +HSplitView +HStack +HStackLayout +HTTPFormatStyle +HTTPURLResponse +Icon +IconOnlyLabelStyle +ID +Identifiable +Identifier +IdentifierType +IdentityTransition +IgnoreOutput +Image +ImagePaint +ImageRenderer +ImageURLAttribute +ImmediateScheduler +ImmersionChangeContext +ImmersionStyle +ImmersiveContentBrightness +ImmersiveEnvironmentBehavior +ImmersiveSpace +ImmersiveSpaceContent +ImmersiveSpaceContentBuilder +ImmersiveSpaceViewContent +ImportFromDevicesCommands +Index +IndexDisplayMode +IndexedIdentifierCollection +IndexPath +IndexSet +IndexViewStyle +Indices +InflectionAlternativeAttribute +InflectionConcept +InflectionRule +InflectionRuleAttribute +InlinePickerStyle +InlinePresentationIntentAttribute +Input +InputDevicePose +InsetGroupedListStyle +InsetListStyle +InsetShape +InsettableShape +InsetTableStyle +InspectorCommands +Instant +InstantProtocol +Int +Int16 +Int32 +Int64 +Int8 +IntDivision +IntegerFormatStyle +IntegerLiteralType +IntegerParseStrategy +IntentType +InterfaceOrientation +Interpolation +InterpolationOptions +InterpretedSyntax +IntervalFormatStyle +IntRemainder +ISO8601DateFormatter +ISO8601FormatStyle +ItemProviderTableRowModifier +Iterator +IteratorProtocol +JSONDecoder +JSONEncoder +JSONSerialization +Just +KerningAttribute +Key +KeyboardShortcut +KeyDecodingStrategy +KeyEncodingStrategy +KeyEquivalent +Keyframe +KeyframeAnimator +Keyframes +KeyframesBuilder +KeyframeTimeline +KeyframeTrack +KeyframeTrackContent +KeyframeTrackContentBuilder +KeyPath +KeyPathComparator +KeyPress +KeyValueObservingPublisher +Kind +Label +LabeledContent +LabeledContentStyle +LabeledContentStyleConfiguration +LabeledControlGroupContent +LabeledToolbarItemGroupContent +LabelStyle +LabelStyleConfiguration +Language +LanguageCode +LanguageDirection +LanguageIdentifierAttribute +Last +LastWhere +Layout +LayoutDirection +LayoutDirectionBehavior +LayoutKey +LayoutProperties +LayoutSubview +LayoutSubviews +LayoutValueKey +LazyFilterSequence +LazyHGrid +LazyHStack +LazyMapSequence +LazySequence +LazyVGrid +LazyVStack +Leading +LegibilityWeight +LiftHoverEffect +LimitBehavior +LimitedAvailabilityConfiguration +Line +LinearCapacityGaugeStyle +LinearGaugeStyle +LinearGradient +LinearKeyframe +LinearProgressViewStyle +LineStyle +Link +LinkAttribute +LinkButtonStyle +LinkShapeStyle +List +ListFormatStyle +ListItemDelimiterAttribute +ListItemTint +ListSectionSpacing +ListStyle +ListType +LocalCoordinateSpace +Locale +Localization +LocalizationOptions +LocalizationValue +LocalizedDateArgumentAttribute +LocalizedDateIntervalArgumentAttribute +LocalizedError +LocalizedNumberFormatAttribute +LocalizedNumericArgumentAttribute +LocalizedStringArgumentAttributes +LocalizedStringKey +LocalizedStringResource +LocalizedURLArgumentAttribute +LocalSession +Locations +Logger +LongPressGesture +LosslessStringConvertible +LowDiskSpaceMessage +MachError +MagnificationGesture +MagnifyGesture +Magnitude +Main +MainActor +MainActorMessage +MakeConnectable +ManagedBuffer +Map +MapError +MapKeyPath +MapKeyPath2 +MapKeyPath3 +MarkdownDecodableAttributedStringKey +MarkdownParsingOptions +MarkdownSourcePosition +MarkdownSourcePositionAttribute +MarkedValueLabel +MatchedGeometryEffect +MatchedGeometryProperties +MatchedTransitionSourceConfiguration +MatchingPolicy +Material +MaterialActiveAppearance +MaximumValueLabel +MeasureInterval +Measurement +MeasurementAttribute +MeasurementFormatUnitUsage +MeasurementSystem +Menu +MenuActionDismissBehavior +MenuBarExtra +MenuBarExtraStyle +MenuButton +MenuButtonStyle +MenuControlGroupStyle +MenuOrder +MenuPickerStyle +MenuStyle +MenuStyleConfiguration +Merge +Merge3 +Merge4 +Merge5 +Merge6 +Merge7 +Merge8 +MergeMany +MeshGradient +Message +MessageIdentifier +MinimumValueLabel +Minute +Mirror +MixedImmersionStyle +ModifiedContent +Month +Morphology +MorphologyAttribute +MoveCommandDirection +MoveKeyframe +MoveTransition +Multicast +MultiDatePicker +MultiViewRoot +MutableCollection +MutableDataProtocol +MutableURLRequest +Name +NamedCoordinateSpace +Namespace +NameStyle +NavigationBarDrawerDisplayMode +NavigationBarItem +NavigationControlGroupStyle +NavigationLink +NavigationLinkPickerStyle +NavigationPath +NavigationSplitView +NavigationSplitViewColumn +NavigationSplitViewStyle +NavigationSplitViewStyleConfiguration +NavigationSplitViewVisibility +NavigationStack +NavigationTransition +NavigationView +NavigationViewStyle +Negation +NetworkServiceType +NetworkUnavailableReason +Never +NewDocumentAction +NewDocumentButton +NilCoalesce +NilLiteral +NonConformingFloatDecodingStrategy +NonConformingFloatEncodingStrategy +Notation +NotEqual +Notification +NotificationCenter +Notifications +NSApplicationDelegateAdaptor +NSAttributedString +NSButton +NSColor +NSError +NSErrorPointer +NSFastEnumerationIterator +NSFont +NSGestureRecognizerRepresentable +NSGestureRecognizerRepresentableContext +NSGestureRecognizerRepresentableCoordinateSpaceConverter +NSHostingController +NSHostingMenu +NSHostingSceneBridgingOptions +NSHostingSceneRepresentation +NSHostingSizingOptions +NSHostingView +NSImage +NSIndexSetIterator +NSKeyValueObservation +NSKeyValueObservedChange +NSKeyValueObservingCustomization +NSNumber +NSObject +NSPredicate +NSRange +NSScrollView +NSSortDescriptor +NSStackView +NSString +NSTableView +NSTextField +NSView +NSViewController +NSViewControllerRepresentable +NSViewControllerRepresentableContext +NSViewRepresentable +NSViewRepresentableContext +NSWindow +NumberFormatAttributes +NumberFormatStyleConfiguration +NumberFormatter +NumberingSystem +NumberPart +NumberPartAttribute +NumberRepresentation +ObjectIdentifier +ObjectiveCConvertibleAttributedStringKey +ObjectiveCValue +ObjectWillChangePublisher +Observable +ObservableObject +ObservableObjectPublisher +ObservationRegistrar +ObservationToken +ObservedObject +OffsetShape +OffsetTransition +OnInsertTableRowModifier +OpacityTransition +OpaquePointer +OpenDocumentAction +OpenImmersiveSpaceAction +OpenSettingsAction +OpenURLAction +OpenWindowAction +Operation +OperationQueue +OperationsOutsideApp +OperationsWithinApp +Optional +OptionalFlatMap +OptionSet +Orientation +OrnamentAttachmentAnchor +OutlineGroup +OutlineSubgroupChildren +Output +OutputFormatting +PageIndexViewStyle +PagePresentationSizing +PageTabViewStyle +PagingScrollTargetBehavior +PaletteControlGroupStyle +PalettePickerStyle +PaletteSelectionEffect +ParseableFormatStyle +ParseInput +ParseOutput +ParseStrategy +PartialKeyPath +PartialRangeFrom +PartialRangeThrough +PartialRangeUpTo +PartOfSpeech +PassthroughSubject +PasteButton +Path +Pattern +PencilDoubleTapGestureValue +PencilHoverPose +PencilPreferredAction +PencilSqueezeGesturePhase +PencilSqueezeGestureValue +Percent +PeriodicTimelineSchedule +PersonNameComponentAttribute +PersonNameComponents +Phase +PhaseAnimator +Phases +PhysicalMetric +Picker +PickerStyle +PinnedScrollableViews +Pipe +Placeholder +PlaceholderContentView +PlaceholderTextShapeStyle +PlainButtonStyle +PlainListStyle +PlainTextEditorStyle +PlainTextFieldStyle +PlainWindowStyle +PointerStyle +PopoverAttachmentAnchor +PopUpButtonPickerStyle +Position +POSIXError +PowerStateDidChangeMessage +Precision +Predicate +PredicateBindings +PredicateCodableConfiguration +PredicateCodableKeyPathProviding +PredicateError +PredicateEvaluate +PredicateExpression +PredicateExpressions +PredicateRegex +PreferenceKey +PreferredColorSchemeKey +PrefetchStrategy +PrefixUntilOutput +PrefixWhile +Presentation +PresentationAdaptation +PresentationBackgroundInteraction +PresentationContentInteraction +PresentationDetent +PresentationIntent +PresentationIntentAttribute +PresentationMode +PresentationSizing +PresentationSizingContext +PresentationSizingRoot +PresentedWindowContent +PressFeedback +PreviewContext +PreviewContextKey +PreviewDevice +PreviewModifier +PreviewModifierContent +PreviewPlatform +PreviewProvider +PrimitiveButtonStyle +PrimitiveButtonStyleConfiguration +Print +Process +ProcessInfo +Progress +ProgressiveImmersionAspectRatio +ProgressiveImmersionStyle +ProgressView +ProgressViewStyle +ProgressViewStyleConfiguration +ProjectionTransform +Prominence +ProminentDetailNavigationSplitViewStyle +Promise +Pronoun +PronounType +Properties +Property +PropertyListDecoder +PropertyListEncoder +PropertyListFormat +PropertyListSerialization +ProposedViewSize +Published +Publisher +Publishers +PullDownButton +PullDownMenuBarExtraStyle +PullDownMenuButtonStyle +PushTransition +PushWindowAction +Quarter +RadialGradient +RadioGroupPickerStyle +RandomAccessCollection +Range +RangeExpressionContains +RangeReplaceableCollection +RangeView +RasterizationOptions +RawRepresentable +RawValue +ReadCompletionMessage +ReadingOptions +ReadToEndOfFileCompletionMessage +ReceiveOn +Record +Recording +RecoverableError +Rectangle +RectangleCornerInsets +RectangleCornerRadii +RecurrenceRule +RedactionReasons +Reduce +ReferenceConvertible +ReferenceFileDocument +ReferenceFileDocumentConfiguration +ReferenceType +ReferenceWritableKeyPath +ReferentConceptAttribute +RefreshAction +RegexOutput +Region +Regions +RelativeDateTimeFormatter +RelativeFormatStyle +ReleaseFeedback +RemoteDeviceIdentifier +RemoteImmersiveSpace +RemoveDuplicates +RenameAction +RenameButton +Renderer +Repeated +RepeatedTimePolicy +ReplaceEmpty +ReplaceError +ReplacementIndexAttribute +Representation +ResetFocusAction +ResizingMode +Resolved +ResolvedHDR +ResolvedImage +ResolvedModifier +ResolvedSymbol +ResolvedText +Result +Retry +ReversedCollection +RGBColorSpace +Root +RotatedShape +RotateGesture +RotationGesture +RoundedBorderTextEditorStyle +RoundedBorderTextFieldStyle +RoundedCornerStyle +RoundedRectangle +RoundedRectangularShape +RoundedRectangularShapeCorners +RoundingMode +RoundingRule +Run +RunLoop +Runs +RunSlice +SafeAreaRegions +Scale +ScaledMetric +ScaledShape +ScaleTransition +Scan +Scanner +Scene +SceneBuilder +SceneLaunchBehavior +ScenePadding +ScenePhase +SceneRestorationBehavior +SceneStorage +Scheduler +SchedulerOptions +SchedulerTimeIntervalConvertible +SchedulerTimeType +Scope +Scoped +ScopedAttributeContainer +Script +ScrollableContent +ScrollAnchorRole +ScrollBounceBehavior +ScrollContentOffsetAdjustmentBehavior +ScrollDismissesKeyboardMode +ScrollEdgeEffectStyle +ScrollGeometry +ScrollIndicatorVisibility +ScrollInputBehavior +ScrollInputKind +ScrollPhase +ScrollPhaseChangeContext +ScrollPosition +ScrollTarget +ScrollTargetBehavior +ScrollTargetBehaviorContext +ScrollTargetBehaviorProperties +ScrollTargetBehaviorPropertiesContext +ScrollTransitionConfiguration +ScrollTransitionPhase +ScrollView +ScrollViewProxy +ScrollViewReader +SearchDirection +SearchFieldPlacement +SearchOptions +SearchPresentationToolbarBehavior +SearchScopeActivation +SearchSuggestionsPlacement +SearchToolbarBehavior +SearchUnavailableContent +Second +SecondFraction +Section +SectionCollection +SectionConfiguration +SectionCustomization +SectionedFetchRequest +SectionedFetchResults +SecureField +SegmentedPickerStyle +SelectionFeedback +SelectionShapeStyle +Sendable +SensoryFeedback +SeparatorShapeStyle +Sequence +SequenceAllSatisfy +SequenceContains +SequenceContainsWhere +SequenceGesture +SequenceMaximum +SequenceMinimum +SequenceStartsWith +Set +SetFailureType +Settings +SettingsLink +Shader +ShaderFunction +ShaderLibrary +Shading +ShadowOptions +ShadowStyle +Shape +ShapeRole +ShapeStyle +ShapeView +Share +ShareLink +SharePreview +SharingBehavior +SidebarAdaptableTabViewStyle +SidebarCommands +SidebarListStyle +SidebarRowSize +SignDisplayStrategy +SimultaneousGesture +SingleAttributeTransformer +Sink +Size +SizeDependentTextVariant +SizeLimitExceededMessage +Slice +Slider +SliderTick +SliderTickBuilder +SliderTickContent +SliderTickContentForEach +SlideTransition +SnapshotData +SnapshotReason +SnapshotResponse +SortComparator +SortDescriptor +SortOrder +Source +Spacer +SpacerSizing +SpatialEventCollection +SpatialEventGesture +SpatialTapGesture +Spring +SpringKeyframe +SpringLoadingBehavior +SquareAzimuth +SquareBorderTextFieldStyle +StackNavigationViewStyle +StandaloneMonth +StandaloneQuarter +StandaloneWeekday +StandardComparator +StandardPredicateExpression +State +StateObject +StaticString +Stepper +StepperFieldDatePickerStyle +Stop +Stopwatch +Storage +Strategy +Stride +StrikethroughStyleAttribute +String +StringCaseInsensitiveCompare +StringContainsRegex +StringInterpolation +StringLiteralType +StringLocalizedCompare +StringLocalizedStandardContains +StringStyle +StrokeBorderShapeView +StrokeShapeView +StrokeStyle +Style +Subdivision +Subject +SubmitLabel +SubmitTriggers +SubscribeOn +Subscriber +Subscribers +Subscription +Subscriptions +SubscriptionView +Subsequence +SubSequence +Substring +Subview +SubviewsCollection +SubviewsCollectionSlice +SurroundingsEffect +SuspendingClock +SwiftUIAttributes +SwitchToggleStyle +SwitchToLatest +Symbol +SymbolAttribute +SymbolColorRenderingMode +SymbolEffectTransition +SymbolRenderingMode +SymbolVariableValueMode +SymbolVariants +SystemClockDidChangeMessage +SystemFormatStyle +SystemTimeZoneDidChangeMessage +Tab +TabBarMinimizeBehavior +TabBarOnlyTabViewStyle +TabBarPlacement +TabContent +TabContentBuilder +TabCustomization +TabCustomizationBehavior +Table +TableColumn +TableColumnAlignment +TableColumnBody +TableColumnBuilder +TableColumnContent +TableColumnCustomization +TableColumnCustomizationBehavior +TableColumnForEach +TableColumnSortComparator +TableForEachContent +TableHeaderRowContent +TableOutlineGroupContent +TableRow +TableRowBody +TableRowBuilder +TableRowContent +TableRowValue +TableStyle +TableStyleConfiguration +TabPlacement +TabRole +TabSearchActivation +TabSection +TabValue +TabView +TabViewBottomAccessoryPlacement +TabViewCustomization +TabViewStyle +TapGesture +Task +TaskGroup +TaskPriority +Template +TemplateRenderingMode +TermOfAddress +Text +TextAlignment +TextAttribute +TextEditingCommands +TextEditor +TextEditorStyle +TextEditorStyleConfiguration +TextField +TextFieldLink +TextFieldStyle +TextFormattingCommands +TextInputDictationActivation +TextInputDictationBehavior +TextInputFormattingControlPlacement +TextProxy +TextRenderer +TextSelectability +TextSelection +TextSelectionAffinity +TextStyle +TextVariantPreference +ThermalStateDidChangeMessage +Thread +Threshold +Throttle +ThrowingTaskGroup +TicksCollection +TimeDataSource +TimeFormatStyle +TimeGroupingStrategy +TimeInterval +TimelineSchedule +TimelineScheduleMode +TimelineView +TimelineViewDefaultContext +Timeout +Timer +TimerPublisher +TimeSeparator +TimeStyle +TimeZone +TimeZoneSeparator +TintShapeStyle +Title +TitleAndIconLabelStyle +TitleBarWindowStyle +TitleDisplayMode +TitleOnlyLabelStyle +Toggle +ToggleStyle +ToggleStyleConfiguration +ToolbarCommands +ToolbarContent +ToolbarContentBuilder +ToolbarCustomizationBehavior +ToolbarCustomizationOptions +ToolbarDefaultItemKind +ToolbarItem +ToolbarItemGroup +ToolbarItemPlacement +ToolbarLabelStyle +ToolbarPlacement +ToolbarRole +ToolbarSpacer +ToolbarTitleDisplayMode +ToolbarTitleMenu +TopLevelDecoder +TopLevelEncoder +Touch +TouchBar +TouchBarItemPresence +TrackingAttribute +Transaction +TransactionKey +Transferable +TransformedShape +Transition +TransitionPhase +TransitionProperties +TransitionStyle +Tree +TruncationMode +TryAllSatisfy +TryCatch +TryCompactMap +TryComparison +TryContainsWhere +TryDropWhile +TryFilter +TryFirstWhere +TryLastWhere +TryMap +TryPrefixWhile +TryReduce +TryRemoveDuplicates +TryScan +TupleDefinition +TupleSliderTickContent +TupleTableColumnContent +TupleTableRowContent +TupleView +TypeCheck +TypedPayloadError +TypesettingLanguage +TypographicBounds +UbiquityIdentityDidChangeMessage +UIApplication +UIBezierPath +UIButton +UICollectionView +UICollectionViewCell +UIColor +UIDevice +UIEdgeInsets +UIFont +UIGestureRecognizer +UIGestureRecognizerRepresentable +UIGestureRecognizerRepresentableContext +UIGestureRecognizerRepresentableCoordinateSpaceConverter +UIHostingController +UIImage +UILabel +UINavigationController +UInt +UInt16 +UInt32 +UInt64 +UInt8 +UIPanGestureRecognizer +UIScreen +UIScrollView +UISegmentedControl +UISlider +UIStackView +UISwitch +UITabBarController +UITableView +UITableViewCell +UITapGestureRecognizer +UITextField +UITextView +UIView +UIViewController +UIViewControllerRepresentable +UIViewRepresentable +UIWindow +UIWindowScene +UnaryMinus +UnaryViewRoot +UnderlineStyleAttribute +UnevenRoundedRectangle +Unicode +UnicodeScalarLiteralType +UnicodeScalarView +UnifiedCompactWindowToolbarStyle +UnifiedWindowToolbarStyle +Unit +UnitCurve +UnitDuration +UnitLength +UnitMass +UnitPoint +UnitPoint3D +Units +UnitsFormatStyle +UnitsStyle +UnitWidth +UnsafeBufferPointer +UnsafeContinuation +UnsafeMutableBufferPointer +UnsafeMutablePointer +UnsafeMutableRawPointer +UnsafePointer +UnsafeRawPointer +URL +URLComponents +URLError +URLQueryItem +URLRequest +URLResource +URLResourceValues +URLResponse +URLSession +URLSessionDataTask +URLSessionTask +UsageType +UserInterfaceSizeClass +UTF16View +UTF8View +UtilityWindow +UUID +Value +ValueConstraint +Variable +VariableID +VariableName +Variant +VectorArithmetic +VerbatimFormatStyle +VerbatimHour +VerticalAlignment +VerticalDirection +VerticalEdge +VerticalPageTabViewStyle +View +ViewAlignedScrollTargetBehavior +ViewBuilder +ViewDimensions +ViewDimensions3D +ViewModifier +Viewpoint3D +ViewRoot +ViewSpacing +ViewThatFits +Visibility +VisualEffect +Void +VolumeViewpointUpdateStrategy +VSplitView +VStack +VStackLayout +Week +Weekday +Weight +WheelDatePickerStyle +WheelPickerStyle +Widget +WidgetBundle +WidgetBundleBuilder +WidgetConfiguration +Width +WillCloseUndoGroupMessage +WillEnterForegroundMessage +WillRedoChangeMessage +WillResignActiveMessage +WillUndoChangeMessage +Window +WindowBackgroundShapeStyle +WindowDragGesture +WindowGroup +WindowIdealSize +WindowInteractionBehavior +WindowLayoutRoot +WindowLevel +WindowManagerRole +WindowMenuBarExtraStyle +WindowPlacement +WindowPlacementContext +WindowProxy +WindowResizability +WindowStyle +WindowToolbarFullScreenVisibility +WindowToolbarStyle +WindowVisibilityToggle +WorldAlignmentBehavior +WorldRecenterPhase +WorldTrackingLimitation +Wrapper +WritableKeyPath +WritingDirection +WritingDirectionAttribute +WritingDirectionStrategy +WritingOptions +WritingToolsBehavior +Year +YearForWeekOfYear +ZeroValueUnitsDisplayStrategy +Zip +Zip2Sequence +Zip3 +Zip4 +ZoomNavigationTransition +ZStack +ZStackLayout +""" +} diff --git a/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift index 80b5746..208297d 100644 --- a/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift +++ b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift @@ -13,9 +13,11 @@ import SwiftSyntax /// labels (`label`). It does so purely from the parsed syntax tree – no type-checker or symbol /// graph – so it is fast and error-tolerant on the partial code fragments common in DocC notes. /// -/// Known limit: the classification is SYNTACTIC, not semantic. It cannot reproduce Xcode's -/// framework-vs-project split (e.g. `ScrollView` vs a project's `StickerListItemView` are both -/// just capitalized type references), so every type reference shares the one `type` role. +/// The classification is SYNTACTIC, not semantic, so the framework-vs-project type split (e.g. +/// `ScrollView` vs a project's `StickerListItemView`) is APPROXIMATED from a committed framework-type +/// allowlist rather than a symbol graph: a capitalized type whose name is in the allowlist keeps the +/// `type` role (framework, purple), any other capitalized type becomes `projecttype` (project, green). +/// Extend the allowlist per site with `additionalFrameworkTypes`. /// /// Non-Swift snippets (and nil/empty languages) are delegated to a fallback highlighter – by /// default the zero-dependency regex `CodeHighlighter` – so a DocC site can inject this single @@ -23,13 +25,23 @@ import SwiftSyntax public struct SwiftSyntaxHighlighter: CodeHighlighting { private let fallback: any CodeHighlighting + /// The effective framework-type set: the committed `FrameworkTypeAllowlist` unioned with any + /// `additionalFrameworkTypes` passed at init. Capitalized types in this set render as framework + /// `type`; all other capitalized types render as project `projecttype`. + private let frameworkTypes: Set + /// Creates a SwiftSyntax-based highlighter. /// - /// - Parameter fallback: The highlighter used for non-Swift snippets and nil/empty languages. - /// Pass nil (the default) to use the zero-dependency regex `CodeHighlighter`. Resolved - /// internally so the default does not reference an internal type across the module boundary. - public init(fallback: (any CodeHighlighting)? = nil) { + /// - Parameters: + /// - fallback: The highlighter used for non-Swift snippets and nil/empty languages. Pass nil + /// (the default) to use the zero-dependency regex `CodeHighlighter`. Resolved internally so the + /// default does not reference an internal type across the module boundary. + /// - additionalFrameworkTypes: Extra type names to treat as framework (purple) types on top of + /// the committed allowlist, e.g. a site's own design-system or umbrella-framework types it + /// wants colored like the SDK. Defaults to empty, leaving the behavior unchanged. + public init(fallback: (any CodeHighlighting)? = nil, additionalFrameworkTypes: Set = []) { self.fallback = fallback ?? CodeHighlighter() + self.frameworkTypes = FrameworkTypeAllowlist.frameworkTypeNames.union(additionalFrameworkTypes) } public func highlight(code: String, language: String?) -> String { @@ -37,20 +49,20 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { language == "swift" else { return self.fallback.highlight(code: code, language: language) } - return Self.highlightSwift(code) + return self.highlightSwift(code) } // MARK: - Swift highlighting /// Parses `code`, merges the base syntactic classification with the per-token role refinement, /// and emits one HTML fragment of escaped, role-tagged spans. - static func highlightSwift(_ code: String) -> String { + func highlightSwift(_ code: String) -> String { let bytes = Array(code.utf8) let count = bytes.count guard count > 0 else { return "" } let tree = Parser.parse(source: code) - let roleMap = SwiftTokenRoleClassifier.classify(tree) + let roleMap = SwiftTokenRoleClassifier.classify(tree, frameworkTypes: self.frameworkTypes) var output = "" var cursor = 0 @@ -68,7 +80,7 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { } let text = Self.escapedSlice(bytes, from: lower, to: upper) - if let role = Self.role(forKind: classified.kind, offset: lower, bytes: bytes, from: lower, to: upper, roleMap: roleMap) { + if let role = self.role(forKind: classified.kind, offset: lower, bytes: bytes, from: lower, to: upper, roleMap: roleMap) { output += "\(text)" } else { output += text @@ -84,7 +96,7 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { /// Resolves the final `sk-tok-*` role class for one classified range: a visitor refinement when /// present, otherwise a direct mapping of the base `SyntaxClassification`. - private static func role( + private func role( forKind kind: SyntaxClassification, offset: Int, bytes: [UInt8], @@ -99,7 +111,11 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { case .keyword, .ifConfigDirective: return "keyword" case .type: - return "type" + // A token the base pass already knows sits in TYPE position (`View` in `: View`, `Sticker` + // in `[Sticker]`). Split it by the framework allowlist like the expression-visitor types, so + // type annotations get the same framework-vs-project palette as type initializers. + let name = String(decoding: bytes[lower.. [Int: String] { - let visitor = SwiftTokenRoleClassifier(viewMode: .sourceAccurate) + /// The effective framework-type set used to split a capitalized type into a framework `type` or a + /// project `projecttype`. Holds the committed allowlist optionally unioned with per-highlighter + /// additions; the split itself goes through `FrameworkTypeAllowlist.role(forTypeName:in:)`. + private let frameworkTypes: Set + + private init(frameworkTypes: Set) { + self.frameworkTypes = frameworkTypes + super.init(viewMode: .sourceAccurate) + } + + /// Classifies every refinable token in `tree` and returns the offset→role map. `frameworkTypes` is + /// the effective allowlist that decides, per capitalized type token, framework `type` vs project + /// `projecttype`. + static func classify(_ tree: SourceFileSyntax, frameworkTypes: Set) -> [Int: String] { + let visitor = SwiftTokenRoleClassifier(frameworkTypes: frameworkTypes) visitor.walk(tree) return visitor.roles } // MARK: - Helpers + /// The role for a capitalized type token (`type` if its name is a framework / known type, else the + /// project-type `projecttype`), resolved against this classifier's effective framework set. + private func typeRole(forName name: String) -> String { + FrameworkTypeAllowlist.role(forTypeName: name, in: self.frameworkTypes) + } + private func byteOffset(of token: TokenSyntax) -> Int { token.positionAfterSkippingLeadingTrivia.utf8Offset } @@ -59,14 +77,15 @@ final class SwiftTokenRoleClassifier: SyntaxVisitor { override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { // The callee identifies what kind of call this is. A capitalized callee is a type - // initializer (`ScrollView { … }`, `ForEach(…)`, `StickerListItemView(…)`) – we cannot - // tell a framework type from a project type syntactically, so every type reference shares - // the one `type` role. A lowercase callee is a free-function call (`print(…)`). A member - // callee (`view.swipeActions(…)`) is left to the member-access visitor so the member keeps - // its `member` role whether or not it is called. + // initializer (`ScrollView { … }`, `ForEach(…)`, `StickerListItemView(…)`); its name is split + // against the committed framework allowlist into a framework `type` (purple) or a project + // `projecttype` (green), approximating Xcode's framework-vs-project palette without a symbol + // graph. A lowercase callee is a free-function call (`print(…)`). A member callee + // (`view.swipeActions(…)`) is left to the member-access visitor so the member keeps its + // `member` role whether or not it is called. if let reference = node.calledExpression.as(DeclReferenceExprSyntax.self), self.isIdentifierToken(reference.baseName) { - let role = self.startsUppercased(reference.baseName.text) ? "type" : "call" + let role = self.startsUppercased(reference.baseName.text) ? self.typeRole(forName: reference.baseName.text) : "call" self.set(role, at: reference.baseName) } return .visitChildren @@ -91,12 +110,13 @@ final class SwiftTokenRoleClassifier: SyntaxVisitor { // Reached for every declaration reference. Callees and member names were already assigned // by their parent above, so only fill the ones still unset: a lowercase reference is a value // (`stickers`, `sticker`) → the headline green `variable`; a capitalized one is a bare type - // reference (`Color.red`'s `Color`, a metatype) → `type`. + // reference (`Color.red`'s `Color`, a metatype) → split by the framework allowlist into a + // framework `type` or a project `projecttype`. let token = node.baseName guard self.isIdentifierToken(token) else { return .visitChildren } let offset = self.byteOffset(of: token) guard self.roles[offset] == nil else { return .visitChildren } - self.roles[offset] = self.startsUppercased(token.text) ? "type" : "variable" + self.roles[offset] = self.startsUppercased(token.text) ? self.typeRole(forName: token.text) : "variable" return .visitChildren } diff --git a/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift b/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift index 61d6066..119a36b 100644 --- a/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift +++ b/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift @@ -71,8 +71,12 @@ struct HighlighterPreviewGenerator {

SwiftSyntax Swift highlighter preview

Each syntactic role is a separate sk-tok-* class, so the palette is a - pure-CSS concern. Variable references (stickers, the sticker usage, - count) classify as variable and render green.

+ pure-CSS concern. Variable references (stickers, the sticker usage) + classify as variable and render green. Capitalized types + are split by a committed framework allowlist: framework types (ScrollView, + View) stay type (purple), project types (StickerListItemView, + DeleteButton, Sticker) become projecttype + (green), as in Xcode.

1 · Classification – each role in a distinct debug color

Arbitrary, deliberately-distinct colors so every role is visible; proves the @@ -99,7 +103,8 @@ struct HighlighterPreviewGenerator { private static let legend: String = { let roles: [(String, String)] = [ - ("keyword", "struct, var, in"), ("type", "ScrollView, Sticker"), ("call", "lowercase callee"), + ("keyword", "struct, var, in"), ("type", "ScrollView, View (framework)"), + ("projecttype", "StickerListItemView (project)"), ("call", "lowercase callee"), ("variable", "stickers, sticker (use)"), ("member", ".swipeActions, .id"), ("param", "sticker binding"), ("string", "\"Delete\""), ("number", "12"), ("boolean", "true / nil"), ("attribute", "@State"), ("comment", "// …"), ("operator", "=="), ("label", "spacing:, edge:"), @@ -129,10 +134,11 @@ struct HighlighterPreviewGenerator { .chip small { color: #777; } /* Panel 1 – distinct debug colors (one per role). */ - .debug .sk-tok-keyword { color: #C026D3; font-weight: 700; } - .debug .sk-tok-type { color: #2563EB; } - .debug .sk-tok-call { color: #EA580C; } - .debug .sk-tok-variable { color: #16A34A; font-weight: 600; } + .debug .sk-tok-keyword { color: #C026D3; font-weight: 700; } + .debug .sk-tok-type { color: #2563EB; } + .debug .sk-tok-projecttype { color: #059669; font-weight: 600; } + .debug .sk-tok-call { color: #EA580C; } + .debug .sk-tok-variable { color: #16A34A; font-weight: 600; } .debug .sk-tok-member { color: #0D9488; } .debug .sk-tok-param { color: #CA8A04; } .debug .sk-tok-string { color: #DC2626; } @@ -144,10 +150,11 @@ struct HighlighterPreviewGenerator { .debug .sk-tok-label { color: #9333EA; } /* Panel 2 – the Apple/Xcode palette that core docc.css ships. call = green. */ - .palette.light .sk-tok-keyword { color: #AD3DA4; font-weight: 600; } - .palette.light .sk-tok-type { color: #703DAA; } - .palette.light .sk-tok-call { color: #3C7D3C; } - .palette.light .sk-tok-variable { color: #3C7D3C; } + .palette.light .sk-tok-keyword { color: #AD3DA4; font-weight: 600; } + .palette.light .sk-tok-type { color: #703DAA; } + .palette.light .sk-tok-projecttype { color: #3C7D3C; } + .palette.light .sk-tok-call { color: #3C7D3C; } + .palette.light .sk-tok-variable { color: #3C7D3C; } .palette.light .sk-tok-string { color: #D12F1B; } .palette.light .sk-tok-number { color: #272AD8; } .palette.light .sk-tok-boolean { color: #AD3DA4; } @@ -155,10 +162,11 @@ struct HighlighterPreviewGenerator { .palette.light .sk-tok-comment { color: #707F8C; font-style: italic; } /* member / param / operator / label inherit the default text color. */ - .palette.dark .sk-tok-keyword { color: #FF7AB2; font-weight: 600; } - .palette.dark .sk-tok-type { color: #DABAFF; } - .palette.dark .sk-tok-call { color: #7FD98A; } - .palette.dark .sk-tok-variable { color: #7FD98A; } + .palette.dark .sk-tok-keyword { color: #FF7AB2; font-weight: 600; } + .palette.dark .sk-tok-type { color: #DABAFF; } + .palette.dark .sk-tok-projecttype { color: #7FD98A; } + .palette.dark .sk-tok-call { color: #7FD98A; } + .palette.dark .sk-tok-variable { color: #7FD98A; } .palette.dark .sk-tok-string { color: #FF8170; } .palette.dark .sk-tok-number { color: #D9C97C; } .palette.dark .sk-tok-boolean { color: #FF7AB2; } diff --git a/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift index aa3eb56..dbd3b14 100644 --- a/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift +++ b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift @@ -78,6 +78,72 @@ struct SwiftSyntaxHighlighterTests { self.expectSpan(result, role: "variable", text: "count") } + // MARK: - Framework-vs-project type split (the committed allowlist heuristic) + + @Test("A framework type stays type (purple) – present in the committed allowlist") + func frameworkTypeStaysType() { + // `ScrollView` is an SDK type in the committed allowlist, so it keeps the framework `type` role. + let code = "ScrollView { Text(title) }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "type", text: "ScrollView") + self.expectSpan(result, role: "type", text: "Text") + } + + @Test("A project type becomes projecttype (green) – absent from the allowlist") + func projectTypeBecomesProjectType() { + // `StickerListItemView` is a project-defined type (not in the allowlist), so it splits off the + // framework purple into the green `projecttype` role – the headline of this refinement. + let code = "StickerListItemView(sticker: sticker)" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "projecttype", text: "StickerListItemView") + } + + @Test("A type in TYPE position is split too (annotation, not just an initializer)") + func typePositionIsSplit() { + // The base SwiftIDEUtils pass classifies these as `.type` (not via the expression visitors), so + // the split must apply to the base mapping as well: `View` framework→type, `Sticker` project→projecttype. + let code = "struct Row: View { var items: [Sticker] = [] }" + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "type", text: "View") + self.expectSpan(result, role: "projecttype", text: "Sticker") + } + + @Test("additionalFrameworkTypes promotes a name to the framework type role") + func additionalFrameworkTypesPromotesToType() { + // A site can color its own umbrella/design-system types like the SDK by passing them in. The + // same name that is `projecttype` by default becomes framework `type` once added. + let code = "StickerListItemView(sticker: sticker)" + let promoting = SwiftSyntaxHighlighter(additionalFrameworkTypes: ["StickerListItemView"]) + let result = promoting.highlight(code: code, language: "swift") + self.expectSpan(result, role: "type", text: "StickerListItemView") + } + + @Test("The swipe-actions block splits framework vs project types and keeps values green") + func swipeBlockFrameworkVsProjectSplit() { + // The representative SwiftUI swipe-actions block: SDK container/iteration types stay purple, + // the project's row/button types go green, and the value references stay the headline green. + let code = """ + ScrollView { + LazyVStack { + ForEach(stickers) { sticker in + StickerListItemView(sticker: sticker) + .swipeActions { + DeleteButton(title: "Delete") { } + } + } + } + } + """ + let result = self.highlighter.highlight(code: code, language: "swift") + self.expectSpan(result, role: "type", text: "ScrollView") + self.expectSpan(result, role: "type", text: "LazyVStack") + self.expectSpan(result, role: "type", text: "ForEach") + self.expectSpan(result, role: "projecttype", text: "StickerListItemView") + self.expectSpan(result, role: "projecttype", text: "DeleteButton") + self.expectSpan(result, role: "variable", text: "stickers") + self.expectSpan(result, role: "variable", text: "sticker") + } + // MARK: - Roles taken straight from the base SwiftIDEUtils classification @Test("Keywords, strings, numbers, comments, and attributes use the base classification") @@ -107,7 +173,8 @@ struct SwiftSyntaxHighlighterTests { let result = self.highlighter.highlight(code: code, language: "swift") #expect(!result.isEmpty) self.expectSpan(result, role: "variable", text: "stickers") - self.expectSpan(result, role: "type", text: "StickerListItemView") + // `StickerListItemView` is a project type (absent from the framework allowlist) → projecttype. + self.expectSpan(result, role: "projecttype", text: "StickerListItemView") } @Test("Empty input returns empty output") From d993ce809899840149ccffd6111fe9182ea607de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 16:55:57 +0200 Subject: [PATCH 4/7] Point WWDCNotes references to the live wwdcnotes.com domain The WWDCNotes documentation site is now live in production at wwdcnotes.com (built with SiteKit), so the README and DocC-blueprint showcase links, the base-URL doc-comment example, and the BaseURLOverride test fixture now use wwdcnotes.com instead of the staging wwdcnotes.fline.dev host. --- Plugin/blueprints/INDEX.md | 2 +- README.md | 2 +- Sources/SiteKit/Pipeline/SiteBuilder.swift | 2 +- Tests/SiteKitTests/BaseURLOverrideTests.swift | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Plugin/blueprints/INDEX.md b/Plugin/blueprints/INDEX.md index b17133d..073c80d 100644 --- a/Plugin/blueprints/INDEX.md +++ b/Plugin/blueprints/INDEX.md @@ -26,7 +26,7 @@ Read `.md` first, then copy files from `/`. | `Podcast` | Episode pages, audio player, chapters, iTunes RSS | Podcast shows, interview series | [Podcast.md](Podcast.md) | [appstore-tagebuch.de](https://appstore-tagebuch.de) | | `Newsletter` | Email newsletter with issue archive, signup forms, email rendering | Topic newsletters, curated digests, weekly/monthly roundups | [Newsletter.md](Newsletter.md) | [evolutionkit.dev](https://evolutionkit.dev) | | `AppLanding` | Single product landing page with hero, features, pricing, reviews | App marketing pages, SaaS products | [AppLanding.md](AppLanding.md) | [translatekit.pages.dev](https://translatekit.pages.dev) | -| `DocC` | DocC catalog → static, AI-fetchable HTML with a sidebar + full-text search | Documentation sites, API/guide docs | [DocC.md](DocC.md) | [wwdcnotes.fline.dev](https://wwdcnotes.fline.dev) | +| `DocC` | DocC catalog → static, AI-fetchable HTML with a sidebar + full-text search | Documentation sites, API/guide docs | [DocC.md](DocC.md) | [wwdcnotes.com](https://wwdcnotes.com) | | `Plain` | Minimal structure, no opinions | Experimentation, custom pipelines | [Plain.md](Plain.md) | – | --- diff --git a/README.md b/README.md index 1ed6c82..9e378c0 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ SiteKit ships **9 blueprints** – starter sites you scaffold and customise. Eac | **Podcast** | Episode pages, audio player, iTunes RSS | [Podcast.md](Plugin/blueprints/Podcast.md) | [appstore-tagebuch.de](https://appstore-tagebuch.de) | | **Newsletter** | Issue archive, signup forms, email rendering | [Newsletter.md](Plugin/blueprints/Newsletter.md) | [evolutionkit.dev](https://evolutionkit.dev) | | **AppLanding** | Single product landing page (hero, features, pricing) | [AppLanding.md](Plugin/blueprints/AppLanding.md) | [translatekit.pages.dev](https://translatekit.pages.dev) | -| **DocC** | DocC catalog → docs site with sidebar + full-text search | [DocC.md](Plugin/blueprints/DocC.md) | [wwdcnotes.fline.dev](https://wwdcnotes.fline.dev) | +| **DocC** | DocC catalog → docs site with sidebar + full-text search | [DocC.md](Plugin/blueprints/DocC.md) | [wwdcnotes.com](https://wwdcnotes.com) | | **Plain** | Minimal, no opinions – a blank canvas | [Plain.md](Plugin/blueprints/Plain.md) | – | Not sure which to pick? The [blueprint catalog](Plugin/blueprints/INDEX.md) has a decision tree and a feature comparison. diff --git a/Sources/SiteKit/Pipeline/SiteBuilder.swift b/Sources/SiteKit/Pipeline/SiteBuilder.swift index 17c9760..8d46aaf 100644 --- a/Sources/SiteKit/Pipeline/SiteBuilder.swift +++ b/Sources/SiteKit/Pipeline/SiteBuilder.swift @@ -733,7 +733,7 @@ extension SiteBuilder { enum BaseURLOverrideError: Error, Equatable, CustomStringConvertible { /// `--base-url` was passed as the last argument, with no value following it. case missingValue - /// The value is not an absolute http(s) URL (e.g. `wwdcnotes.fline.dev` without a scheme). + /// The value is not an absolute http(s) URL (e.g. `wwdcnotes.com` without a scheme). case notAnAbsoluteHTTPURL(String) var description: String { diff --git a/Tests/SiteKitTests/BaseURLOverrideTests.swift b/Tests/SiteKitTests/BaseURLOverrideTests.swift index 9a6dc62..cbd37d8 100644 --- a/Tests/SiteKitTests/BaseURLOverrideTests.swift +++ b/Tests/SiteKitTests/BaseURLOverrideTests.swift @@ -39,8 +39,8 @@ struct BaseURLOverrideTests { @Test("Scheme-less value throws instead of silently building broken absolute URLs") func schemelessValue() { - #expect(throws: BaseURLOverrideError.notAnAbsoluteHTTPURL("wwdcnotes.fline.dev")) { - try SiteBuilder.baseURLOverride(from: ["Site", "build", "--base-url", "wwdcnotes.fline.dev"]) + #expect(throws: BaseURLOverrideError.notAnAbsoluteHTTPURL("wwdcnotes.com")) { + try SiteBuilder.baseURLOverride(from: ["Site", "build", "--base-url", "wwdcnotes.com"]) } } From 6370ed7be062b60eccc39956661735991835c85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 17:37:13 +0200 Subject: [PATCH 5/7] Drop the framework-vs-project type split from the Swift highlighter The optional SwiftSyntax highlighter classified capitalized types against a committed 1755-name framework allowlist, rendering SDK types purple (sk-tok-type) and everything else green (sk-tok-projecttype). Resolving every type through a hand-maintained allowlist is large, error-prone, and drifts with each SDK bump. Map every capitalized type (initializer, annotation, or bare reference) to the single sk-tok-type role, matching the regex highlighter. Delete the allowlist file, the additionalFrameworkTypes init parameter, and the sk-tok-projecttype CSS (light and dark). The highlighter keeps the syntactic roles the regex pass cannot produce, above all the green variable references. --- Sources/SiteKit/Resources/DocC/docc.css | 21 +- .../FrameworkTypeAllowlist.swift | 1799 ----------------- .../SwiftSyntaxHighlighter.swift | 42 +- .../SwiftTokenRoleClassifier.swift | 43 +- .../SwiftSyntaxHighlighterTests.swift | 55 +- 5 files changed, 49 insertions(+), 1911 deletions(-) delete mode 100644 Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift diff --git a/Sources/SiteKit/Resources/DocC/docc.css b/Sources/SiteKit/Resources/DocC/docc.css index 9b3a3d7..dee169f 100644 --- a/Sources/SiteKit/Resources/DocC/docc.css +++ b/Sources/SiteKit/Resources/DocC/docc.css @@ -4138,23 +4138,15 @@ body.sk-docc-shell-body { font-weight: 600; } -/* Framework / known type references: purple. `sk-tok-type` now means a capitalized type whose name is - in the highlighter's committed framework allowlist (SDK + curated UIKit/AppKit/CoreGraphics/stdlib), - so `ScrollView` reads purple while a project's own type goes green via sk-tok-projecttype below. */ +/* Type references: purple. Every capitalized type – initializer, annotation, or bare reference – + uses this single role, matching the regex highlighter (`ScrollView`, `Text`, and a project's own + `StickerListItemView` all read purple). */ .sk-docc-highlight .sk-tok-type { color: #703DAA; } -/* Project (non-framework) type references: green – approximates Xcode, where a project's own - `StickerListItemView` reads green while SDK types read purple. Placeholder green (matches the - variable green for now); the exact, distinct-from-variable green is finalised in the colour pass. */ -.sk-docc-highlight .sk-tok-projecttype { - color: #3C7D3C; -} - -/* Calls: seeded green for "more colour" (it greens project initializers like `StickerListItemView`); - flip to the type purple for the cleaner look that keeps framework `ForEach` from going green – a - CSS-only change, no Swift rebuild. */ +/* Free-function calls – a lowercase callee like `print`: green. A capitalized callee is a type + initializer and takes the purple `sk-tok-type` above instead. */ .sk-docc-highlight .sk-tok-call { color: #3C7D3C; } @@ -4194,9 +4186,6 @@ body.sk-docc-shell-body { [data-theme="dark"] .sk-docc-highlight .sk-tok-type { color: #DABAFF; } -[data-theme="dark"] .sk-docc-highlight .sk-tok-projecttype { - color: #7FD98A; -} [data-theme="dark"] .sk-docc-highlight .sk-tok-call { color: #7FD98A; } diff --git a/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift b/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift deleted file mode 100644 index 31db577..0000000 --- a/Sources/SiteKitSyntaxHighlighting/FrameworkTypeAllowlist.swift +++ /dev/null @@ -1,1799 +0,0 @@ -import Foundation - -/// The committed framework-type allowlist that drives the highlighter's framework-vs-project split. -/// -/// A capitalized identifier the classifier would mark as a type is looked up here: a name in this set -/// is a framework / known type and keeps `sk-tok-type` (purple); any other capitalized type is a -/// project-defined type and becomes `sk-tok-projecttype` (green). This mirrors Xcode, where SDK types -/// like `ScrollView` read purple while a project's own `StickerListItemView` reads green. -/// -/// Provenance: extracted once OFFLINE from the installed SDK module `.swiftinterface` public -/// type-declarations (SwiftUI + SwiftUICore, where `View` / `Text` / `ForEach` actually live, plus -/// Foundation and Combine), unioned with a curated set of common Swift stdlib / Concurrency, UIKit, -/// AppKit, and CoreGraphics types (which the SwiftUI/Foundation extraction does not cover, so -/// `UIView` / `CGRect` would otherwise be misclassified as project types in SwiftUI-interop -/// snippets). The list is COMMITTED and never re-extracted at build time, so the coloring is -/// reproducible across machines, CI, and consumers regardless of the SDK installed there. Regenerate -/// it when bumping the supported SDK / toolchain. -enum FrameworkTypeAllowlist { - /// The framework / known-type names. A capitalized type token whose name is in this set renders as - /// `sk-tok-type`; every other capitalized type renders as `sk-tok-projecttype`. Built once from - /// `rawList`, so callers pay the parse cost a single time per process. - static let frameworkTypeNames: Set = Set( - Self.rawList - .split(whereSeparator: \.isNewline) - .lazy - .map { $0.trimmingCharacters(in: .whitespaces) } - .filter { !$0.isEmpty } - ) - - /// The role class for a capitalized type `name`, given the effective framework set in play (the - /// committed `frameworkTypeNames`, optionally unioned with a highlighter's `additionalFrameworkTypes`): - /// `type` for a framework / known type (name in the set), `projecttype` for any other - /// (project-defined) type. The single decision point for the framework-vs-project split, so the two - /// assignment sites (the role classifier's expression visitors and the base-classification mapping) - /// can never disagree on the role strings. - static func role(forTypeName name: String, in frameworkTypes: Set) -> String { - frameworkTypes.contains(name) ? "type" : "projecttype" - } - - /// One framework type per line. Kept as a single multi-line string literal (parsed once into the - /// set above) to avoid both a 1700+-element array-literal compile cost and any resource bundling. - private static let rawList = """ -AccessibilityActionCategory -AccessibilityActionKind -AccessibilityAdjustmentDirection -AccessibilityAttachmentModifier -AccessibilityChildBehavior -AccessibilityCustomContentKey -AccessibilityDirectTouchOptions -AccessibilityFocusState -AccessibilityHeadingLevel -AccessibilityLabeledPairRole -AccessibilityQuickActionOutlineStyle -AccessibilityQuickActionPromptStyle -AccessibilityQuickActionStyle -AccessibilityRotorContent -AccessibilityRotorContentBuilder -AccessibilityRotorEntry -AccessibilitySystemRotor -AccessibilityTechnologies -AccessibilityTextContentType -AccessibilityTraits -AccessibilityZoomGestureAction -AccessoryBarActionButtonStyle -AccessoryBarButtonStyle -AccessoryCircularCapacityGaugeStyle -AccessoryCircularGaugeStyle -AccessoryLinearCapacityGaugeStyle -AccessoryLinearGaugeStyle -Actions -ActionSheet -Actor -ActorType -AdaptableTabBarPlacement -AdaptiveImageGlyphAttribute -AffineTransform -AgreementArgumentAttribute -AgreementConceptAttribute -Alert -AlertScene -AligningContentProviderLayout -Alignment -Alignment3D -AlignmentID -AlignmentKey -AlignmentStrategy -AllCases -AllSatisfy -AlternateDescriptionAttribute -AlternatingRowBackgroundBehavior -AMPMStyle -Anchor -AnchoredLayout -AnchoredRelativeFormatStyle -Angle -Angle2D -AngularGradient -Animatable -AnimatableData -AnimatableModifier -AnimatablePair -AnimatableValues -Animation -AnimationCompletionCriteria -AnimationContext -AnimationState -AnimationStateKey -AnimationTimelineSchedule -Any -AnyCancellable -AnyCompositorContent -AnyDefinition -AnyGesture -AnyGradient -AnyHashable -AnyKeyPath -AnyLayout -AnyLocation -AnyLocationBase -AnyObject -AnyPublisher -AnyScrollTargetBehavior -AnyShape -AnyShapeStyle -AnySubscriber -AnyTabContent -AnyTransition -AnyView -App -AppStorage -Argument -Arithmetic -ArithmeticOperator -Array -ArrayLiteralElement -ArraySlice -AssertNoFailure -Assign -AssistiveAccess -AsymmetricTransition -AsyncBytes -AsyncCharacterSequence -AsyncImage -AsyncImagePhase -AsyncIterator -AsyncIteratorProtocol -AsyncLineSequence -AsyncMessage -AsyncPublisher -AsyncSequence -AsyncStream -AsyncThrowingPublisher -AsyncThrowingStream -AsyncUnicodeScalarSequence -AttributeContainer -AttributeContainerProxy -Attributed -AttributedString -AttributedStringAttributeMutation -AttributedStringKey -AttributedStringProtocol -AttributedStyle -AttributedSubstring -AttributedTextFormatting -AttributedTextFormattingDefinition -AttributedTextSelection -AttributedTextValueConstraint -AttributeDynamicLookup -AttributeInvalidationCondition -AttributeMergePolicy -AttributeRunBoundaries -Attributes -AttributeScope -AttributeScopeCodableConfiguration -AttributeScopes -AttributesSlice1 -AttributesSlice2 -AttributesSlice3 -AttributesSlice4 -AttributesSlice5 -Attribution -Autoconnect -AutomaticControlGroupStyle -AutomaticDisclosureGroupStyle -AutomaticFormStyle -AutomaticHoverEffect -AutomaticImmersionStyle -AutomaticLabeledContentStyle -AutomaticMenuBarExtraStyle -AutomaticNavigationSplitViewStyle -AutomaticNavigationTransition -AutomaticPresentationSizing -AutomaticTableStyle -AutomaticTextEditorStyle -AXChartDescriptorRepresentable -Axis -BackgroundColorAttribute -BackgroundDisplayMode -BackgroundProminence -BackgroundStyle -BackgroundTask -BackgroundTaskCancelledReason -BadgeProminence -BalancedNavigationSplitViewStyle -Base64DecodingOptions -Base64EncodingOptions -BaselineOffsetAttribute -BaseMessageIdentifier -Behavior -BezierPoint -BidirectionalCollection -Bindable -Binding -BlendMode -BlockOperation -BlurOptions -BlurReplaceTransition -Body -BookmarkCreationOptions -BookmarkResolutionOptions -Bool -BooleanLiteralType -BorderedButtonMenuStyle -BorderedButtonStyle -BorderedListStyle -BorderedProminentButtonStyle -BorderedTableStyle -BorderlessButtonMenuButtonStyle -BorderlessButtonMenuStyle -BorderlessButtonStyle -BorderlessPullDownMenuButtonStyle -Breakpoint -Buffer -BufferingStrategy -Builder -Bundle -BundleDescription -Button -ButtonBorderShape -ButtonMenuStyle -ButtonRepeatBehavior -ButtonRole -ButtonSizing -ButtonStyle -ButtonStyleConfiguration -ButtonToggleStyle -ByteCount -ByteCountAttribute -ByteCountFormatStyle -ByteCountFormatter -Cache -CachePolicy -Cadence -CalculationError -Calendar -CalendarDayChangedMessage -Cancellable -Canvas -CapitalizationContext -Capsule -CardButtonStyle -CarouselListStyle -CarouselTabViewStyle -Case -CaseIterable -Catch -Category -CGAffineTransform -CGColor -CGContext -CGFloat -CGGradient -CGImage -CGMutablePath -CGPath -CGPoint -CGRect -CGSize -CGVector -Character -CharacterIndex -CharacterSet -CharacterView -CheckboxToggleStyle -CheckedContinuation -CheckpointMessage -Children -Chirality -Circle -CircularGaugeStyle -CircularProgressViewStyle -ClipOptions -Clock -ClosedRange -CocoaError -Codable -CodableAttributedStringKey -CodableConfiguration -CodableRepresentation -CodableWithConfiguration -Code -CodingKey -Collation -Collect -CollectByCount -CollectByTime -Collection -CollectionContainsCollection -CollectionIndexSubscript -CollectionRangeSubscript -Color -ColorMatrix -ColorPicker -ColorRenderingMode -Colors -ColorScheme -ColorSchemeContrast -ColorSpace -ColumnNavigationViewStyle -ColumnsFormStyle -CombineIdentifier -CombineLatest -CombineLatest3 -CombineLatest4 -CommandGroup -CommandGroupPlacement -CommandMenu -Commands -CommandsBuilder -CommonKeyPathKind -CompactDatePickerStyle -CompactMap -CompactMenuControlGroupStyle -Comparable -ComparableComparator -Comparator -Compared -CompareOptions -Comparison -ComparisonOperator -Completion -Component -ComponentDisplayOption -ComponentParseStrategy -Components -ComponentsFormatStyle -CompositorContent -CompositorContentBuilder -Concatenate -ConcentricRectangle -Conditional -ConditionalCast -Configuration -Conjunction -ConnectablePublisher -ConnectionAcceptedMessage -ContainerBackgroundPlacement -ContainerRelativeShape -ContainerSizingOptions -ContainerValueKey -ContainerValues -Contains -ContainsWhere -Content -ContentHoverEffect -ContentMarginPlacement -ContentMode -ContentOffset -ContentShapeKinds -ContentSizeCategory -ContentToolbarPlacement -ContentTransition -ContentUnavailableView -Context -ContextMenu -ContiguousArray -ContiguousBytes -ContinuousClock -ControlActiveState -ControlGroup -ControlGroupStyle -ControlGroupStyleConfiguration -ControlSize -ControlWidget -ControlWidgetConfiguration -ControlWidgetConfigurationBuilder -ControlWidgetTemplate -ControlWidgetTemplateBuilder -CookiesChangedMessage -CoordinateSpace -CoordinateSpaceProtocol -Corner -Count -CubicKeyframe -Currency -CurrencyFormatStyleConfiguration -CurrentLocaleDidChangeMessage -CurrentValueLabel -CurrentValueSubject -CustomAnimation -CustomCombineIdentifierConvertible -CustomDebugStringConvertible -CustomHoverEffect -CustomizableToolbarContent -CustomLocalizedStringResourceConvertible -CustomNSError -CustomPresentationDetent -CustomPronoun -CustomStringConvertible -CyclicYear -Data -DataAvailableMessage -DataDecodingStrategy -DataEncodingStrategy -DataProtocol -DataTaskPublisher -Date -DateComponents -DateComponentsFormatter -DateDecodingStrategy -DateEncodingStrategy -DateFieldAttribute -DateFormatter -DateInterval -DateOffset -DatePicker -DatePickerComponents -DatePickerStyle -DatePickerStyleConfiguration -DateReference -DateSeparator -DateStyle -DateTimeSeparator -Day -DayOfYear -DayPeriod -Deallocator -Debounce -DebugReplaceableView -Decimal -DecimalSeparatorDisplayStrategy -Decodable -DecodableAttributedStringKey -DecodableWithConfiguration -Decode -Decoder -DecodingConfiguration -DecodingConfigurationProviding -DefaultButtonLabel -DefaultButtonStyle -DefaultDatePickerStyle -DefaultDateProgressLabel -DefaultDocumentGroupLaunchActions -DefaultFocusEvaluationPriority -DefaultGaugeStyle -DefaultGlassEffectShape -DefaultGroupBoxStyle -DefaultLabelStyle -DefaultListStyle -DefaultMenuButtonStyle -DefaultMenuStyle -DefaultNavigationViewStyle -DefaultPickerStyle -DefaultProgressViewStyle -DefaultSettingsLinkLabel -DefaultShareLinkLabel -DefaultTabLabel -DefaultTabViewStyle -DefaultTextFieldStyle -DefaultToggleStyle -DefaultToolbarItem -DefaultWindowStyle -DefaultWindowToolbarStyle -DefaultWindowVisibilityToggleLabel -Deferred -Definiteness -DefinitionBuilder -Delay -Demand -DepthAlignment -DepthAlignmentID -DepthAlignmentKey -Description -DescriptiveNumberFormatConfiguration -Design -Determination -DialogSeverity -Dictionary -DictionaryKeyDefaultValueSubscript -DictionaryKeySubscript -DidBecomeActiveMessage -DidBecomeInvalidMessage -DidChangeMessage -DidCloseUndoGroupMessage -DidEnterBackgroundMessage -DidFinishGatheringMessage -DidLoadMessage -DidOpenUndoGroupMessage -DidRedoChangeMessage -DidStartGatheringMessage -DidTerminateMessage -DidUndoChangeMessage -DigitalCrownEvent -DigitalCrownRotationalSensitivity -Direction -DirectoryHint -DisabledTextSelectability -DisclosureGroup -DisclosureGroupStyle -DisclosureGroupStyleConfiguration -DisclosureTableRow -DiscontiguousAttributedSubstring -DiscreteFormatStyle -Disjunction -DismissAction -DismissBehavior -DismissImmersiveSpaceAction -DismissSearchAction -DismissWindowAction -DispatchGroup -DispatchQueue -DispatchSemaphore -DispatchTime -DispatchWorkItem -DisplayProxy -Divider -DocumentBaseBox -DocumentConfiguration -DocumentGroup -DocumentGroupLaunchScene -DocumentLaunchGeometryProxy -DocumentLaunchView -Double -DoubleColumnNavigationViewStyle -DragConfiguration -DragDropPreviewsFormation -DragGesture -DragSession -DrawingOptions -Drop -DropConfiguration -DropDelegate -DropInfo -DropOperation -DropProposal -DropSession -DropUntilOutput -DropWhile -Duration -DurationFieldAttribute -DynamicProperty -DynamicRange -DynamicTableRowContent -DynamicTypeSize -DynamicViewContent -Edge -EdgeInsets -EditableCollectionContent -EditActions -EditButton -EditMode -Element -Ellipse -EllipticalGradient -EllipticalListStyle -Empty -EmptyAnimatableData -EmptyCommands -EmptyControlWidgetConfiguration -EmptyControlWidgetTemplate -EmptyDefinition -EmptyHoverEffect -EmptyHoverEffectContent -EmptyMatchedTransitionSourceConfiguration -EmptyModifier -EmptyTableRowContent -EmptyView -EmptyVisualEffect -EmptyWidgetConfiguration -EnabledTextSelectability -Encodable -EncodableAttributedStringKey -EncodableWithConfiguration -Encode -Encoder -Encoding -EncodingConfiguration -EncodingConfigurationProviding -EncodingConversionOptions -End -Entries -EnumeratedSequence -EnumerationOptions -Environment -EnvironmentalModifier -EnvironmentKey -EnvironmentObject -EnvironmentValues -Equal -Equatable -EquatableView -Era -Error -ErrorPointer -ErrorUserInfoKey -Event -EventModifiers -EveryMinuteTimelineSchedule -ExclusiveGesture -ExpandedWindowToolbarStyle -ExplicitTimelineSchedule -ExpressibleByArrayLiteral -ExpressibleByDictionaryLiteral -ExpressibleByIntegerLiteral -ExpressibleByStringLiteral -Expression -ExpressionEvaluate -ExtendedGraphemeClusterLiteralType -Fail -Failure -FailurePolicy -FetchedResults -FetchRequest -Field -FieldDatePickerStyle -FileDialogBrowserOptions -FileDocument -FileDocumentConfiguration -FileDocumentReadConfiguration -FileDocumentWriteConfiguration -FileHandle -FileManager -FileWrapper -FillShapeStyle -FillShapeView -FillStyle -Filter -FilterOptions -FindContext -First -FirstWhere -FittedPresentationSizing -FixedTextVariant -FlatMap -Flexibility -Float -Float16 -FloatDivision -FloatingPointFormatStyle -FloatingPointParseStrategy -FloatLiteralType -FocusedBinding -FocusedObject -FocusedValue -FocusedValueKey -FocusedValues -FocusInteractions -FocusState -Font -FontAttribute -ForceCast -ForcedUnwrap -ForEach -ForEachSectionCollection -ForEachSubviewCollection -ForegroundColorAttribute -ForegroundStyle -Form -FormatInput -FormatOutput -FormatString -FormatStyle -FormatStyleCapitalizationContext -FormattingOptions -FormPresentationSizing -FormStyle -FormStyleConfiguration -FoundationAttributes -FractionalPartDisplayStrategy -FrameResizeDirection -FrameResizePosition -Frequency -FullImmersionStyle -Future -Gauge -GaugeStyle -GaugeStyleConfiguration -GeometryEffect -GeometryProxy -GeometryReader -GeometryReader3D -Gesture -GestureMask -GestureState -GestureStateGesture -Glass -GlassButtonStyle -GlassEffectContainer -GlassEffectTransition -GlassProminentButtonStyle -GlobalActor -GlobalCoordinateSpace -Gradient -GradientOptions -GrammaticalCase -GrammaticalGender -GrammaticalNumber -GrammaticalPerson -GraphicalDatePickerStyle -GraphicsContext -Grid -GridItem -GridLayout -GridRow -Group -GroupBox -GroupBoxStyle -GroupBoxStyleConfiguration -GroupedFormStyle -GroupedListStyle -GroupedTabViewStyle -GroupElementsOfContent -GroupHoverEffect -Grouping -GroupSectionsOfContent -HandGestureShortcut -HandleEvents -Hashable -Hasher -HelpLink -HiddenTitleBarWindowStyle -HierarchicalShapeStyle -HierarchicalShapeStyleModifier -HighlightHoverEffect -HorizontalAlignment -HorizontalDirection -HorizontalEdge -HostDisplayOption -HostingSheetRepresentation -Hour -HourCycle -HoverEffect -HoverEffectContent -HoverEffectGroup -HoverEffectPhaseOverride -HoverPhase -HSplitView -HStack -HStackLayout -HTTPFormatStyle -HTTPURLResponse -Icon -IconOnlyLabelStyle -ID -Identifiable -Identifier -IdentifierType -IdentityTransition -IgnoreOutput -Image -ImagePaint -ImageRenderer -ImageURLAttribute -ImmediateScheduler -ImmersionChangeContext -ImmersionStyle -ImmersiveContentBrightness -ImmersiveEnvironmentBehavior -ImmersiveSpace -ImmersiveSpaceContent -ImmersiveSpaceContentBuilder -ImmersiveSpaceViewContent -ImportFromDevicesCommands -Index -IndexDisplayMode -IndexedIdentifierCollection -IndexPath -IndexSet -IndexViewStyle -Indices -InflectionAlternativeAttribute -InflectionConcept -InflectionRule -InflectionRuleAttribute -InlinePickerStyle -InlinePresentationIntentAttribute -Input -InputDevicePose -InsetGroupedListStyle -InsetListStyle -InsetShape -InsettableShape -InsetTableStyle -InspectorCommands -Instant -InstantProtocol -Int -Int16 -Int32 -Int64 -Int8 -IntDivision -IntegerFormatStyle -IntegerLiteralType -IntegerParseStrategy -IntentType -InterfaceOrientation -Interpolation -InterpolationOptions -InterpretedSyntax -IntervalFormatStyle -IntRemainder -ISO8601DateFormatter -ISO8601FormatStyle -ItemProviderTableRowModifier -Iterator -IteratorProtocol -JSONDecoder -JSONEncoder -JSONSerialization -Just -KerningAttribute -Key -KeyboardShortcut -KeyDecodingStrategy -KeyEncodingStrategy -KeyEquivalent -Keyframe -KeyframeAnimator -Keyframes -KeyframesBuilder -KeyframeTimeline -KeyframeTrack -KeyframeTrackContent -KeyframeTrackContentBuilder -KeyPath -KeyPathComparator -KeyPress -KeyValueObservingPublisher -Kind -Label -LabeledContent -LabeledContentStyle -LabeledContentStyleConfiguration -LabeledControlGroupContent -LabeledToolbarItemGroupContent -LabelStyle -LabelStyleConfiguration -Language -LanguageCode -LanguageDirection -LanguageIdentifierAttribute -Last -LastWhere -Layout -LayoutDirection -LayoutDirectionBehavior -LayoutKey -LayoutProperties -LayoutSubview -LayoutSubviews -LayoutValueKey -LazyFilterSequence -LazyHGrid -LazyHStack -LazyMapSequence -LazySequence -LazyVGrid -LazyVStack -Leading -LegibilityWeight -LiftHoverEffect -LimitBehavior -LimitedAvailabilityConfiguration -Line -LinearCapacityGaugeStyle -LinearGaugeStyle -LinearGradient -LinearKeyframe -LinearProgressViewStyle -LineStyle -Link -LinkAttribute -LinkButtonStyle -LinkShapeStyle -List -ListFormatStyle -ListItemDelimiterAttribute -ListItemTint -ListSectionSpacing -ListStyle -ListType -LocalCoordinateSpace -Locale -Localization -LocalizationOptions -LocalizationValue -LocalizedDateArgumentAttribute -LocalizedDateIntervalArgumentAttribute -LocalizedError -LocalizedNumberFormatAttribute -LocalizedNumericArgumentAttribute -LocalizedStringArgumentAttributes -LocalizedStringKey -LocalizedStringResource -LocalizedURLArgumentAttribute -LocalSession -Locations -Logger -LongPressGesture -LosslessStringConvertible -LowDiskSpaceMessage -MachError -MagnificationGesture -MagnifyGesture -Magnitude -Main -MainActor -MainActorMessage -MakeConnectable -ManagedBuffer -Map -MapError -MapKeyPath -MapKeyPath2 -MapKeyPath3 -MarkdownDecodableAttributedStringKey -MarkdownParsingOptions -MarkdownSourcePosition -MarkdownSourcePositionAttribute -MarkedValueLabel -MatchedGeometryEffect -MatchedGeometryProperties -MatchedTransitionSourceConfiguration -MatchingPolicy -Material -MaterialActiveAppearance -MaximumValueLabel -MeasureInterval -Measurement -MeasurementAttribute -MeasurementFormatUnitUsage -MeasurementSystem -Menu -MenuActionDismissBehavior -MenuBarExtra -MenuBarExtraStyle -MenuButton -MenuButtonStyle -MenuControlGroupStyle -MenuOrder -MenuPickerStyle -MenuStyle -MenuStyleConfiguration -Merge -Merge3 -Merge4 -Merge5 -Merge6 -Merge7 -Merge8 -MergeMany -MeshGradient -Message -MessageIdentifier -MinimumValueLabel -Minute -Mirror -MixedImmersionStyle -ModifiedContent -Month -Morphology -MorphologyAttribute -MoveCommandDirection -MoveKeyframe -MoveTransition -Multicast -MultiDatePicker -MultiViewRoot -MutableCollection -MutableDataProtocol -MutableURLRequest -Name -NamedCoordinateSpace -Namespace -NameStyle -NavigationBarDrawerDisplayMode -NavigationBarItem -NavigationControlGroupStyle -NavigationLink -NavigationLinkPickerStyle -NavigationPath -NavigationSplitView -NavigationSplitViewColumn -NavigationSplitViewStyle -NavigationSplitViewStyleConfiguration -NavigationSplitViewVisibility -NavigationStack -NavigationTransition -NavigationView -NavigationViewStyle -Negation -NetworkServiceType -NetworkUnavailableReason -Never -NewDocumentAction -NewDocumentButton -NilCoalesce -NilLiteral -NonConformingFloatDecodingStrategy -NonConformingFloatEncodingStrategy -Notation -NotEqual -Notification -NotificationCenter -Notifications -NSApplicationDelegateAdaptor -NSAttributedString -NSButton -NSColor -NSError -NSErrorPointer -NSFastEnumerationIterator -NSFont -NSGestureRecognizerRepresentable -NSGestureRecognizerRepresentableContext -NSGestureRecognizerRepresentableCoordinateSpaceConverter -NSHostingController -NSHostingMenu -NSHostingSceneBridgingOptions -NSHostingSceneRepresentation -NSHostingSizingOptions -NSHostingView -NSImage -NSIndexSetIterator -NSKeyValueObservation -NSKeyValueObservedChange -NSKeyValueObservingCustomization -NSNumber -NSObject -NSPredicate -NSRange -NSScrollView -NSSortDescriptor -NSStackView -NSString -NSTableView -NSTextField -NSView -NSViewController -NSViewControllerRepresentable -NSViewControllerRepresentableContext -NSViewRepresentable -NSViewRepresentableContext -NSWindow -NumberFormatAttributes -NumberFormatStyleConfiguration -NumberFormatter -NumberingSystem -NumberPart -NumberPartAttribute -NumberRepresentation -ObjectIdentifier -ObjectiveCConvertibleAttributedStringKey -ObjectiveCValue -ObjectWillChangePublisher -Observable -ObservableObject -ObservableObjectPublisher -ObservationRegistrar -ObservationToken -ObservedObject -OffsetShape -OffsetTransition -OnInsertTableRowModifier -OpacityTransition -OpaquePointer -OpenDocumentAction -OpenImmersiveSpaceAction -OpenSettingsAction -OpenURLAction -OpenWindowAction -Operation -OperationQueue -OperationsOutsideApp -OperationsWithinApp -Optional -OptionalFlatMap -OptionSet -Orientation -OrnamentAttachmentAnchor -OutlineGroup -OutlineSubgroupChildren -Output -OutputFormatting -PageIndexViewStyle -PagePresentationSizing -PageTabViewStyle -PagingScrollTargetBehavior -PaletteControlGroupStyle -PalettePickerStyle -PaletteSelectionEffect -ParseableFormatStyle -ParseInput -ParseOutput -ParseStrategy -PartialKeyPath -PartialRangeFrom -PartialRangeThrough -PartialRangeUpTo -PartOfSpeech -PassthroughSubject -PasteButton -Path -Pattern -PencilDoubleTapGestureValue -PencilHoverPose -PencilPreferredAction -PencilSqueezeGesturePhase -PencilSqueezeGestureValue -Percent -PeriodicTimelineSchedule -PersonNameComponentAttribute -PersonNameComponents -Phase -PhaseAnimator -Phases -PhysicalMetric -Picker -PickerStyle -PinnedScrollableViews -Pipe -Placeholder -PlaceholderContentView -PlaceholderTextShapeStyle -PlainButtonStyle -PlainListStyle -PlainTextEditorStyle -PlainTextFieldStyle -PlainWindowStyle -PointerStyle -PopoverAttachmentAnchor -PopUpButtonPickerStyle -Position -POSIXError -PowerStateDidChangeMessage -Precision -Predicate -PredicateBindings -PredicateCodableConfiguration -PredicateCodableKeyPathProviding -PredicateError -PredicateEvaluate -PredicateExpression -PredicateExpressions -PredicateRegex -PreferenceKey -PreferredColorSchemeKey -PrefetchStrategy -PrefixUntilOutput -PrefixWhile -Presentation -PresentationAdaptation -PresentationBackgroundInteraction -PresentationContentInteraction -PresentationDetent -PresentationIntent -PresentationIntentAttribute -PresentationMode -PresentationSizing -PresentationSizingContext -PresentationSizingRoot -PresentedWindowContent -PressFeedback -PreviewContext -PreviewContextKey -PreviewDevice -PreviewModifier -PreviewModifierContent -PreviewPlatform -PreviewProvider -PrimitiveButtonStyle -PrimitiveButtonStyleConfiguration -Print -Process -ProcessInfo -Progress -ProgressiveImmersionAspectRatio -ProgressiveImmersionStyle -ProgressView -ProgressViewStyle -ProgressViewStyleConfiguration -ProjectionTransform -Prominence -ProminentDetailNavigationSplitViewStyle -Promise -Pronoun -PronounType -Properties -Property -PropertyListDecoder -PropertyListEncoder -PropertyListFormat -PropertyListSerialization -ProposedViewSize -Published -Publisher -Publishers -PullDownButton -PullDownMenuBarExtraStyle -PullDownMenuButtonStyle -PushTransition -PushWindowAction -Quarter -RadialGradient -RadioGroupPickerStyle -RandomAccessCollection -Range -RangeExpressionContains -RangeReplaceableCollection -RangeView -RasterizationOptions -RawRepresentable -RawValue -ReadCompletionMessage -ReadingOptions -ReadToEndOfFileCompletionMessage -ReceiveOn -Record -Recording -RecoverableError -Rectangle -RectangleCornerInsets -RectangleCornerRadii -RecurrenceRule -RedactionReasons -Reduce -ReferenceConvertible -ReferenceFileDocument -ReferenceFileDocumentConfiguration -ReferenceType -ReferenceWritableKeyPath -ReferentConceptAttribute -RefreshAction -RegexOutput -Region -Regions -RelativeDateTimeFormatter -RelativeFormatStyle -ReleaseFeedback -RemoteDeviceIdentifier -RemoteImmersiveSpace -RemoveDuplicates -RenameAction -RenameButton -Renderer -Repeated -RepeatedTimePolicy -ReplaceEmpty -ReplaceError -ReplacementIndexAttribute -Representation -ResetFocusAction -ResizingMode -Resolved -ResolvedHDR -ResolvedImage -ResolvedModifier -ResolvedSymbol -ResolvedText -Result -Retry -ReversedCollection -RGBColorSpace -Root -RotatedShape -RotateGesture -RotationGesture -RoundedBorderTextEditorStyle -RoundedBorderTextFieldStyle -RoundedCornerStyle -RoundedRectangle -RoundedRectangularShape -RoundedRectangularShapeCorners -RoundingMode -RoundingRule -Run -RunLoop -Runs -RunSlice -SafeAreaRegions -Scale -ScaledMetric -ScaledShape -ScaleTransition -Scan -Scanner -Scene -SceneBuilder -SceneLaunchBehavior -ScenePadding -ScenePhase -SceneRestorationBehavior -SceneStorage -Scheduler -SchedulerOptions -SchedulerTimeIntervalConvertible -SchedulerTimeType -Scope -Scoped -ScopedAttributeContainer -Script -ScrollableContent -ScrollAnchorRole -ScrollBounceBehavior -ScrollContentOffsetAdjustmentBehavior -ScrollDismissesKeyboardMode -ScrollEdgeEffectStyle -ScrollGeometry -ScrollIndicatorVisibility -ScrollInputBehavior -ScrollInputKind -ScrollPhase -ScrollPhaseChangeContext -ScrollPosition -ScrollTarget -ScrollTargetBehavior -ScrollTargetBehaviorContext -ScrollTargetBehaviorProperties -ScrollTargetBehaviorPropertiesContext -ScrollTransitionConfiguration -ScrollTransitionPhase -ScrollView -ScrollViewProxy -ScrollViewReader -SearchDirection -SearchFieldPlacement -SearchOptions -SearchPresentationToolbarBehavior -SearchScopeActivation -SearchSuggestionsPlacement -SearchToolbarBehavior -SearchUnavailableContent -Second -SecondFraction -Section -SectionCollection -SectionConfiguration -SectionCustomization -SectionedFetchRequest -SectionedFetchResults -SecureField -SegmentedPickerStyle -SelectionFeedback -SelectionShapeStyle -Sendable -SensoryFeedback -SeparatorShapeStyle -Sequence -SequenceAllSatisfy -SequenceContains -SequenceContainsWhere -SequenceGesture -SequenceMaximum -SequenceMinimum -SequenceStartsWith -Set -SetFailureType -Settings -SettingsLink -Shader -ShaderFunction -ShaderLibrary -Shading -ShadowOptions -ShadowStyle -Shape -ShapeRole -ShapeStyle -ShapeView -Share -ShareLink -SharePreview -SharingBehavior -SidebarAdaptableTabViewStyle -SidebarCommands -SidebarListStyle -SidebarRowSize -SignDisplayStrategy -SimultaneousGesture -SingleAttributeTransformer -Sink -Size -SizeDependentTextVariant -SizeLimitExceededMessage -Slice -Slider -SliderTick -SliderTickBuilder -SliderTickContent -SliderTickContentForEach -SlideTransition -SnapshotData -SnapshotReason -SnapshotResponse -SortComparator -SortDescriptor -SortOrder -Source -Spacer -SpacerSizing -SpatialEventCollection -SpatialEventGesture -SpatialTapGesture -Spring -SpringKeyframe -SpringLoadingBehavior -SquareAzimuth -SquareBorderTextFieldStyle -StackNavigationViewStyle -StandaloneMonth -StandaloneQuarter -StandaloneWeekday -StandardComparator -StandardPredicateExpression -State -StateObject -StaticString -Stepper -StepperFieldDatePickerStyle -Stop -Stopwatch -Storage -Strategy -Stride -StrikethroughStyleAttribute -String -StringCaseInsensitiveCompare -StringContainsRegex -StringInterpolation -StringLiteralType -StringLocalizedCompare -StringLocalizedStandardContains -StringStyle -StrokeBorderShapeView -StrokeShapeView -StrokeStyle -Style -Subdivision -Subject -SubmitLabel -SubmitTriggers -SubscribeOn -Subscriber -Subscribers -Subscription -Subscriptions -SubscriptionView -Subsequence -SubSequence -Substring -Subview -SubviewsCollection -SubviewsCollectionSlice -SurroundingsEffect -SuspendingClock -SwiftUIAttributes -SwitchToggleStyle -SwitchToLatest -Symbol -SymbolAttribute -SymbolColorRenderingMode -SymbolEffectTransition -SymbolRenderingMode -SymbolVariableValueMode -SymbolVariants -SystemClockDidChangeMessage -SystemFormatStyle -SystemTimeZoneDidChangeMessage -Tab -TabBarMinimizeBehavior -TabBarOnlyTabViewStyle -TabBarPlacement -TabContent -TabContentBuilder -TabCustomization -TabCustomizationBehavior -Table -TableColumn -TableColumnAlignment -TableColumnBody -TableColumnBuilder -TableColumnContent -TableColumnCustomization -TableColumnCustomizationBehavior -TableColumnForEach -TableColumnSortComparator -TableForEachContent -TableHeaderRowContent -TableOutlineGroupContent -TableRow -TableRowBody -TableRowBuilder -TableRowContent -TableRowValue -TableStyle -TableStyleConfiguration -TabPlacement -TabRole -TabSearchActivation -TabSection -TabValue -TabView -TabViewBottomAccessoryPlacement -TabViewCustomization -TabViewStyle -TapGesture -Task -TaskGroup -TaskPriority -Template -TemplateRenderingMode -TermOfAddress -Text -TextAlignment -TextAttribute -TextEditingCommands -TextEditor -TextEditorStyle -TextEditorStyleConfiguration -TextField -TextFieldLink -TextFieldStyle -TextFormattingCommands -TextInputDictationActivation -TextInputDictationBehavior -TextInputFormattingControlPlacement -TextProxy -TextRenderer -TextSelectability -TextSelection -TextSelectionAffinity -TextStyle -TextVariantPreference -ThermalStateDidChangeMessage -Thread -Threshold -Throttle -ThrowingTaskGroup -TicksCollection -TimeDataSource -TimeFormatStyle -TimeGroupingStrategy -TimeInterval -TimelineSchedule -TimelineScheduleMode -TimelineView -TimelineViewDefaultContext -Timeout -Timer -TimerPublisher -TimeSeparator -TimeStyle -TimeZone -TimeZoneSeparator -TintShapeStyle -Title -TitleAndIconLabelStyle -TitleBarWindowStyle -TitleDisplayMode -TitleOnlyLabelStyle -Toggle -ToggleStyle -ToggleStyleConfiguration -ToolbarCommands -ToolbarContent -ToolbarContentBuilder -ToolbarCustomizationBehavior -ToolbarCustomizationOptions -ToolbarDefaultItemKind -ToolbarItem -ToolbarItemGroup -ToolbarItemPlacement -ToolbarLabelStyle -ToolbarPlacement -ToolbarRole -ToolbarSpacer -ToolbarTitleDisplayMode -ToolbarTitleMenu -TopLevelDecoder -TopLevelEncoder -Touch -TouchBar -TouchBarItemPresence -TrackingAttribute -Transaction -TransactionKey -Transferable -TransformedShape -Transition -TransitionPhase -TransitionProperties -TransitionStyle -Tree -TruncationMode -TryAllSatisfy -TryCatch -TryCompactMap -TryComparison -TryContainsWhere -TryDropWhile -TryFilter -TryFirstWhere -TryLastWhere -TryMap -TryPrefixWhile -TryReduce -TryRemoveDuplicates -TryScan -TupleDefinition -TupleSliderTickContent -TupleTableColumnContent -TupleTableRowContent -TupleView -TypeCheck -TypedPayloadError -TypesettingLanguage -TypographicBounds -UbiquityIdentityDidChangeMessage -UIApplication -UIBezierPath -UIButton -UICollectionView -UICollectionViewCell -UIColor -UIDevice -UIEdgeInsets -UIFont -UIGestureRecognizer -UIGestureRecognizerRepresentable -UIGestureRecognizerRepresentableContext -UIGestureRecognizerRepresentableCoordinateSpaceConverter -UIHostingController -UIImage -UILabel -UINavigationController -UInt -UInt16 -UInt32 -UInt64 -UInt8 -UIPanGestureRecognizer -UIScreen -UIScrollView -UISegmentedControl -UISlider -UIStackView -UISwitch -UITabBarController -UITableView -UITableViewCell -UITapGestureRecognizer -UITextField -UITextView -UIView -UIViewController -UIViewControllerRepresentable -UIViewRepresentable -UIWindow -UIWindowScene -UnaryMinus -UnaryViewRoot -UnderlineStyleAttribute -UnevenRoundedRectangle -Unicode -UnicodeScalarLiteralType -UnicodeScalarView -UnifiedCompactWindowToolbarStyle -UnifiedWindowToolbarStyle -Unit -UnitCurve -UnitDuration -UnitLength -UnitMass -UnitPoint -UnitPoint3D -Units -UnitsFormatStyle -UnitsStyle -UnitWidth -UnsafeBufferPointer -UnsafeContinuation -UnsafeMutableBufferPointer -UnsafeMutablePointer -UnsafeMutableRawPointer -UnsafePointer -UnsafeRawPointer -URL -URLComponents -URLError -URLQueryItem -URLRequest -URLResource -URLResourceValues -URLResponse -URLSession -URLSessionDataTask -URLSessionTask -UsageType -UserInterfaceSizeClass -UTF16View -UTF8View -UtilityWindow -UUID -Value -ValueConstraint -Variable -VariableID -VariableName -Variant -VectorArithmetic -VerbatimFormatStyle -VerbatimHour -VerticalAlignment -VerticalDirection -VerticalEdge -VerticalPageTabViewStyle -View -ViewAlignedScrollTargetBehavior -ViewBuilder -ViewDimensions -ViewDimensions3D -ViewModifier -Viewpoint3D -ViewRoot -ViewSpacing -ViewThatFits -Visibility -VisualEffect -Void -VolumeViewpointUpdateStrategy -VSplitView -VStack -VStackLayout -Week -Weekday -Weight -WheelDatePickerStyle -WheelPickerStyle -Widget -WidgetBundle -WidgetBundleBuilder -WidgetConfiguration -Width -WillCloseUndoGroupMessage -WillEnterForegroundMessage -WillRedoChangeMessage -WillResignActiveMessage -WillUndoChangeMessage -Window -WindowBackgroundShapeStyle -WindowDragGesture -WindowGroup -WindowIdealSize -WindowInteractionBehavior -WindowLayoutRoot -WindowLevel -WindowManagerRole -WindowMenuBarExtraStyle -WindowPlacement -WindowPlacementContext -WindowProxy -WindowResizability -WindowStyle -WindowToolbarFullScreenVisibility -WindowToolbarStyle -WindowVisibilityToggle -WorldAlignmentBehavior -WorldRecenterPhase -WorldTrackingLimitation -Wrapper -WritableKeyPath -WritingDirection -WritingDirectionAttribute -WritingDirectionStrategy -WritingOptions -WritingToolsBehavior -Year -YearForWeekOfYear -ZeroValueUnitsDisplayStrategy -Zip -Zip2Sequence -Zip3 -Zip4 -ZoomNavigationTransition -ZStack -ZStackLayout -""" -} diff --git a/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift index 208297d..991f916 100644 --- a/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift +++ b/Sources/SiteKitSyntaxHighlighting/SwiftSyntaxHighlighter.swift @@ -13,11 +13,10 @@ import SwiftSyntax /// labels (`label`). It does so purely from the parsed syntax tree – no type-checker or symbol /// graph – so it is fast and error-tolerant on the partial code fragments common in DocC notes. /// -/// The classification is SYNTACTIC, not semantic, so the framework-vs-project type split (e.g. -/// `ScrollView` vs a project's `StickerListItemView`) is APPROXIMATED from a committed framework-type -/// allowlist rather than a symbol graph: a capitalized type whose name is in the allowlist keeps the -/// `type` role (framework, purple), any other capitalized type becomes `projecttype` (project, green). -/// Extend the allowlist per site with `additionalFrameworkTypes`. +/// The classification is SYNTACTIC, not semantic: every capitalized type (initializer, annotation, +/// or bare reference) gets the single `type` role, exactly like the regex `CodeHighlighter`. The +/// extra value this highlighter adds is the EXPRESSION-position roles the regex pass cannot see – +/// the green `variable` references above all. /// /// Non-Swift snippets (and nil/empty languages) are delegated to a fallback highlighter – by /// default the zero-dependency regex `CodeHighlighter` – so a DocC site can inject this single @@ -25,23 +24,13 @@ import SwiftSyntax public struct SwiftSyntaxHighlighter: CodeHighlighting { private let fallback: any CodeHighlighting - /// The effective framework-type set: the committed `FrameworkTypeAllowlist` unioned with any - /// `additionalFrameworkTypes` passed at init. Capitalized types in this set render as framework - /// `type`; all other capitalized types render as project `projecttype`. - private let frameworkTypes: Set - /// Creates a SwiftSyntax-based highlighter. /// - /// - Parameters: - /// - fallback: The highlighter used for non-Swift snippets and nil/empty languages. Pass nil - /// (the default) to use the zero-dependency regex `CodeHighlighter`. Resolved internally so the - /// default does not reference an internal type across the module boundary. - /// - additionalFrameworkTypes: Extra type names to treat as framework (purple) types on top of - /// the committed allowlist, e.g. a site's own design-system or umbrella-framework types it - /// wants colored like the SDK. Defaults to empty, leaving the behavior unchanged. - public init(fallback: (any CodeHighlighting)? = nil, additionalFrameworkTypes: Set = []) { + /// - Parameter fallback: The highlighter used for non-Swift snippets and nil/empty languages. Pass + /// nil (the default) to use the zero-dependency regex `CodeHighlighter`. Resolved internally so + /// the default does not reference an internal type across the module boundary. + public init(fallback: (any CodeHighlighting)? = nil) { self.fallback = fallback ?? CodeHighlighter() - self.frameworkTypes = FrameworkTypeAllowlist.frameworkTypeNames.union(additionalFrameworkTypes) } public func highlight(code: String, language: String?) -> String { @@ -62,7 +51,7 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { guard count > 0 else { return "" } let tree = Parser.parse(source: code) - let roleMap = SwiftTokenRoleClassifier.classify(tree, frameworkTypes: self.frameworkTypes) + let roleMap = SwiftTokenRoleClassifier.classify(tree) var output = "" var cursor = 0 @@ -112,10 +101,9 @@ public struct SwiftSyntaxHighlighter: CodeHighlighting { return "keyword" case .type: // A token the base pass already knows sits in TYPE position (`View` in `: View`, `Sticker` - // in `[Sticker]`). Split it by the framework allowlist like the expression-visitor types, so - // type annotations get the same framework-vs-project palette as type initializers. - let name = String(decoding: bytes[lower.. - - private init(frameworkTypes: Set) { - self.frameworkTypes = frameworkTypes - super.init(viewMode: .sourceAccurate) - } - - /// Classifies every refinable token in `tree` and returns the offset→role map. `frameworkTypes` is - /// the effective allowlist that decides, per capitalized type token, framework `type` vs project - /// `projecttype`. - static func classify(_ tree: SourceFileSyntax, frameworkTypes: Set) -> [Int: String] { - let visitor = SwiftTokenRoleClassifier(frameworkTypes: frameworkTypes) + /// Classifies every refinable token in `tree` and returns the offset→role map. + static func classify(_ tree: SourceFileSyntax) -> [Int: String] { + let visitor = SwiftTokenRoleClassifier(viewMode: .sourceAccurate) visitor.walk(tree) return visitor.roles } // MARK: - Helpers - /// The role for a capitalized type token (`type` if its name is a framework / known type, else the - /// project-type `projecttype`), resolved against this classifier's effective framework set. - private func typeRole(forName name: String) -> String { - FrameworkTypeAllowlist.role(forTypeName: name, in: self.frameworkTypes) - } - private func byteOffset(of token: TokenSyntax) -> Int { token.positionAfterSkippingLeadingTrivia.utf8Offset } @@ -76,16 +58,14 @@ final class SwiftTokenRoleClassifier: SyntaxVisitor { // MARK: - Calls override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - // The callee identifies what kind of call this is. A capitalized callee is a type - // initializer (`ScrollView { … }`, `ForEach(…)`, `StickerListItemView(…)`); its name is split - // against the committed framework allowlist into a framework `type` (purple) or a project - // `projecttype` (green), approximating Xcode's framework-vs-project palette without a symbol - // graph. A lowercase callee is a free-function call (`print(…)`). A member callee - // (`view.swipeActions(…)`) is left to the member-access visitor so the member keeps its - // `member` role whether or not it is called. + // The callee identifies what kind of call this is. A capitalized callee is a type initializer + // (`ScrollView { … }`, `ForEach(…)`, `StickerListItemView(…)`) and takes the `type` role. A + // lowercase callee is a free-function call (`print(…)`). A member callee (`view.swipeActions(…)`) + // is left to the member-access visitor so the member keeps its `member` role whether or not it + // is called. if let reference = node.calledExpression.as(DeclReferenceExprSyntax.self), self.isIdentifierToken(reference.baseName) { - let role = self.startsUppercased(reference.baseName.text) ? self.typeRole(forName: reference.baseName.text) : "call" + let role = self.startsUppercased(reference.baseName.text) ? "type" : "call" self.set(role, at: reference.baseName) } return .visitChildren @@ -110,13 +90,12 @@ final class SwiftTokenRoleClassifier: SyntaxVisitor { // Reached for every declaration reference. Callees and member names were already assigned // by their parent above, so only fill the ones still unset: a lowercase reference is a value // (`stickers`, `sticker`) → the headline green `variable`; a capitalized one is a bare type - // reference (`Color.red`'s `Color`, a metatype) → split by the framework allowlist into a - // framework `type` or a project `projecttype`. + // reference (`Color.red`'s `Color`, a metatype) → the `type` role. let token = node.baseName guard self.isIdentifierToken(token) else { return .visitChildren } let offset = self.byteOffset(of: token) guard self.roles[offset] == nil else { return .visitChildren } - self.roles[offset] = self.startsUppercased(token.text) ? self.typeRole(forName: token.text) : "variable" + self.roles[offset] = self.startsUppercased(token.text) ? "type" : "variable" return .visitChildren } diff --git a/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift index dbd3b14..02332c6 100644 --- a/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift +++ b/Tests/SiteKitSyntaxHighlightingTests/SwiftSyntaxHighlighterTests.swift @@ -78,50 +78,31 @@ struct SwiftSyntaxHighlighterTests { self.expectSpan(result, role: "variable", text: "count") } - // MARK: - Framework-vs-project type split (the committed allowlist heuristic) + // MARK: - Capitalized types all map to the single type role - @Test("A framework type stays type (purple) – present in the committed allowlist") - func frameworkTypeStaysType() { - // `ScrollView` is an SDK type in the committed allowlist, so it keeps the framework `type` role. - let code = "ScrollView { Text(title) }" - let result = self.highlighter.highlight(code: code, language: "swift") - self.expectSpan(result, role: "type", text: "ScrollView") - self.expectSpan(result, role: "type", text: "Text") - } - - @Test("A project type becomes projecttype (green) – absent from the allowlist") - func projectTypeBecomesProjectType() { - // `StickerListItemView` is a project-defined type (not in the allowlist), so it splits off the - // framework purple into the green `projecttype` role – the headline of this refinement. + @Test("A project-defined type is classified type, just like an SDK type") + func projectDefinedTypeIsType() { + // With the framework-vs-project split removed, every capitalized type – SDK or project-defined – + // takes the single `type` role, exactly like the regex highlighter. let code = "StickerListItemView(sticker: sticker)" let result = self.highlighter.highlight(code: code, language: "swift") - self.expectSpan(result, role: "projecttype", text: "StickerListItemView") + self.expectSpan(result, role: "type", text: "StickerListItemView") } - @Test("A type in TYPE position is split too (annotation, not just an initializer)") - func typePositionIsSplit() { + @Test("A type in TYPE position is classified type (annotation, not just an initializer)") + func typePositionIsType() { // The base SwiftIDEUtils pass classifies these as `.type` (not via the expression visitors), so - // the split must apply to the base mapping as well: `View` framework→type, `Sticker` project→projecttype. + // the base mapping must also produce `type`: both `View` and the project's `Sticker`. let code = "struct Row: View { var items: [Sticker] = [] }" let result = self.highlighter.highlight(code: code, language: "swift") self.expectSpan(result, role: "type", text: "View") - self.expectSpan(result, role: "projecttype", text: "Sticker") + self.expectSpan(result, role: "type", text: "Sticker") } - @Test("additionalFrameworkTypes promotes a name to the framework type role") - func additionalFrameworkTypesPromotesToType() { - // A site can color its own umbrella/design-system types like the SDK by passing them in. The - // same name that is `projecttype` by default becomes framework `type` once added. - let code = "StickerListItemView(sticker: sticker)" - let promoting = SwiftSyntaxHighlighter(additionalFrameworkTypes: ["StickerListItemView"]) - let result = promoting.highlight(code: code, language: "swift") - self.expectSpan(result, role: "type", text: "StickerListItemView") - } - - @Test("The swipe-actions block splits framework vs project types and keeps values green") - func swipeBlockFrameworkVsProjectSplit() { - // The representative SwiftUI swipe-actions block: SDK container/iteration types stay purple, - // the project's row/button types go green, and the value references stay the headline green. + @Test("The swipe-actions block classifies every capitalized type as type and keeps values green") + func swipeBlockTypesAndGreenValues() { + // The representative SwiftUI swipe-actions block: SDK and project types alike read as `type`, + // and the value references stay the headline green `variable`. let code = """ ScrollView { LazyVStack { @@ -138,8 +119,8 @@ struct SwiftSyntaxHighlighterTests { self.expectSpan(result, role: "type", text: "ScrollView") self.expectSpan(result, role: "type", text: "LazyVStack") self.expectSpan(result, role: "type", text: "ForEach") - self.expectSpan(result, role: "projecttype", text: "StickerListItemView") - self.expectSpan(result, role: "projecttype", text: "DeleteButton") + self.expectSpan(result, role: "type", text: "StickerListItemView") + self.expectSpan(result, role: "type", text: "DeleteButton") self.expectSpan(result, role: "variable", text: "stickers") self.expectSpan(result, role: "variable", text: "sticker") } @@ -173,8 +154,8 @@ struct SwiftSyntaxHighlighterTests { let result = self.highlighter.highlight(code: code, language: "swift") #expect(!result.isEmpty) self.expectSpan(result, role: "variable", text: "stickers") - // `StickerListItemView` is a project type (absent from the framework allowlist) → projecttype. - self.expectSpan(result, role: "projecttype", text: "StickerListItemView") + // `StickerListItemView` is a capitalized type → the single `type` role. + self.expectSpan(result, role: "type", text: "StickerListItemView") } @Test("Empty input returns empty output") From fd79d4deeebe154e830c9b8243209c412f2f8467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 17:37:19 +0200 Subject: [PATCH 6/7] Delete the temporary highlighter preview generator HighlighterPreviewGenerator was scaffolding for eyeballing the token palette during the highlighter's colour pass: it wrote a self-contained HTML preview only when an environment variable pointed at an output path. The palette has landed, so the generator is no longer needed in the test target. --- .../HighlighterPreviewGenerator.swift | 176 ------------------ 1 file changed, 176 deletions(-) delete mode 100644 Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift diff --git a/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift b/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift deleted file mode 100644 index 119a36b..0000000 --- a/Tests/SiteKitSyntaxHighlightingTests/HighlighterPreviewGenerator.swift +++ /dev/null @@ -1,176 +0,0 @@ -import Testing -import Foundation -import SiteKit -@testable import SiteKitSyntaxHighlighting - -/// Generates a self-contained HTML preview of the SwiftSyntax highlighter, for eyeballing the token -/// classification and the palette. It renders a representative SwiftUI snippet three ways: BEFORE -/// (the regex highlighter), AFTER with one DISTINCT debug color per role (proves the classification -/// independent of the palette), and AFTER with the shipped Apple/Xcode palette (light + dark, the -/// look that core `docc.css` applies). -/// -/// Not an assertion test – it only writes a file, and only when `SITEKIT_HIGHLIGHTER_PREVIEW_OUT` -/// points at an output path, so a plain `swift test` stays fast. Run it with: -/// -/// ``` -/// SITEKIT_HIGHLIGHTER_PREVIEW_OUT=/tmp/highlighter-preview.html swift test --filter HighlighterPreview -/// ``` -@Suite("HighlighterPreview") -struct HighlighterPreviewGenerator { - /// A representative SwiftUI example exercising every role: types, a green variable reference, a - /// parameter binding, member accesses, argument labels, a boolean, a string, a number, an - /// attribute, an operator, and a comment. - static let sample = """ - struct StickerList: View { - @State private var stickers: [Sticker] = [] - - var body: some View { - ScrollView { - LazyVStack(spacing: 12) { - // Each row keeps its own swipe actions. - ForEach(stickers) { sticker in - StickerListItemView(sticker: sticker) - .swipeActions(edge: .trailing) { - DeleteButton(title: "Delete", isDestructive: true) { - stickers.removeAll { $0.id == sticker.id } - } - } - } - } - .swipeActionsContainer() - } - } - } - """ - - @Test("generate the highlighter preview HTML") - func generate() throws { - guard let outPath = ProcessInfo.processInfo.environment["SITEKIT_HIGHLIGHTER_PREVIEW_OUT"] else { - return - } - let before = CodeHighlighter().highlight(code: Self.sample, language: "swift") - let after = SwiftSyntaxHighlighter().highlight(code: Self.sample, language: "swift") - try Self.document(before: before, after: after).write(toFile: outPath, atomically: true, encoding: .utf8) - print("HIGHLIGHTER_PREVIEW_WROTE \(outPath)") - } - - // MARK: - HTML assembly - - private static func document(before: String, after: String) -> String { - """ - - - - - - SiteKit SwiftSyntax highlighter preview - - - -

SwiftSyntax Swift highlighter preview

-

Each syntactic role is a separate sk-tok-* class, so the palette is a - pure-CSS concern. Variable references (stickers, the sticker usage) - classify as variable and render green. Capitalized types - are split by a committed framework allowlist: framework types (ScrollView, - View) stay type (purple), project types (StickerListItemView, - DeleteButton, Sticker) become projecttype - (green), as in Xcode.

- -

1 · Classification – each role in a distinct debug color

-

Arbitrary, deliberately-distinct colors so every role is visible; proves the - classification, not the final look. BEFORE is the regex highlighter (capitalized = type only).

- \(Self.legend) -
-
BEFORE – regex highlighter
-
\(before)
-
AFTER – SwiftSyntax roles
-
\(after)
-
- -

2 · Shipped Apple/Xcode palette (call = green; alternative is call = type)

-
-
AFTER – light
-
\(after)
-
AFTER – dark
-
\(after)
-
- - - """ - } - - private static let legend: String = { - let roles: [(String, String)] = [ - ("keyword", "struct, var, in"), ("type", "ScrollView, View (framework)"), - ("projecttype", "StickerListItemView (project)"), ("call", "lowercase callee"), - ("variable", "stickers, sticker (use)"), ("member", ".swipeActions, .id"), ("param", "sticker binding"), - ("string", "\"Delete\""), ("number", "12"), ("boolean", "true / nil"), - ("attribute", "@State"), ("comment", "// …"), ("operator", "=="), ("label", "spacing:, edge:"), - ] - let items = roles.map { role, example in - "\(role) \(example)" - }.joined(separator: "\n") - return "
\(items)
" - }() - - private static let css = """ - body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; color: #1d1d1f; background: #fff; } - h1 { font-size: 1.4rem; } h2 { font-size: 1.1rem; margin-top: 2rem; } - .note { color: #555; max-width: 70ch; line-height: 1.5; } - .legend-var { color: #3C7D3C; font-weight: 700; } - .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; } - @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } } - figure { margin: 0; } figcaption { font-size: .8rem; color: #666; margin-bottom: .3rem; } - pre { margin: 0; padding: 1rem 1.2rem; border-radius: 10px; overflow-x: auto; - font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace; font-size: 13px; line-height: 1.6; } - pre code { display: block; background: none; border: 0; padding: 0; } - .light { background: #f5f5f7; color: #1d1d1f; border: 1px solid #e2e2e6; } - .dark { background: #1f1f24; color: #e6e6ea; border: 1px solid #34343a; } - .legend { display: flex; flex-wrap: wrap; gap: .5rem; padding: .8rem; border-radius: 10px; margin: .6rem 0 1rem; } - .chip { display: inline-flex; align-items: center; gap: .3rem; background: #fff; border: 1px solid #e2e2e6; border-radius: 6px; padding: .15rem .45rem; } - .chip .sw { font-family: ui-monospace, monospace; font-weight: 700; font-size: 12px; } - .chip small { color: #777; } - - /* Panel 1 – distinct debug colors (one per role). */ - .debug .sk-tok-keyword { color: #C026D3; font-weight: 700; } - .debug .sk-tok-type { color: #2563EB; } - .debug .sk-tok-projecttype { color: #059669; font-weight: 600; } - .debug .sk-tok-call { color: #EA580C; } - .debug .sk-tok-variable { color: #16A34A; font-weight: 600; } - .debug .sk-tok-member { color: #0D9488; } - .debug .sk-tok-param { color: #CA8A04; } - .debug .sk-tok-string { color: #DC2626; } - .debug .sk-tok-number { color: #7C3AED; } - .debug .sk-tok-boolean { color: #DB2777; } - .debug .sk-tok-attribute { color: #65A30D; } - .debug .sk-tok-comment { color: #6B7280; font-style: italic; } - .debug .sk-tok-operator { color: #0891B2; } - .debug .sk-tok-label { color: #9333EA; } - - /* Panel 2 – the Apple/Xcode palette that core docc.css ships. call = green. */ - .palette.light .sk-tok-keyword { color: #AD3DA4; font-weight: 600; } - .palette.light .sk-tok-type { color: #703DAA; } - .palette.light .sk-tok-projecttype { color: #3C7D3C; } - .palette.light .sk-tok-call { color: #3C7D3C; } - .palette.light .sk-tok-variable { color: #3C7D3C; } - .palette.light .sk-tok-string { color: #D12F1B; } - .palette.light .sk-tok-number { color: #272AD8; } - .palette.light .sk-tok-boolean { color: #AD3DA4; } - .palette.light .sk-tok-attribute { color: #947100; } - .palette.light .sk-tok-comment { color: #707F8C; font-style: italic; } - /* member / param / operator / label inherit the default text color. */ - - .palette.dark .sk-tok-keyword { color: #FF7AB2; font-weight: 600; } - .palette.dark .sk-tok-type { color: #DABAFF; } - .palette.dark .sk-tok-projecttype { color: #7FD98A; } - .palette.dark .sk-tok-call { color: #7FD98A; } - .palette.dark .sk-tok-variable { color: #7FD98A; } - .palette.dark .sk-tok-string { color: #FF8170; } - .palette.dark .sk-tok-number { color: #D9C97C; } - .palette.dark .sk-tok-boolean { color: #FF7AB2; } - .palette.dark .sk-tok-attribute { color: #CC9768; } - .palette.dark .sk-tok-comment { color: #7F8C98; font-style: italic; } - """ -} From d2328adfdc9a4f4a07b5b56089ee43bf2da89f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cihat=20G=C3=BCnd=C3=BCz?= Date: Thu, 18 Jun 2026 17:37:27 +0200 Subject: [PATCH 7/7] Rewrite the DocC code-block matcher with Swift Regex applyToBodyHTML located each
...
block with an NSRegularExpression and rebuilt the result by hand: NSString bridging, manual cursor arithmetic, and NSNotFound group checks. Swap in a Swift Regex literal with named lang and body captures and drive the rewrite through String.replacing(_:with:). The optional lang capture is nil exactly when the class attribute is absent, which folds the NSNotFound and empty-language cases into one expression and drops the cursor bookkeeping. The match semantics are unchanged: the existing applyToBodyHTML tests (tagged, default-language, no-default passthrough, escaping, and the // in a string case) stay green. --- .../SiteKit/Utilities/CodeHighlighting.swift | 59 +++++-------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/Sources/SiteKit/Utilities/CodeHighlighting.swift b/Sources/SiteKit/Utilities/CodeHighlighting.swift index f819b65..2e933d5 100644 --- a/Sources/SiteKit/Utilities/CodeHighlighting.swift +++ b/Sources/SiteKit/Utilities/CodeHighlighting.swift @@ -78,58 +78,27 @@ extension CodeHighlighting { public func applyToBodyHTML(_ html: String, defaultLanguage: String?) -> String { // Match:
...content...
// or
...content...
(no language class) - // The content may span multiple lines, so .dotMatchesLineSeparators is required. - guard let regex = try? NSRegularExpression( - pattern: #"
(.*?)
"#, - options: [.dotMatchesLineSeparators] - ) else { return html } + // `lang` is nil when the class attribute is absent; `body` may span multiple lines, so the dot + // must match newlines. `replacing` rewrites each non-overlapping block and leaves the rest of + // the body verbatim. + let blockPattern = #/
[^"]*)")?\s*>(?.*?)
/# + .dotMatchesNewlines() - let ns = html as NSString - var result = "" - var cursor = 0 + return html.replacing(blockPattern) { match in + // An absent class attribute and an empty `language-` value both fall back to `defaultLanguage`. + let language = match.lang.flatMap { $0.isEmpty ? nil : String($0) } ?? defaultLanguage - let matches = regex.matches(in: html, range: NSRange(location: 0, length: ns.length)) - for match in matches { - // Append verbatim text before this match. - let wholeRange = match.range - result += ns.substring(with: NSRange(location: cursor, length: wholeRange.location - cursor)) + // The captured content is already HTML-escaped; unescape so the tokenizer works on plain text. + let rawContent = HTMLEscaping.unescape(String(match.body)) - // Extract language from `class="language-X"` (group 2). When the class attribute is - // entirely absent group 1 and group 2 are both empty. - let language: String? - if match.range(at: 2).location != NSNotFound { - let langRaw = ns.substring(with: match.range(at: 2)) - language = langRaw.isEmpty ? defaultLanguage : langRaw - } else { - language = defaultLanguage - } - - // The already-escaped code content (group 3). - let escapedContent: String - if match.range(at: 3).location != NSNotFound { - escapedContent = ns.substring(with: match.range(at: 3)) - } else { - escapedContent = "" - } - - // Unescape then re-highlight so the tokenizer works on plain text. - let rawContent = HTMLEscaping.unescape(escapedContent) - let highlighted: String - let classAttr: String if let lang = language?.lowercased().trimmingCharacters(in: .whitespaces), !lang.isEmpty { - highlighted = self.highlight(code: rawContent, language: lang) - classAttr = " class=\"language-\(HTMLEscaping.escape(lang))\"" + let highlighted = self.highlight(code: rawContent, language: lang) + return "
\(highlighted)
" } else { // No language and no default: just re-escape the raw content. - highlighted = HTMLEscaping.escape(rawContent) - classAttr = "" + let highlighted = HTMLEscaping.escape(rawContent) + return "
\(highlighted)
" } - - result += "
\(highlighted)
" - cursor = wholeRange.location + wholeRange.length } - - result += ns.substring(with: NSRange(location: cursor, length: ns.length - cursor)) - return result } }