diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9b65dd6..8e81282 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -40,3 +40,16 @@ jobs:
xcode: ${{ matrix.xcode }}
- name: Run Unit Tests
run: swift test
+
+ site-build:
+ name: Site Build
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v6
+ - uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ - name: Build site
+ run: bundle exec rake site:build
+ env:
+ JEKYLL_ENV: production
diff --git a/.gitignore b/.gitignore
index b69502f..7381c9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
.build
.swiftpm
.DS_Store
-.vscode
\ No newline at end of file
+.vscode
+
+# The site generated by Jekyll.
+_site
diff --git a/Gemfile b/Gemfile
index 9c085a2..48506b8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,4 @@
-source 'https://rubygems.org' do
- gem "rake", "~> 13.0.0"
-end
+source 'https://rubygems.org'
+
+gem "rake", "~> 13.0.0"
+gem "jekyll", "~> 4.4.1"
diff --git a/Gemfile.lock b/Gemfile.lock
index 2ffc012..48e937e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,17 +1,89 @@
-GEM
- specs:
-
GEM
remote: https://rubygems.org/
specs:
+ addressable (2.8.7)
+ public_suffix (>= 2.0.2, < 7.0)
+ base64 (0.3.0)
+ bigdecimal (3.3.1)
+ colorator (1.1.0)
+ concurrent-ruby (1.3.5)
+ csv (3.3.5)
+ em-websocket (0.5.3)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0)
+ eventmachine (1.2.7)
+ ffi (1.17.2)
+ ffi (1.17.2-arm64-darwin)
+ forwardable-extended (2.6.0)
+ google-protobuf (4.33.1)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.33.1-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
+ http_parser.rb (0.8.0)
+ i18n (1.14.7)
+ concurrent-ruby (~> 1.0)
+ jekyll (4.4.1)
+ addressable (~> 2.4)
+ base64 (~> 0.2)
+ colorator (~> 1.0)
+ csv (~> 3.0)
+ em-websocket (~> 0.5)
+ i18n (~> 1.0)
+ jekyll-sass-converter (>= 2.0, < 4.0)
+ jekyll-watch (~> 2.0)
+ json (~> 2.6)
+ kramdown (~> 2.3, >= 2.3.1)
+ kramdown-parser-gfm (~> 1.0)
+ liquid (~> 4.0)
+ mercenary (~> 0.3, >= 0.3.6)
+ pathutil (~> 0.9)
+ rouge (>= 3.0, < 5.0)
+ safe_yaml (~> 1.0)
+ terminal-table (>= 1.8, < 4.0)
+ webrick (~> 1.7)
+ jekyll-sass-converter (3.1.0)
+ sass-embedded (~> 1.75)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ json (2.16.0)
+ kramdown (2.5.1)
+ rexml (>= 3.3.9)
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.9.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ mercenary (0.4.0)
+ pathutil (0.16.2)
+ forwardable-extended (~> 2.6)
+ public_suffix (6.0.2)
rake (13.0.6)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.11.1)
+ ffi (~> 1.0)
+ rexml (3.4.4)
+ rouge (4.6.1)
+ safe_yaml (1.0.5)
+ sass-embedded (1.94.2)
+ google-protobuf (~> 4.31)
+ rake (>= 13)
+ sass-embedded (1.94.2-arm64-darwin)
+ google-protobuf (~> 4.31)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ unicode-display_width (2.6.0)
+ webrick (1.9.1)
PLATFORMS
arm64-darwin-23
ruby
DEPENDENCIES
- rake (~> 13.0.0)!
+ jekyll (~> 4.4.1)
+ rake (~> 13.0.0)
BUNDLED WITH
2.5.4
diff --git a/README.md b/README.md
index 0f8c97e..20c6b4f 100644
--- a/README.md
+++ b/README.md
@@ -1056,6 +1056,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
]
```
+
+
* (link) [Long](https://github.com/airbnb/swift#column-width) type aliases of protocol compositions should wrap before the `=` and before each individual `&`. [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#wrapArguments)
@@ -1084,6 +1086,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
& UniverseSimulatorServiceProviding
```
+
+
* (link) **Sort protocol composition type aliases alphabetically.** [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#sortTypealiases)
@@ -1110,6 +1114,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
& UniverseSimulatorServiceProviding
```
+
+
* (link) Omit the right-hand side of the expression when unwrapping an optional property to a non-optional property with the same name. [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#redundantOptionalBinding)
@@ -1142,6 +1148,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
else { … }
```
+
+
* (link) **Else statements should start on the same line as the previous condition's closing brace, unless the conditions are separated by a blank line or comments.** [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#elseOnSameLine)
@@ -1196,6 +1204,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```
+
+
* (link) **Multi-line conditional statements should break after the leading keyword.** Indent each individual statement by [2 spaces](https://github.com/airbnb/swift#spaces-over-tabs). [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#wrapArguments)
@@ -1609,6 +1619,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```
+
+
* (link) **Indent the body and closing triple-quote of multiline string literals**, unless the string literal begins on its own line in which case the string literal contents and closing triple-quote should have the same indentation as the opening triple-quote. [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#indent)
@@ -1779,6 +1791,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```
+
+
* (link) For function calls and declarations, there should be no spaces before or inside the parentheses of the argument list. [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#spaceInsideParens) [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#spaceAroundParens)
@@ -3010,6 +3024,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```
+
+
* (link) **Avoid global functions whenever possible.** Prefer methods within type definitions.
@@ -4694,6 +4710,8 @@ _You can enable the following settings in Xcode by running [this script](resourc
}
```
+
+
* (link) **Prefer throwing tests to `try!`**. `try!` will crash your test suite like a force-unwrapped optional. XCTest and Swift Testing support throwing test methods, so use that instead. [](https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md#noForceTryInTests)
diff --git a/Rakefile b/Rakefile
index 1dbdf0d..a31cc5f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -30,14 +30,14 @@ namespace :update do
temp_dir = Dir.mktmpdir
artifact_bundle_url = "https://github.com/calda/SwiftFormat-nightly/releases/download/#{latest_version_number}/swiftformat.artifactbundle.zip"
artifact_bundle_zip_path = "#{temp_dir}/swiftformat.artifactbundle.zip"
-
+
sh "curl #{artifact_bundle_url} -L --output #{artifact_bundle_zip_path}"
checksum = `swift package compute-checksum #{artifact_bundle_zip_path}`
# Update the Package.swift file to reference this version
package_manifest_path = 'Package.swift'
package_manifest_content = File.read(package_manifest_path)
-
+
updated_swift_format_reference = <<-EOS
.binaryTarget(
name: "swiftformat",
@@ -45,11 +45,40 @@ namespace :update do
checksum: "#{checksum.strip}"
),
EOS
-
+
regex = /[ ]*.binaryTarget\([\S\s]*name: "swiftformat"[\S\s]*?\),\s/
updated_package_manifest = package_manifest_content.gsub(regex, updated_swift_format_reference)
File.open(package_manifest_path, "w") { |file| file.puts updated_package_manifest }
-
+
puts "Updated Package.swift to reference SwiftFormat #{latest_version_number}"
end
end
+
+namespace :site do
+ desc 'Prints the README content used to build the site'
+ task :filter_readme do
+ require_relative 'site/site_content'
+ puts SiteContent.new.filter_readme
+ end
+
+ desc 'Prepares index.md and syntax highlighting assets'
+ task :prepare do
+ require_relative 'site/site_content'
+ puts '📋 Generating index.md from README.md with frontmatter...'
+ SiteContent.new.write_index
+ puts '🎨 Generating syntax highlighting CSS...'
+ SiteContent.new.generate_syntax_css
+ end
+
+ desc 'Builds the static site into _site/'
+ task build: :prepare do
+ env = { 'JEKYLL_ENV' => ENV.fetch('JEKYLL_ENV', 'production') }
+ sh env, 'bundle exec jekyll build --source site/src'
+ end
+
+ desc 'Serves the site to support previewing its content during development'
+ task serve: :prepare do
+ env = { 'JEKYLL_ENV' => 'development' }
+ sh env, 'bundle exec jekyll serve --source site/src'
+ end
+end
diff --git a/site/.gitignore b/site/.gitignore
new file mode 100644
index 0000000..46dab74
--- /dev/null
+++ b/site/.gitignore
@@ -0,0 +1,3 @@
+# These site source files are generated.
+src/index.md
+src/assets/css/syntax.css
diff --git a/site/README.md b/site/README.md
new file mode 100644
index 0000000..6f9e17d
--- /dev/null
+++ b/site/README.md
@@ -0,0 +1,11 @@
+# Website
+
+## Local development
+
+To start the local development server, run the following command:
+
+```bash
+bundle exec rake site:serve
+```
+
+Once the server is running, open [http://localhost:4000](http://localhost:4000) in your browser to preview the site.
diff --git a/site/site_content.rb b/site/site_content.rb
new file mode 100644
index 0000000..f086e98
--- /dev/null
+++ b/site/site_content.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'open3'
+
+class SiteContent
+ attr_reader :readme_path, :index_path, :syntax_css_path
+
+ def initialize()
+ site_dir = File.expand_path('src', __dir__)
+ @readme_path = File.expand_path('../README.md', __dir__)
+ @index_path = File.join(site_dir, 'index.md')
+ @syntax_css_path = File.join(site_dir, 'assets/css/syntax.css')
+ end
+
+ def filter_readme
+ (filter_readme_lines + ['']).join("\n")
+ end
+
+ # Write index.md file.
+ def write_index
+ File.write(index_path, generate_front_matter + filter_readme)
+ end
+
+ # Write syntax.css file.
+ def generate_syntax_css
+ stdout, stderr, status = Open3.capture3('bundle', 'exec', 'rougify', 'style', 'github.light')
+ raise "rougify failed:\n#{stderr}" unless status.success?
+
+ File.write(syntax_css_path, stdout)
+ end
+
+ private
+
+ def generate_front_matter
+ <<~FRONT
+ ---
+ layout: default
+ ---
+
+ FRONT
+ end
+
+ def filter_readme_lines
+ lines = File.readlines(readme_path, chomp: true)
+ filtered = []
+ skip_plugin_section = false
+ skip_amendments_section = false
+
+ lines.each do |line|
+ # Exclude the SPM command plugin section from the site.
+ if line.start_with?('## Swift Package Manager command plugin')
+ skip_plugin_section = true
+ next
+ elsif skip_plugin_section
+ skip_plugin_section = false if line.start_with?('## ')
+ next if skip_plugin_section
+ end
+
+ if line.start_with?('## Amendments')
+ skip_amendments_section = true
+ next
+ elsif skip_amendments_section
+ skip_amendments_section = false if line.start_with?('** ')
+ next if skip_amendments_section
+ end
+
+ # Exclude the badges from the site.
+ stripped = line.strip
+ next if stripped.start_with?('[ && stripped.include?('swiftpackageindex.com')
+
+ filtered << line.rstrip
+ end
+
+ filtered
+ end
+end
diff --git a/site/src/_config.yml b/site/src/_config.yml
new file mode 100644
index 0000000..04594c4
--- /dev/null
+++ b/site/src/_config.yml
@@ -0,0 +1,6 @@
+title: Airbnb Swift Style Guide
+github_url: https://github.com/airbnb/swift
+markdown: kramdown
+kramdown:
+ input: GFM
+ parse_block_html: true
diff --git a/site/src/_layouts/default.html b/site/src/_layouts/default.html
new file mode 100644
index 0000000..3e1fbca
--- /dev/null
+++ b/site/src/_layouts/default.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ {{ page.title | default: site.title }}
+
+
+
+
+ {{ content }}
+
+
+
diff --git a/site/src/assets/css/style.css b/site/src/assets/css/style.css
new file mode 100644
index 0000000..dd98738
--- /dev/null
+++ b/site/src/assets/css/style.css
@@ -0,0 +1,38 @@
+body {
+ font-family: system-ui, sans-serif;
+ line-height: 1.6;
+ margin: 0 auto;
+ padding: 3rem 2rem 2rem;
+ max-width: 900px;
+}
+
+pre,
+code {
+ font-family: 'SF Mono', Monaco, Menlo, 'Courier New', monospace;
+}
+
+:not(pre) > code {
+ background: #f5f5f5;
+ padding: 0.1rem 0.35rem;
+ border-radius: 3px;
+ font-size: 0.9em;
+}
+
+pre {
+ margin: 1.5rem 0;
+ padding: 1rem;
+ overflow-x: auto;
+}
+
+footer {
+ margin-top: 3rem;
+ padding-top: 1rem;
+ border-top: 1px solid #ddd;
+ text-align: center;
+ font-weight: 600;
+}
+
+footer a {
+ color: #555;
+ text-decoration: none;
+}