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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.resolved

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

32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion Plugin/blueprints/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Read `<Name>.md` first, then copy files from `<Name>/`.
| `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) | – |

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 7 additions & 5 deletions Sources/SiteKit/Pipeline/SiteBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -731,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 {
Expand Down
24 changes: 21 additions & 3 deletions Sources/SiteKit/Plugins/DocC/DocCLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -119,15 +137,15 @@ 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
}

// Apply build-time syntax highlighting to the community body. This is scoped to
// 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
)
Expand Down
149 changes: 124 additions & 25 deletions Sources/SiteKit/Resources/DocC/docc.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<code class="language-...">) and the
class-less <code> of an un-highlighted block. */
Expand All @@ -4062,43 +4089,115 @@ 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 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;
}
[data-theme="dark"] .sk-article-body pre.sk-docc-highlight {
background: #1f1f24;
}

/* Comments: muted, italicised – conventional across editors. */
/* Comments: muted grey, italicised – conventional across editors. */
.sk-docc-highlight .sk-tok-comment {
color: color-mix(in srgb, var(--color-text-secondary, var(--color-text)) 70%, transparent);
color: #707F8C;
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. */
.sk-docc-highlight .sk-tok-string {
color: color-mix(in srgb, var(--color-accent) 80%, #22c55e);
}

/* Numeric literals: a complementary warm tone. */
.sk-docc-highlight .sk-tok-number {
color: color-mix(in srgb, var(--color-accent) 60%, #f59e0b);
/* Keywords, and #if/#else directives: Apple pink – the strongest semantic signal. */
.sk-docc-highlight .sk-tok-keyword {
color: #AD3DA4;
font-weight: 600;
}

/* Keywords: accent color (the strongest semantic signal). */
.sk-docc-highlight .sk-tok-keyword {
color: var(--color-accent);
/* Booleans and nil: the same pink as keywords. */
.sk-docc-highlight .sk-tok-boolean {
color: #AD3DA4;
font-weight: 600;
}

/* Type names: a slightly lighter or shifted accent-adjacent tone. */
/* 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: color-mix(in srgb, var(--color-accent) 75%, #818cf8);
color: #703DAA;
}

/* Attributes (@MainActor, @State, …): a purple-shifted tone distinct from keywords. */
/* 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;
}

/* Variable references: green – the headline of the semantic-near palette. */
.sk-docc-highlight .sk-tok-variable {
color: #3C7D3C;
}

/* 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: color-mix(in srgb, var(--color-accent) 55%, #a78bfa);
color: #947100;
}

/* 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-keyword {
color: #FF7AB2;
}
[data-theme="dark"] .sk-docc-highlight .sk-tok-boolean {
color: #FF7AB2;
}
[data-theme="dark"] .sk-docc-highlight .sk-tok-type {
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: #CC9768;
}
Loading
Loading