diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b2c0622..1027d0a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,5 +23,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v1 + - name: Install System Dependencies + run: | + apt-get update + apt-get install -y libxml2-dev - name: Build and Test run: swift test --enable-test-discovery diff --git a/Package.resolved b/Package.resolved index 76827592..b01d7a92 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,13 +15,31 @@ "repositoryURL": "https://github.com/SwiftDocOrg/GraphViz.git", "state": { "branch": null, - "revision": "08e0cddd013fa2272379d27ec3e0093db51f34fb", - "version": "0.1.0" + "revision": "03405c13dc1c31f50c08bbec6e7587cbee1c7fb3", + "version": null + } + }, + { + "package": "HypertextLiteral", + "repositoryURL": "https://github.com/NSHipster/HypertextLiteral.git", + "state": { + "branch": null, + "revision": "3e45da849e507d171c7264146176bb834a01be4f", + "version": "0.0.2" + } + }, + { + "package": "Markup", + "repositoryURL": "https://github.com/SwiftDocOrg/Markup.git", + "state": { + "branch": null, + "revision": "9a429d0011d738059bc94f5f92ee406689597a91", + "version": "0.0.3" } }, { "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, "revision": "8d31a0905c346a45c87773ad50862b5b3df8dff6", @@ -37,6 +55,15 @@ "version": "0.28.3+20200207.1168665" } }, + { + "package": "HTMLEntities", + "repositoryURL": "https://github.com/IBM-Swift/swift-html-entities.git", + "state": { + "branch": null, + "revision": "744c094976355aa96ca61b9b60ef0a38e979feb7", + "version": "3.0.14" + } + }, { "package": "SwiftSyntax", "repositoryURL": "https://github.com/apple/swift-syntax.git", @@ -51,16 +78,25 @@ "repositoryURL": "https://github.com/SwiftDocOrg/SwiftMarkup.git", "state": { "branch": null, - "revision": "8e82d625b0342fc80525956c22f9f0defa0cffce", - "version": "0.0.4" + "revision": "431f418ae1833a312646e934a2891e778c1b03b0", + "version": "0.0.5" } }, { "package": "SwiftSemantics", "repositoryURL": "https://github.com/SwiftDocOrg/SwiftSemantics.git", "state": { - "branch": "swift-5.2", + "branch": null, "revision": "4fdc48bddbbb8311079ed111e5a4f2b92423b05c", + "version": "0.1.0" + } + }, + { + "package": "SwiftSyntaxHighlighter", + "repositoryURL": "https://github.com/NSHipster/SwiftSyntaxHighlighter.git", + "state": { + "branch": "1.0.0", + "revision": "4a20d10bba17241b66650d99081801536146b43c", "version": null } } diff --git a/Package.swift b/Package.swift index 7b4e973e..667833a4 100644 --- a/Package.swift +++ b/Package.swift @@ -6,22 +6,26 @@ import PackageDescription let package = Package( name: "swift-doc", products: [ + .executable(name: "swift-doc", targets: ["swift-doc"]), .library(name: "SwiftDoc", targets: ["SwiftDoc"]) ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", .revision("0.50200.0")), .package(url: "https://github.com/SwiftDocOrg/SwiftSemantics.git", .upToNextMinor(from: "0.1.0")), .package(url: "https://github.com/SwiftDocOrg/CommonMark.git", .branch("master")), - .package(url: "https://github.com/SwiftDocOrg/SwiftMarkup.git", .upToNextMinor(from: "0.0.4")), - .package(url: "https://github.com/SwiftDocOrg/GraphViz.git", .upToNextMinor(from: "0.1.0")), - .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.0.2")), + .package(url: "https://github.com/SwiftDocOrg/SwiftMarkup.git", .upToNextMinor(from: "0.0.5")), + .package(url: "https://github.com/SwiftDocOrg/GraphViz.git", .revision("03405c13dc1c31f50c08bbec6e7587cbee1c7fb3")), + .package(url: "https://github.com/NSHipster/HypertextLiteral.git", .upToNextMinor(from: "0.0.2")), + .package(url: "https://github.com/SwiftDocOrg/Markup.git", .upToNextMinor(from: "0.0.3")), + .package(url: "https://github.com/NSHipster/SwiftSyntaxHighlighter.git", .revision("1.0.0")), + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.2")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "swift-doc", - dependencies: ["ArgumentParser", "SwiftDoc", "SwiftSemantics", "SwiftMarkup", "CommonMarkBuilder", "DCOV", "GraphViz"] + dependencies: ["ArgumentParser", "SwiftDoc", "SwiftSemantics", "SwiftMarkup", "CommonMarkBuilder", "HypertextLiteral", "Markup", "DCOV", "GraphViz", "SwiftSyntaxHighlighter"] ), .target( name: "DCOV", diff --git a/README.md b/README.md index c55d232c..1793d88c 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,15 @@ A package for generating documentation for Swift projects. -**This project is under active development -and is expected to change significantly before its first stable release.** - Given a directory of Swift files, -`swift-doc` generates CommonMark (Markdown) files +`swift-doc` generates HTML or CommonMark (Markdown) files for each class, structure, enumeration, and protocol as well as top-level type aliases, functions, and variables. -For an example of generated documentation, -[check out the Wiki for our fork of Alamofire][alamofire wiki]. +**Example Output** -> **Note**: -> Output is currently limited to CommonMark, -> but the plan is to support HTML and other formats as well. +- [HTML][swiftsemantics html] +- [CommonMark / GitHub Wiki][alamofire wiki] ## Requirements @@ -54,9 +49,9 @@ collecting all Swift files into a single "module" and generating documentation accordingly. ```terminal -$ swift doc generate path/to/SwiftProject/Sources --output Documentation -$ tree Documentation -$ Documentation/ +$ swift doc generate path/to/SwiftProject/Sources +$ tree .build/documentation +$ documentation/ ├── Home ├── (...) ├── _Footer.md @@ -64,8 +59,16 @@ $ Documentation/ ``` By default, -output files are written to `.build/documentation`, -but you can change that with the `--output` option flag. +output files are written to `.build/documentation` +in CommonMark / GitHub Wiki format, +but you can change that with the `--output` and `--format` option flags. + +```terminal +$ swift doc generate path/to/SwiftProject/Sources --output Documentation --format html +$ Documentation/ +├── (...) +└── index.html +``` #### swift-doc coverage @@ -178,114 +181,9 @@ jobs: with: path: "Documentation" env: - GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} -``` - -* * * - -## Motivation - -From its earliest days, -Swift has been fortunate to have [Jazzy][jazzy], -which is a fantastic tool for generating documentation -for both Swift and Objective-C projects. -Over time, however, -the way we write Swift code — -and indeed the language itself — -has evolved to incorporate patterns and features -that are difficult to understand using -the same documentation standards that served us well for Objective-C. - -Whereas in Objective-C, -you could get a complete view of a type's functionality from its class hierarchy, -Swift code today tends to layer and distribute functionality across -[a network of types][swift number protocols diagram]. -While adopting a -[protocol-oriented paradigm][protocol-oriented programming] -can make Swift easier and more expressive to write, -it can also make Swift code more difficult to understand. - -Our primary goal for `swift-doc` -is to make Swift documentation more useful -by surfacing the information you need to understand how an API works -and presenting it in a way that can be easily searched and accessed. -We want developers to be empowered to use Swift packages to their full extent, -without being reliant on (often outdated) blog posts or Stack Overflow threads. -We want documentation coverage to become as important as test coverage: -a valuable metric for code quality, -and an expected part of first-rate open source projects. - -Jazzy styles itself after Apple's official documentation circa 2014 -(code-named "Jazz", as it were), -which was well-suited to understanding Swift code as we wrote it back then -when it was more similar to Objective-C. -But this design is less capable of documenting the behavior of -generically-constrained types, -default implementations, -[dynamic member lookup][se-0195], -[property wrappers][se-o258], or -[function builders][se-xxxx]. -(Alas, -Apple's [most recent take][apple documentation] on reference documentation -hasn't improved matters, -having instead focused on perceived cosmetic issues.) - -Without much in the way of strong design guidance, -we're not entirely sure what Swift documentation _should_ look like. -But we do think plain text is a good place to start. -We look forward to -soliciting feedback and ideas from everyone -so that we can identify those needs -and figure out the best ways to meet them. - -In the meantime, -we've set ourselves up for success -by investing in the kind of foundation we'll need -to build whatever we decide best solves the problems at hand. -`swift-doc` is built on top of a constellation of projects -that take advantage of modern infrastructure and tooling: - -- [SwiftSemantics][swiftsemantics]: - Parses Swift code into its constituent declarations - using [SwiftSyntax][swiftsyntax] -- [SwiftMarkup][swiftmarkup]: - Parses Swift documentation comments into structured entities - using [CommonMark][commonmark] -- [github-wiki-publish-action][github-wiki-publish-action]: - Publishes the contents of a directory to your project's wiki - -These new technologies have already yielded some promising results. -`swift-doc` is built in Swift, -and can be installed on both macOS and Linux as a small, standalone binary. -Because it relies only on a syntactic reading of Swift source code, -without needing code first to be compiled, -`swift-doc` is quite fast. -As a baseline, -compare its performance to Jazzy -when generating documentation for [SwiftSemantics][swiftsemantics]: - -```terminal -$ cd SwiftSemantics - -$ time swift-doc Sources - 0.21 real 0.16 user 0.02 sys - -$ time jazzy # fresh build -jam out ♪♫ to your fresh new docs in `docs` - 67.36 real 98.76 user 8.89 sys - - -$ time jazzy # with build cache -jam out ♪♫ to your fresh new docs in `docs` - 17.70 real 2.17 user 0.88 sys + GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} ``` -Of course, -some of that is simply Jazzy doing more, -generating HTML, CSS, and a search index instead of just text. -Compare its [generated HTML output][jazzy swiftsemantics] -to [a GitHub wiki generated with `swift-doc`][swift-doc swiftsemantics]. - ## License MIT @@ -297,6 +195,7 @@ Mattt ([@mattt](https://twitter.com/mattt)) [ci badge]: https://github.com/SwiftDocOrg/swift-doc/workflows/CI/badge.svg [alamofire wiki]: https://github.com/SwiftDocOrg/Alamofire/wiki +[swiftsemantics html]: https://swift-doc-preview.netlify.app [github wiki]: https://help.github.com/en/github/building-a-strong-community/about-wikis [github actions]: https://github.com/features/actions [swiftsyntax]: https://github.com/apple/swift-syntax diff --git a/Sources/SwiftDoc/Extensions/Array+Parallel.swift b/Sources/SwiftDoc/Extensions/Array+Parallel.swift index 7a3d6eb0..1e98a7bc 100644 --- a/Sources/SwiftDoc/Extensions/Array+Parallel.swift +++ b/Sources/SwiftDoc/Extensions/Array+Parallel.swift @@ -1,17 +1,21 @@ import Dispatch -extension Array { +public extension RandomAccessCollection { func parallelMap(_ transform: (Element) throws -> T) throws -> [T] { guard count > 1 else { return try map(transform) } - var results = [(index: Int, result: Result)]() + let indices = Array(self.indices) + + var results = [(index: Index, result: Result)]() results.reserveCapacity(count) - let queue = DispatchQueue(label: "org.swiftdoc.swift-doc.parallelMap") + let queue = DispatchQueue(label: #function) withoutActuallyEscaping(transform) { escapingtransform in - DispatchQueue.concurrentPerform(iterations: count) { (index) in + DispatchQueue.concurrentPerform(iterations: count) { (iteration) in + let index = indices[iteration] + do { let transformed = try escapingtransform(self[index]) queue.sync { @@ -36,4 +40,8 @@ extension Array { func parallelFlatMap(transform: (Element) throws -> [T]) throws -> [T] { return try parallelMap(transform).flatMap { $0 } } + + func parallelForEach(_ body: (Element) throws -> Void) throws { + _ = try parallelMap(body) + } } diff --git a/Sources/SwiftDoc/Interface.swift b/Sources/SwiftDoc/Interface.swift index 1157b474..9ca8b626 100644 --- a/Sources/SwiftDoc/Interface.swift +++ b/Sources/SwiftDoc/Interface.swift @@ -13,16 +13,20 @@ public final class Interface: Codable { // MARK: - - private lazy var symbolsByIdentifier: [Symbol.ID: [Symbol]] = { + public lazy var symbolsGroupedByIdentifier: [Symbol.ID: [Symbol]] = { return Dictionary(grouping: symbols, by: { $0.id }) }() + public lazy var symbolsGroupedByQualifiedName: [String: [Symbol]] = { + return Dictionary(grouping: symbols, by: { $0.id.description }) + }() + public private(set) lazy var topLevelSymbols: [Symbol] = { - return symbols.filter { $0.declaration is Type || $0.id.pathComponents.isEmpty } + return symbols.filter { $0.api is Type || $0.id.pathComponents.isEmpty } }() public private(set) lazy var baseClasses: [Symbol] = { - return symbols.filter { $0.declaration is Class && + return symbols.filter { $0.api is Class && typesInherited(by: $0).isEmpty } }() @@ -42,6 +46,12 @@ public final class Interface: Codable { return classClusters }() + private lazy var extensionsByExtendedType: [String: [Extension]] = { + return Dictionary(grouping: symbols.flatMap { $0.context.compactMap { $0 as? Extension } }) { + $0.extendedType + } + }() + public private(set) lazy var relationships: [Relationship] = { var relationships: Set = [] for symbol in symbols { @@ -50,9 +60,9 @@ public final class Interface: Codable { if let container = symbol.context.compactMap({ $0 as? Symbol }).last { let predicate: Relationship.Predicate - switch container.declaration { + switch container.api { case is Protocol: - if symbol.declaration.modifiers.contains(where: { $0.name == "optional" }) { + if symbol.api.modifiers.contains(where: { $0.name == "optional" }) { predicate = .optionalRequirementOf } else { predicate = .requirementOf @@ -65,9 +75,10 @@ public final class Interface: Codable { } if let `extension` = `extension` { - for extended in symbols.filter({ $0.declaration is Type && $0.id.matches(`extension`.extendedType) }) { + if let extended = symbols.first(where: { $0.api is Type && $0.id.matches(`extension`.extendedType) }) { + let predicate: Relationship.Predicate - switch extended.declaration { + switch extended.api { case is Protocol: predicate = .defaultImplementationOf default: @@ -78,17 +89,26 @@ public final class Interface: Codable { } } - if let type = symbol.declaration as? Type { - let inheritance = Set((type.inheritance + (`extension`?.inheritance ?? [])).flatMap { $0.split(separator: "&").map { $0.trimmingCharacters(in: .whitespaces) } }) - for name in inheritance { - let inheritedTypes = symbols.filter({ ($0.declaration is Class || $0.declaration is Protocol) && $0.id.matches(name) }) + if let type = symbol.api as? Type { + var inheritedTypeNames: Set = [] + inheritedTypeNames.formUnion(type.inheritance.flatMap { $0.split(separator: "&").map { $0.trimmingCharacters(in: .whitespaces) } + }) + + for `extension` in extensionsByExtendedType[symbol.id.description] ?? [] { + inheritedTypeNames.formUnion(`extension`.inheritance) + } + + inheritedTypeNames = Set(inheritedTypeNames.flatMap { $0.split(separator: "&").map { $0.trimmingCharacters(in: .whitespaces) } }) + + for name in inheritedTypeNames { + let inheritedTypes = symbols.filter({ ($0.api is Class || $0.api is Protocol) && $0.id.description == name }) if inheritedTypes.isEmpty { - let inherited = Symbol(declaration: Unknown(name: name), context: [], documentation: nil, sourceLocation: nil) - relationships.insert(Relationship(subject: symbol, predicate: .inheritsFrom, object: inherited)) + let inherited = Symbol(api: Unknown(name: name), context: [], declaration: nil, documentation: nil, sourceLocation: nil) + relationships.insert(Relationship(subject: symbol, predicate: .conformsTo, object: inherited)) } else { for inherited in inheritedTypes { let predicate: Relationship.Predicate - if symbol.declaration is Class, inherited.declaration is Class { + if symbol.api is Class, inherited.api is Class { predicate = .inheritsFrom } else { predicate = .conformsTo @@ -104,26 +124,26 @@ public final class Interface: Codable { return Array(relationships) }() - private lazy var relationshipsBySubject: [Symbol.ID: [Relationship]] = { + public private(set) lazy var relationshipsBySubject: [Symbol.ID: [Relationship]] = { Dictionary(grouping: relationships, by: { $0.subject.id }) }() - private lazy var relationshipsByObject: [Symbol.ID: [Relationship]] = { + public private(set) lazy var relationshipsByObject: [Symbol.ID: [Relationship]] = { Dictionary(grouping: relationships, by: { $0.object.id }) }() // MARK: - public func members(of symbol: Symbol) -> [Symbol] { - return relationshipsByObject[symbol.id]?.filter { $0.predicate == .memberOf }.map { $0.subject } ?? [] + return relationshipsByObject[symbol.id]?.filter { $0.predicate == .memberOf }.map { $0.subject }.sorted() ?? [] } public func requirements(of symbol: Symbol) -> [Symbol] { - return relationshipsByObject[symbol.id]?.filter { $0.predicate == .requirementOf }.map { $0.subject } ?? [] + return relationshipsByObject[symbol.id]?.filter { $0.predicate == .requirementOf }.map { $0.subject }.sorted() ?? [] } public func optionalRequirements(of symbol: Symbol) -> [Symbol] { - return relationshipsByObject[symbol.id]?.filter { $0.predicate == .optionalRequirementOf }.map { $0.subject } ?? [] + return relationshipsByObject[symbol.id]?.filter { $0.predicate == .optionalRequirementOf }.map { $0.subject }.sorted() ?? [] } public func typesInherited(by symbol: Symbol) -> [Symbol] { @@ -143,6 +163,6 @@ public final class Interface: Codable { } public func conditionalCounterparts(of symbol: Symbol) -> [Symbol] { - return symbolsByIdentifier[symbol.id]?.filter { $0 != symbol } ?? [] + return symbolsGroupedByIdentifier[symbol.id]?.filter { $0 != symbol }.sorted() ?? [] } } diff --git a/Sources/SwiftDoc/Module.swift b/Sources/SwiftDoc/Module.swift index b8a17c67..009346ed 100644 --- a/Sources/SwiftDoc/Module.swift +++ b/Sources/SwiftDoc/Module.swift @@ -8,7 +8,9 @@ public final class Module: Codable { public let sourceFiles: [SourceFile] public lazy var interface: Interface = { - Interface(imports: sourceFiles.flatMap { $0.imports }, symbols: sourceFiles.flatMap { $0.symbols }) + let imports = sourceFiles.flatMap { $0.imports } + let symbols = sourceFiles.flatMap { $0.symbols } + return Interface(imports: imports, symbols: symbols) }() public required init(name: String = "Anonymous", sourceFiles: [SourceFile]) { diff --git a/Sources/SwiftDoc/SourceFile.swift b/Sources/SwiftDoc/SourceFile.swift index 63cff6bc..f5541d1b 100644 --- a/Sources/SwiftDoc/SourceFile.swift +++ b/Sources/SwiftDoc/SourceFile.swift @@ -35,9 +35,12 @@ public struct SourceFile: Hashable, Codable { var visitedSymbols: [Symbol] = [] var visitedImports: [Import] = [] + let fileURL: URL let sourceLocationConverter: SourceLocationConverter init(file url: URL, relativeTo directory: URL) throws { + self.fileURL = url + let tree = try SyntaxParser.parse(url) sourceLocationConverter = SourceLocationConverter(file: url.path(relativeTo: directory), tree: tree) super.init() @@ -48,21 +51,22 @@ public struct SourceFile: Hashable, Codable { } func symbol(_ type: Declaration.Type, _ node: Node) -> Symbol? where Declaration: API & ExpressibleBySyntax, Node == Declaration.Syntax { - guard let declaration = Declaration(node) else { return nil } - return symbol(node, declaration: declaration) + guard let api = Declaration(node) else { return nil } + return symbol(node, api: api) } - func symbol(_ node: Node, declaration: API) -> Symbol? { + func symbol(_ node: Node, api: API) -> Symbol? { guard let documentation = try? Documentation.parse(node.documentation) else { return nil } let sourceLocation = sourceLocationConverter.location(for: node.position) - return Symbol(declaration: declaration, context: context, documentation: documentation, sourceLocation: sourceLocation) + + return Symbol(api: api, context: context, declaration: "\(api)", documentation: documentation, sourceLocation: sourceLocation) } func push(_ symbol: Symbol?) { guard let symbol = symbol else { return } visitedSymbols.append(symbol) - switch symbol.declaration { + switch symbol.api { case is Class, is Enumeration, is Protocol, @@ -108,7 +112,7 @@ public struct SourceFile: Hashable, Codable { override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { for `case` in Enumeration.Case.cases(from: node) { - push(symbol(node, declaration: `case`)) + push(symbol(node, api: `case`)) } return .skipChildren } @@ -175,7 +179,7 @@ public struct SourceFile: Hashable, Codable { override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { for variable in Variable.variables(from: node) { - push(symbol(node, declaration: variable)) + push(symbol(node, api: variable)) } return .skipChildren } @@ -183,11 +187,11 @@ public struct SourceFile: Hashable, Codable { // MARK: - override func visitPost(_ node: ClassDeclSyntax) { - assert((pop() as? Symbol)?.declaration is Class) + assert((pop() as? Symbol)?.api is Class) } override func visitPost(_ node: EnumDeclSyntax) { - assert((pop() as? Symbol)?.declaration is Enumeration) + assert((pop() as? Symbol)?.api is Enumeration) } override func visitPost(_ node: ExtensionDeclSyntax) { @@ -199,11 +203,11 @@ public struct SourceFile: Hashable, Codable { } override func visitPost(_ node: ProtocolDeclSyntax) { - assert((pop() as? Symbol)?.declaration is Protocol) + assert((pop() as? Symbol)?.api is Protocol) } override func visitPost(_ node: StructDeclSyntax) { - assert((pop() as? Symbol)?.declaration is Structure) + assert((pop() as? Symbol)?.api is Structure) } } } diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index e7683707..c41f5933 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -5,23 +5,25 @@ import SwiftSemantics public final class Symbol { public typealias ID = Identifier - public let declaration: API + public let api: API public let context: [Contextual] + public let declaration: String public let documentation: Documentation? public let sourceLocation: SourceLocation? public private(set) lazy var `extension`: Extension? = context.compactMap { $0 as? Extension }.first public private(set) lazy var conditions: [CompilationCondition] = context.compactMap { $0 as? CompilationCondition } - init(declaration: API, context: [Contextual], documentation: Documentation?, sourceLocation: SourceLocation?) { - self.declaration = declaration + init(api: API, context: [Contextual], declaration: String?, documentation: Documentation?, sourceLocation: SourceLocation?) { + self.api = api self.context = context + self.declaration = declaration ?? "\(api)" self.documentation = documentation self.sourceLocation = sourceLocation } public var name: String { - return declaration.name + return api.name } public private(set) lazy var id: ID = { @@ -31,7 +33,7 @@ public final class Symbol { }() public var isPublic: Bool { - if declaration.modifiers.contains(where: { $0.name == "public" || $0.name == "open" }) { + if api.modifiers.contains(where: { $0.name == "public" || $0.name == "open" }) { return true } @@ -41,14 +43,14 @@ public final class Symbol { return true } - if let symbol = context.compactMap({ $0 as? Symbol }).first, - symbol.declaration.modifiers.contains(where: { $0.name == "public" }) + if let symbol = context.compactMap({ $0 as? Symbol }).last, + symbol.api.modifiers.contains(where: { $0.name == "public" }) { - switch symbol.declaration { + switch symbol.api { case is Enumeration: - return declaration is Enumeration.Case + return api is Enumeration.Case case is Protocol: - return declaration is Function || declaration is Variable + return api is Function || api is Variable default: break } @@ -84,7 +86,7 @@ extension Symbol: Equatable { } } - switch (lhs.declaration, rhs.declaration) { + switch (lhs.api, rhs.api) { case let (ls, rs) as (AssociatedType, AssociatedType): return ls == rs case let (ls, rs) as (Class, Class): @@ -123,7 +125,11 @@ extension Symbol: Equatable { extension Symbol: Comparable { public static func < (lhs: Symbol, rhs: Symbol) -> Bool { - return lhs.name < rhs.name + if let lsl = lhs.sourceLocation, let rsl = rhs.sourceLocation { + return lsl < rsl + } else { + return lhs.name < rhs.name + } } } @@ -133,7 +139,7 @@ extension Symbol: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(documentation) hasher.combine(sourceLocation) - switch declaration { + switch api { case let api as AssociatedType: hasher.combine(api) case let api as Class: @@ -172,6 +178,7 @@ extension Symbol: Hashable { extension Symbol: Codable { private enum CodingKeys: String, CodingKey { + case declaration case documentation case sourceLocation @@ -194,79 +201,80 @@ extension Symbol: Codable { public convenience init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let declaration: API + let api: API if container.contains(.associatedType) { - declaration = try container.decode(AssociatedType.self, forKey: .associatedType) + api = try container.decode(AssociatedType.self, forKey: .associatedType) } else if container.contains(.`case`) { - declaration = try container.decode(Enumeration.Case.self, forKey: .case) + api = try container.decode(Enumeration.Case.self, forKey: .case) } else if container.contains(.`class`) { - declaration = try container.decode(Class.self, forKey: .class) + api = try container.decode(Class.self, forKey: .class) } else if container.contains(.enumeration) { - declaration = try container.decode(Enumeration.self, forKey: .enumeration) + api = try container.decode(Enumeration.self, forKey: .enumeration) } else if container.contains(.function) { - declaration = try container.decode(Function.self, forKey: .function) + api = try container.decode(Function.self, forKey: .function) } else if container.contains(.initializer) { - declaration = try container.decode(Initializer.self, forKey: .initializer) + api = try container.decode(Initializer.self, forKey: .initializer) } else if container.contains(.`operator`) { - declaration = try container.decode(Operator.self, forKey: .operator) + api = try container.decode(Operator.self, forKey: .operator) } else if container.contains(.precedenceGroup) { - declaration = try container.decode(PrecedenceGroup.self, forKey: .precedenceGroup) + api = try container.decode(PrecedenceGroup.self, forKey: .precedenceGroup) } else if container.contains(.`protocol`) { - declaration = try container.decode(Protocol.self, forKey: .protocol) + api = try container.decode(Protocol.self, forKey: .protocol) } else if container.contains(.structure) { - declaration = try container.decode(Structure.self, forKey: .structure) + api = try container.decode(Structure.self, forKey: .structure) } else if container.contains(.`subscript`) { - declaration = try container.decode(Subscript.self, forKey: .subscript) + api = try container.decode(Subscript.self, forKey: .subscript) } else if container.contains(.`typealias`) { - declaration = try container.decode(Typealias.self, forKey: .typealias) + api = try container.decode(Typealias.self, forKey: .typealias) } else if container.contains(.variable) { - declaration = try container.decode(Variable.self, forKey: .variable) + api = try container.decode(Variable.self, forKey: .variable) } else if container.contains(.unknown) { - declaration = try container.decode(Unknown.self, forKey: .variable) + api = try container.decode(Unknown.self, forKey: .variable) } else { let context = DecodingError.Context(codingPath: container.codingPath, debugDescription: "missing declaration") throw DecodingError.dataCorrupted(context) } - let documentation = try container.decode(Documentation.self, forKey: .documentation) - let sourceLocation = try container.decode(SourceLocation.self, forKey: .sourceLocation) + let declaration = try container.decodeIfPresent(String.self, forKey: .declaration) + let documentation = try container.decodeIfPresent(Documentation.self, forKey: .documentation) + let sourceLocation = try container.decodeIfPresent(SourceLocation.self, forKey: .sourceLocation) - self.init(declaration: declaration, context: [] /* TODO */, documentation: documentation, sourceLocation: sourceLocation) + self.init(api: api, context: [] /* TODO */, declaration: declaration, documentation: documentation, sourceLocation: sourceLocation) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - if let declaration = declaration as? AssociatedType { + if let declaration = api as? AssociatedType { try container.encode(declaration, forKey: .associatedType) - } else if let declaration = declaration as? Class { + } else if let declaration = api as? Class { try container.encode(declaration, forKey: .class) - } else if let declaration = declaration as? Enumeration { + } else if let declaration = api as? Enumeration { try container.encode(declaration, forKey: .enumeration) - } else if let declaration = declaration as? Enumeration.Case { + } else if let declaration = api as? Enumeration.Case { try container.encode(declaration, forKey: .case) - } else if let declaration = declaration as? Function { + } else if let declaration = api as? Function { try container.encode(declaration, forKey: .function) - } else if let declaration = declaration as? Initializer { + } else if let declaration = api as? Initializer { try container.encode(declaration, forKey: .initializer) - } else if let declaration = declaration as? Operator { + } else if let declaration = api as? Operator { try container.encode(declaration, forKey: .operator) - } else if let declaration = declaration as? PrecedenceGroup { + } else if let declaration = api as? PrecedenceGroup { try container.encode(declaration, forKey: .precedenceGroup) - } else if let declaration = declaration as? Protocol { + } else if let declaration = api as? Protocol { try container.encode(declaration, forKey: .protocol) - } else if let declaration = declaration as? Structure { + } else if let declaration = api as? Structure { try container.encode(declaration, forKey: .structure) - } else if let declaration = declaration as? Subscript { + } else if let declaration = api as? Subscript { try container.encode(declaration, forKey: .subscript) - } else if let declaration = declaration as? Typealias { + } else if let declaration = api as? Typealias { try container.encode(declaration, forKey: .typealias) - } else if let declaration = declaration as? Variable { + } else if let declaration = api as? Variable { try container.encode(declaration, forKey: .variable) - } else if let declaration = declaration as? Unknown { + } else if let declaration = api as? Unknown { try container.encode(declaration, forKey: .unknown) } else { let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "unhandled declaration type") - throw EncodingError.invalidValue(declaration, context) + throw EncodingError.invalidValue(api, context) } try container.encode(documentation, forKey: .documentation) diff --git a/Sources/swift-doc/Extensions/CommonMark+Extensions.swift b/Sources/swift-doc/Extensions/CommonMark+Extensions.swift new file mode 100644 index 00000000..ccc7d332 --- /dev/null +++ b/Sources/swift-doc/Extensions/CommonMark+Extensions.swift @@ -0,0 +1,8 @@ +import CommonMark +import HypertextLiteral + +extension CommonMark.Document: HypertextLiteralConvertible { + public var html: HypertextLiteral.HTML { + HypertextLiteral.HTML(render(format: .html, options: .unsafe)) + } +} diff --git a/Sources/swift-doc/Extensions/DCOV+Extensions.swift b/Sources/swift-doc/Extensions/DCOV+Extensions.swift index 78704a7d..fbd423cd 100644 --- a/Sources/swift-doc/Extensions/DCOV+Extensions.swift +++ b/Sources/swift-doc/Extensions/DCOV+Extensions.swift @@ -5,7 +5,7 @@ import SwiftSemantics extension Entry { public init(_ symbol: Symbol) { let name = symbol.id.description - let type = String(describing: Swift.type(of: symbol.declaration)) + let type = String(describing: Swift.type(of: symbol.api)) let documented = symbol.isDocumented let file = symbol.sourceLocation?.file let line = symbol.sourceLocation?.line diff --git a/Sources/swift-doc/Extensions/HypertextLiteral+Extensions.swift b/Sources/swift-doc/Extensions/HypertextLiteral+Extensions.swift new file mode 100644 index 00000000..367e2258 --- /dev/null +++ b/Sources/swift-doc/Extensions/HypertextLiteral+Extensions.swift @@ -0,0 +1,9 @@ +import HypertextLiteral +import CommonMark + +extension HypertextLiteral.HTML.StringInterpolation { + mutating func appendInterpolation(commonmark: String) { + guard let document = try? Document(commonmark) else { return } + appendLiteral(document.render(format: .html, options: [.unsafe])) + } +} diff --git a/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift b/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift new file mode 100644 index 00000000..6a0c306a --- /dev/null +++ b/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift @@ -0,0 +1,81 @@ +import SwiftDoc +import SwiftSemantics +import GraphViz +import DOT +import struct Foundation.URL + +extension Symbol { + var node: Node { + var node = Node(id.description) + node.fontName = "Menlo" + node.shape = .box + node.style = .rounded + + node.width = 3 + node.height = 0.5 + node.fixedSize = .shape + + if !(api is Unknown) { + node.href = "/" + path(for: self) + } + + switch api { + case let `class` as Class: + node.class = "class" + if `class`.modifiers.contains(where: { $0.name == "final" }) { + node.strokeWidth = 2.0 + } + case is Enumeration: + node.class = "enumeration" + case is Structure: + node.class = "structure" + case is Protocol: + node.class = "protocol" + case is Unknown: + node.class = "unknown" + default: + break + } + + return node + } + + func graph(in module: Module) -> Graph { + var graph = Graph(directed: true) + + let relationships = module.interface.relationships.filter { + ($0.predicate == .inheritsFrom || $0.predicate == .conformsTo) && + ($0.subject == self || $0.object == self) + } + + var symbolNode = self.node + symbolNode.strokeWidth = 3.0 + symbolNode.class = [symbolNode.class, "current"].compactMap { $0 }.joined(separator: " ") + + graph.append(symbolNode) + + for node in Set(relationships.flatMap { [$0.subject.node, $0.object.node] }) where node.id != symbolNode.id { + graph.append(node) + } + + for relationship in relationships { + let edge = relationship.edge + graph.append(edge) + } + + return graph + } +} + +extension Relationship { + var edge: Edge { + let from = subject.node + let to = object.node + + var edge = Edge(from: from.id, to: to.id) + edge.class = predicate.rawValue + edge.preferredEdgeLength = 1.5 + + return edge + } +} diff --git a/Sources/swift-doc/Subcommands/Diagram.swift b/Sources/swift-doc/Subcommands/Diagram.swift index 4b0f6edf..50c72443 100644 --- a/Sources/swift-doc/Subcommands/Diagram.swift +++ b/Sources/swift-doc/Subcommands/Diagram.swift @@ -5,6 +5,7 @@ import SwiftSemantics import GraphViz import DOT + extension SwiftDoc { struct Diagram: ParsableCommand { struct Options: ParsableArguments { @@ -36,7 +37,7 @@ fileprivate func diagram(of module: Module) -> String { var subclassNode = Node("\(subclass.id)") subclassNode.shape = .box - if subclass.declaration.modifiers.contains(where: { $0.name == "final" }) { + if subclass.api.modifiers.contains(where: { $0.name == "final" }) { subclassNode.strokeWidth = 2.0 } @@ -60,7 +61,7 @@ fileprivate func diagram(of module: Module) -> String { } - for symbol in (module.interface.symbols.filter { $0.isPublic && $0.declaration is Type }) { + for symbol in (module.interface.symbols.filter { $0.isPublic && $0.api is Type }) { let symbolNode = Node("\(symbol.id)") graph.append(symbolNode) diff --git a/Sources/swift-doc/Subcommands/Generate.swift b/Sources/swift-doc/Subcommands/Generate.swift index 89269f8c..1fbd1f47 100644 --- a/Sources/swift-doc/Subcommands/Generate.swift +++ b/Sources/swift-doc/Subcommands/Generate.swift @@ -7,14 +7,28 @@ import SwiftDoc extension SwiftDoc { struct Generate: ParsableCommand { + enum Format: String, ExpressibleByArgument { + case commonmark + case html + } + struct Options: ParsableArguments { @Argument(help: "One or more paths to Swift files") var inputs: [String] + @Option(name: [.long, .customShort("n")], + help: "The name of the module") + var moduleName: String + @Option(name: .shortAndLong, default: ".build/documentation", help: "The path for generated output") var output: String + + @Option(name: .shortAndLong, + default: .commonmark, + help: "The output format") + var format: Format } static var configuration = CommandConfiguration(abstract: "Generates Swift documentation") @@ -23,29 +37,32 @@ extension SwiftDoc { var options: Options func run() throws { - let module = try Module(paths: options.inputs) + let module = try Module(name: options.moduleName, paths: options.inputs) let outputDirectoryURL = URL(fileURLWithPath: options.output) try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) do { - try HomePage(module: module).write(to: outputDirectoryURL.appendingPathComponent("Home.md")) - try SidebarPage(module: module).write(to: outputDirectoryURL.appendingPathComponent("_Sidebar.md")) - try FooterPage().write(to: outputDirectoryURL.appendingPathComponent("_Footer.md")) + let format = options.format + + var pages: [String: Page] = [:] + + switch format { + case .commonmark: + pages["Home"] = HomePage(module: module) + pages["_Sidebar"] = SidebarPage(module: module) + pages["_Footer"] = FooterPage() + case .html: + pages["Home"] = HomePage(module: module) + } var globals: [String: [Symbol]] = [:] for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { - switch symbol.declaration { - case is Class: - try TypePage(module: module, symbol: symbol).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: symbol.id.description)).md")) - case is Enumeration: - try TypePage(module: module, symbol: symbol).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: symbol.id.description)).md")) - case is Structure: - try TypePage(module: module, symbol: symbol).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: symbol.id.description)).md")) - case let `protocol` as Protocol: - try TypePage(module: module, symbol: symbol).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: `protocol`.name)).md")) + switch symbol.api { + case is Class, is Enumeration, is Structure, is Protocol: + pages[path(for: symbol)] = TypePage(module: module, symbol: symbol) case let `typealias` as Typealias: - try TypealiasPage(module: module, symbol: symbol).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: `typealias`.name)).md")) + pages[path(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol) case let function as Function where !function.isOperator: globals[function.name, default: []] += [symbol] case let variable as Variable: @@ -56,7 +73,22 @@ extension SwiftDoc { } for (name, symbols) in globals { - try GlobalPage(module: module, name: name, symbols: symbols).write(to: outputDirectoryURL.appendingPathComponent("\(path(for: name)).md")) + pages[path(for: name)] = GlobalPage(module: module, name: name, symbols: symbols) + } + + try pages.map { $0 }.parallelForEach { + let filename: String + switch format { + case .commonmark: + filename = "\($0.key).md" + case .html where $0.key == "Home": + filename = "index.html" + case .html: + filename = "\($0.key)/index.html" + } + + let url = outputDirectoryURL.appendingPathComponent(filename) + try $0.value.write(to: url, format: format) } } } diff --git a/Sources/swift-doc/Supporting Types/CSS.swift b/Sources/swift-doc/Supporting Types/CSS.swift new file mode 100644 index 00000000..d9354a9c --- /dev/null +++ b/Sources/swift-doc/Supporting Types/CSS.swift @@ -0,0 +1,1172 @@ +let css = #""" +:root { + --system-red: rgb(255, 59, 48); + --system-orange: rgb(255, 149, 0); + --system-yellow: rgb(255, 204, 0); + --system-green: rgb(52, 199, 89); + --system-teal: rgb(90, 200, 250); + --system-blue: rgb(0, 122, 255); + --system-indigo: rgb(88, 86, 214); + --system-purple: rgb(175, 82, 222); + --system-pink: rgb(255, 45, 85); + --system-gray: rgb(142, 142, 147); + --system-gray2: rgb(174, 174, 178); + --system-gray3: rgb(199, 199, 204); + --system-gray4: rgb(209, 209, 214); + --system-gray5: rgb(229, 229, 234); + --system-gray6: rgb(242, 242, 247); + + --label: rgb(0, 0, 0); + --secondary-label: rgb(60, 60, 67); + --tertiary-label: rgb(72, 72, 74); + --quaternary-label: rgb(99, 99, 102); + --placeholder-text: rgb(142, 142, 147); + --link: rgb(0, 122, 255); + --separator: rgb(229, 229, 234); + --opaque-separator: rgb(198, 198, 200); + --system-fill: rgb(120, 120, 128); + --secondary-system-fill: rgb(120, 120, 128); + --tertiary-system-fill: rgb(118, 118, 128); + --quaternary-system-fill: rgb(116, 116, 128); + --system-background: rgb(255, 255, 255); + --secondary-system-background: rgb(242, 242, 247); + --tertiary-system-background: rgb(255, 255, 255); + --system-grouped-background: rgb(242, 242, 247); + --secondary-system-grouped-background: rgb(255, 255, 255); + --tertiary-system-grouped-background: rgb(242, 242, 247); +} + +@supports (color: color(display-p3 1 1 1)) { + :root { + --system-red: color(display-p3 1 0.2314 0.1882); + --system-orange: color(display-p3 1 0.5843 0); + --system-yellow: color(display-p3 1 0.8 0); + --system-green: color(display-p3 0.2039 0.7804 0.349); + --system-teal: color(display-p3 0.3529 0.7843 0.9804); + --system-blue: color(display-p3 0 0.4784 1); + --system-indigo: color(display-p3 0.3451 0.3373 0.8392); + --system-purple: color(display-p3 0.6863 0.3216 0.8706); + --system-pink: color(display-p3 1 0.1765 0.3333); + --system-gray: color(display-p3 0.5569 0.5569 0.5765); + --system-gray2: color(display-p3 0.6824 0.6824 0.698); + --system-gray3: color(display-p3 0.7804 0.7804 0.8); + --system-gray4: color(display-p3 0.8196 0.8196 0.8392); + --system-gray5: color(display-p3 0.898 0.898 0.9176); + --system-gray6: color(display-p3 0.949 0.949 0.9686); + + --label: color(display-p3 0 0 0); + --secondary-label: color(display-p3 0.2353 0.2353 0.2627); + --tertiary-label: color(display-p3 0.2823 0.2823 0.2901); + --quaternary-label: color(display-p3 0.4627 0.4627 0.5019); + --placeholder-text: color(display-p3 0.5568 0.5568 0.5764); + --link: color(display-p3 0 0.4784 1); + --separator: color(display-p3 0.898 0.898 0.9176); + --opaque-separator: color(display-p3 0.7765 0.7765 0.7843); + --system-fill: color(display-p3 0.4706 0.4706 0.502); + --secondary-system-fill: color(display-p3 0.4706 0.4706 0.502); + --tertiary-system-fill: color(display-p3 0.4627 0.4627 0.502); + --quaternary-system-fill: color(display-p3 0.4549 0.4549 0.502); + --system-background: color(display-p3 1 1 1); + --secondary-system-background: color(display-p3 0.949 0.949 0.9686); + --tertiary-system-background: color(display-p3 1 1 1); + --system-grouped-background: color(display-p3 0.949 0.949 0.9686); + --secondary-system-grouped-background: color(display-p3 1 1 1); + --tertiary-system-grouped-background: color( + display-p3 0.949 0.949 0.9686 + ); + } +} + +/* +@media (prefers-color-scheme: dark) { + :root { + --system-red: rgb(255, 69, 58); + --system-orange: rgb(255, 159, 10); + --system-yellow: rgb(255, 214, 10); + --system-green: rgb(48, 209, 88); + --system-teal: rgb(100, 210, 255); + --system-blue: rgb(10, 132, 255); + --system-indigo: rgb(94, 92, 230); + --system-purple: rgb(191, 90, 242); + --system-pink: rgb(255, 55, 95); + --system-gray: rgb(142, 142, 147); + --system-gray2: rgb(99, 99, 102); + --system-gray3: rgb(72, 72, 74); + --system-gray4: rgb(58, 58, 60); + --system-gray5: rgb(44, 44, 46); + --system-gray6: rgb(28, 28, 30); + + --label: rgb(255, 255, 255); + --secondary-label: rgb(235, 235, 245); + --tertiary-label: rgb(235, 235, 245); + --quaternary-label: rgb(235, 235, 245); + --placeholder-text: rgb(235, 235, 245); + --link: rgb(9, 132, 255); + --separator: rgb(44, 44, 46); + --opaque-separator: rgb(56, 56, 58); + --system-fill: rgb(120, 120, 128); + --secondary-system-fill: rgb(120, 120, 128); + --tertiary-system-fill: rgb(118, 118, 128); + --quaternary-system-fill: rgb(118, 118, 128); + --system-background: rgb(0, 0, 0); + --secondary-system-background: rgb(28, 28, 30); + --tertiary-system-background: rgb(44, 44, 46); + --system-grouped-background: rgb(0, 0, 0); + --secondary-system-grouped-background: rgb(28, 28, 30); + --tertiary-system-grouped-background: rgb(44, 44, 46); + } + + @supports (color: color(display-p3 1 1 1)) { + :root { + --system-red: color(display-p3 1 0.4118 0.3804); + --system-orange: color(display-p3 1 0.702 0.251); + --system-yellow: color(display-p3 1 0.8314 0.149); + --system-green: color(display-p3 0.1882 0.8588 0.3569); + --system-teal: color(display-p3 0.4392 0.8431 1); + --system-blue: color(display-p3 0.251 0.6118 1); + --system-indigo: color(display-p3 0.4902 0.4784 1); + --system-purple: color(display-p3 0.8549 0.5608 1); + --system-pink: color(display-p3 1 0.3922 0.5098); + --system-gray: color(display-p3 0.6824 0.6824 0.698); + --system-gray2: color(display-p3 0.4863 0.4863 0.502); + --system-gray3: color(display-p3 0.3294 0.3294 0.3373); + --system-gray4: color(display-p3 0.2667 0.2667 0.2745); + --system-gray5: color(display-p3 0.2118 0.2118 0.2196); + --system-gray6: color(display-p3 0.1412 0.1412 0.149); + + --label: color(display-p3 1 1 1); + --secondary-label: color(display-p3 0.9216 0.9216 0.9608); + --tertiary-label: color(display-p3 0.9216 0.9216 0.9608); + --quaternary-label: color(display-p3 0.9216 0.9216 0.9608); + --placeholder-text: color(display-p3 0.9216 0.9216 0.9608); + --link: color(display-p3 0.03529 0.5176 1); + --separator: color(display-p3 0.2118 0.2118 0.2196); + --opaque-separator: color(display-p3 0.2196 0.2196 0.2275); + --system-fill: color(display-p3 0.4706 0.4706 0.502); + --secondary-system-fill: color(display-p3 0.4706 0.4706 0.502); + --tertiary-system-fill: color(display-p3 0.4627 0.4627 0.502); + --quaternary-system-fill: color(display-p3 0.4627 0.4627 0.502); + --system-background: color(display-p3 0 0 0); + --secondary-system-background: color( + display-p3 0.1412 0.1412 0.149 + ); + --tertiary-system-background: color( + display-p3 0.2118 0.2118 0.2196 + ); + --system-grouped-background: color(display-p3 0 0 0); + --secondary-system-grouped-background: color( + display-p3 0.1412 0.1412 0.149 + ); + --tertiary-system-grouped-background: color( + display-p3 0.2118 0.2118 0.2196 + ); + } + } +} */ + +:root { + --large-title: 600 32pt / 39pt sans-serif; + --title-1: 600 26pt / 32pt sans-serif; + --title-2: 600 20pt / 25pt sans-serif; + --title-3: 500 18pt / 23pt sans-serif; + --headline: 500 15pt / 20pt sans-serif; + --body: 300 15pt / 20pt sans-serif; + --callout: 300 14pt / 19pt sans-serif; + --subhead: 300 13pt / 18pt sans-serif; + --footnote: 300 12pt / 16pt sans-serif; + --caption-1: 300 11pt / 13pt sans-serif; + --caption-2: 300 11pt / 13pt sans-serif; +} + +:root { + --icon-case: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32h-15.52c-.79-7.53-6.1-12.42-13.19-12.42-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25-19.01-.03-30.8-12.12-30.8-32.84z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-class: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%239b98e6' height='90' rx='8' stroke='%235856d6' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32h-15.52c-.79-7.53-6.1-12.42-13.19-12.42-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25-19.01-.03-30.8-12.12-30.8-32.84z' fill='%23fff'/%3E%3C/svg%3E"); + --icon-enumeration: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5.17' y='5'/%3E%3Cpath d='m71.9 81.71h-43.47v-63.42h43.47v13h-27.34v12.62h25.71v11.87h-25.71v12.92h27.34z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-extension: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m54.43 81.93h-33.92v-63.86h33.92v12.26h-21.82v13.8h20.45v11.32h-20.45v14.22h21.82z'/%3E%3Cpath d='m68.74 74.58h-.27l-2.78 7.35h-7.28l5.59-12.61-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E%0A"); + --icon-function: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m24.25 75.66a5.47 5.47 0 0 1 5.75-5.73c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 1 1 0-12h11.1l1.09-7.27c1.55-10.89 8.01-16.58 17.73-16.58 6.69 0 11.74 1.77 11.74 6.64a5.47 5.47 0 0 1 -5.74 5.73c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 1 1 .05 12h-13.19l-1.78 12.11c-1.59 10.92-8.1 16.61-17.82 16.61-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-method: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6h-14.19v-63.42h18.63l16 41.44h.36l16-41.44h18.61v63.42z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-property: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37h-10.34v18.59h-16.16v-63.42zm-11.31 32.71h7c6.85 0 10.89-3.56 10.89-10.2s-4.08-10.16-10.89-10.16h-7z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-protocol: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='m46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71h-10.81v20h-12.09v-63.42zm-11.41 33.05h8.13c6.93 0 11-4 11-11.29s-4-11.25-10.93-11.25h-8.2z'/%3E%3Cpath d='m62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 0 1 1.92.35v7.55a5.69 5.69 0 0 0 -2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1h-8.2z'/%3E%3C/g%3E%3C/svg%3E%0A"); + --icon-structure: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23c-12.66-2.35-19.21-8.49-19.21-18.21 0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23c13.56 2.6 19.71 8.09 19.71 18.09 0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-typealias: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m42 81.71v-50.41h-17.53v-13h51.06v13h-17.53v50.41z' fill='%23fff'/%3E%3C/svg%3E%0A"); + --icon-variable: url("data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m39.85 81.71-20.22-63.42h18.37l12.18 47.64h.35l12.17-47.64h17.67l-20.22 63.42z' fill='%23fff'/%3E%3C/svg%3E%0A"); +} + +/************/ + +body, +input, +textarea, +select, +button { + font-synthesis: none; + -moz-font-feature-settings: "kern"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + direction: ltr; + text-align: left; +} + +h1:first-of-type, +h2:first-of-type, +h3:first-of-type, +h4:first-of-type, +h5:first-of-type, +h6:first-of-type { + margin-top: 0; +} + +h1 code, +h2 code, +h3 code, +h4 code, +h5 code, +h6 code { + font-family: inherit; + font-weight: inherit; +} + +h1 img, +h2 img, +h3 img, +h4 img, +h5 img, +h6 img { + margin: 0 0.5em 0.2em 0; + vertical-align: middle; + display: inline-block; +} + +img + h1 { + margin-top: 0.5em; +} + +img + h1, +img + h2, +img + h3, +img + h4, +img + h5, +img + h6 { + margin-top: 0.3em; +} + +h1 + *, +h2 + *, +h3 + *, +h4 + *, +h5 + *, +h6 + * { + margin-top: 0.8em; +} + +:is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) { + margin-top: 0.4em; +} + +:matches(h1, h2, h3, h4, h5, h6) + :matches(h1, h2, h3, h4, h5, h6) { + margin-top: 0.4em; +} + +:is(p, ul, ol) + :is(h1, h2, h3, h4, h5, h6) { + margin-top: 1.6em; +} + +:matches(p, ul, ol) + :matches(h1, h2, h3, h4, h5, h6) { + margin-top: 1.6em; +} + +:is(p, ul, ol) + * { + margin-top: 0.8em; +} + +:matches(p, ul, ol) + * { + margin-top: 0.8em; +} + +ul, +ol { + margin-left: 1.17647em; +} + +:matches(ul, ol) :matches(ul, ol) { + margin-top: 0; + margin-bottom: 0; +} + +nav h2 { + color: var(--secondary-label); + text-transform: uppercase; + font-variant: small-caps; + font-weight: 600; + font-size: 1rem; +} + +nav ul, +nav ol { + margin: 0; + list-style: none; +} + +nav li li { + font-size: smaller; +} + +a:link, +a:visited { + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +a:active { + text-decoration: none; +} + +p + a { + display: inline-block; +} + +b, +strong { + font-weight: 600; +} + +.summary, +.discussion { + font: var(--callout); +} + +article > .discussion { + margin-bottom: 2em; +} + +.discussion .highlight { + font: var(--caption-1); + background: transparent; + border: 1px var(--separator) solid; +} + +em, +i, +cite, +dfn { + font-style: italic; +} + +/* sup { + font-size: 0.6em; + vertical-align: top; + position: relative; + bottom: -0.2em; +} */ + +:matches(h1, h2, h3) sup { + font-size: 0.4em; +} + +sup a { + vertical-align: inherit; + color: inherit; +} + +sup a:hover { + color: var(--link); + text-decoration: none; +} + +sub { + line-height: 1; +} + +abbr { + border: 0; +} + +:lang(ja), +:lang(ko), +:lang(th), +:lang(zh) { + font-style: normal; +} + +:lang(ko) { + word-break: keep-all; +} + +form fieldset { + width: 95%; + margin: 1em auto; + max-width: 450px; +} + +form label { + position: relative; + display: block; + margin-bottom: 14px; + width: 100%; + font-size: 1em; + font-weight: 400; + line-height: 1.5em; +} + +input[type="text"], +input[type="email"], +input[type="number"], +input[type="password"], +input[type="tel"], +input[type="url"], +textarea { + margin: 0; + width: 100%; + height: 34px; + font-family: inherit; + font-size: 100%; + font-weight: 400; + border: 1px solid var(--separator); + border-radius: 4px; + padding: 0 1em 0; + position: relative; + z-index: 1; + color: #333333; + vertical-align: top; +} + +input[type="text"], +input[type="text"]:focus, +input[type="email"], +input[type="email"]:focus, +input[type="number"], +input[type="number"]:focus, +input[type="password"], +input[type="password"]:focus, +input[type="tel"], +input[type="tel"]:focus, +input[type="url"], +input[type="url"]:focus, +textarea, +textarea:focus { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +input[type="text"]:focus, +input[type="email"]:focus, +input[type="number"]:focus, +input[type="password"]:focus, +input[type="tel"]:focus, +input[type="url"]:focus, +textarea:focus { + border-color: #0088cc; + outline: 0; + -webkit-box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + z-index: 9; +} + +input[type="text"]:-moz-read-only, +input[type="text"]:-moz-read-only, +input[type="email"]:-moz-read-only, +input[type="email"]:-moz-read-only, +input[type="number"]:-moz-read-only, +input[type="number"]:-moz-read-only, +input[type="password"]:-moz-read-only, +input[type="password"]:-moz-read-only, +input[type="tel"]:-moz-read-only, +input[type="tel"]:-moz-read-only, +input[type="url"]:-moz-read-only, +input[type="url"]:-moz-read-only { + background: none; + border: none; + box-shadow: none; + padding-left: 0; +} + +input[type="text"]:-moz-read-only, +input[type="text"]:read-only, +input[type="email"]:-moz-read-only, +input[type="email"]:read-only, +input[type="number"]:-moz-read-only, +input[type="number"]:read-only, +input[type="password"]:-moz-read-only, +input[type="password"]:read-only, +input[type="tel"]:-moz-read-only, +input[type="tel"]:read-only, +input[type="url"]:-moz-read-only, +input[type="url"]:read-only { + background: none; + border: none; + box-shadow: none; + padding-left: 0; +} + +::-webkit-input-placeholder, +:-moz-placeholder, +::-moz-placeholder, +:-ms-input-placeholder { + color: var(--placeholder-text); +} + +textarea { + min-height: 134px; + line-height: 1.4737; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + resize: vertical; +} +textarea, +textarea:focus { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +select { + background: transparent; + width: 100%; + height: 34px; + padding: 0 1em; + font-size: 1em; + font-family: inherit; + border-radius: 4px; + border: none; + margin: 0; + cursor: pointer; +} +select, +select:focus { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +select:focus { + border-color: #0088cc; + outline: 0; + -webkit-box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + z-index: 9; +} + +input[type="file"] { + margin: 0; + font-family: inherit; + font-size: 100%; + background: #fafafa; + width: 100%; + height: 34px; + border-radius: 4px; + padding: 6px 1em; + position: relative; + z-index: 1; + color: #333333; + vertical-align: top; + cursor: pointer; +} + +input[type="file"]:focus { + border-color: #0088cc; + outline: 0; + -webkit-box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); + z-index: 9; +} +input[type="file"]:focus, +input[type="file"]:focus:focus { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +button, +button:focus, +input[type="reset"], +input[type="reset"]:focus, +input[type="submit"], +input[type="submit"]:focus { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; +} + +:matches(button, input[type="reset"], input[type="submit"]) { + background-color: #e3e3e3; + background: -webkit-gradient( + linear, + left top, + left bottom, + from(white), + to(#e3e3e3) + ); + background: linear-gradient(white, #e3e3e3); + border-color: #d6d6d6; + color: #0070c9; +} +:matches(button, input[type="reset"], input[type="submit"]):hover { + background-color: #eeeeee; + background: -webkit-gradient( + linear, + left top, + left bottom, + from(white), + to(#eeeeee) + ); + background: linear-gradient(white, #eeeeee); + border-color: #d9d9d9; +} +:matches(button, input[type="reset"], input[type="submit"]):active { + background-color: gainsboro; + background: -webkit-gradient( + linear, + left top, + left bottom, + from(#f7f7f7), + to(gainsboro) + ); + background: linear-gradient(#f7f7f7, gainsboro); + border-color: #d0d0d0; +} +:matches(button, input[type="reset"], input[type="submit"]):disabled { + background-color: #e3e3e3; + background: -webkit-gradient( + linear, + left top, + left bottom, + from(white), + to(#e3e3e3) + ); + background: linear-gradient(white, #e3e3e3); + border-color: #d6d6d6; + color: #0070c9; +} + +/* */ + +body { + background: var(--system-grouped-background); + font: var(--body); + font-family: ui-system, -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--label); +} + +h1 { + font: var(--large-title); +} + +h2 { + font: var(--title-2); +} + +h3 { + font: var(--title-3); +} + +h4, +h5, +h6 { + font: var(--headline); +} + +/* strong, + th, + dt { + font: var(--headline); + } */ + +a { + color: var(--link); +} + +label { + font: var(--callout); +} + +label, +input { + display: block; +} + +input { + margin-bottom: 1em; +} + +/* button, + input[type="submit"] { + color: var(--link); + background: transparent; + border: none; + padding: 0.5em; + } */ + +/*********************/ + +hr { + border: none; + border-top: 1px var(--separator) solid; + margin: 1em 0; +} + +table { + width: 100%; + font: var(--caption-1); + caption-side: bottom; + margin-bottom: 2em; +} + +th, +td { + padding: 0 1em; +} + +th { + font-weight: 600; + text-align: left; +} + +thead th { + border-bottom: 1px var(--separator) solid; +} + +tr:last-of-type td, +tr:last-of-type th { + border-bottom: none; +} + +th, +td { + border-bottom: 1px var(--separator) solid; + color: var(--secondary-label); +} + +caption { + text-align: left; + margin-top: 2em; + font: var(--caption-2); + color: var(--tertiary-label); +} + +code, +.graph text { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + font-weight: 300; +} + +.graph > polygon { + display: none; +} + +.graph text { + fill: currentColor !important; +} + +.graph path, +.graph ellipse, +.graph rect, +.graph polygon { + stroke: currentColor !important; +} + +body { + width: 90vw; + max-width: 1280px; + margin: 1em auto; +} + +body > header { + font: var(--title-1); +} + +body > header a { + color: var(--label); +} + +body > header span { + font-weight: normal; +} + +body > header sup { + text-transform: uppercase; + font-size: small; + font-weight: 300; + color: var(--secondary-label); + letter-spacing: 0.1ch; +} + +@media screen and (max-width: 768px) { + body { + width: 96vw; + max-width: 100%; + } + + body > header { + font: var(--title-3); + text-align: left; + padding: 1em 0; + } + + body > nav { + display: none; + } + + body > main { + padding: 0 1em; + } + + #relationships figure { + display: none; + } + + section > [role="article"][class] pre { + margin-left: -2.5em; + } + + section > [role="article"][class] div { + margin-left: -2em; + } +} + +body > header { + padding: 0.5em 0; +} + +main, +nav { + overflow-x: scroll; +} + +main { + background: var(--system-background); + border-radius: 8px; +} + +form.search { +} + +body > footer { + clear: both; + padding: 1em 0; + font: var(--caption-1); + color: var(--secondary-label); +} + +main { + padding: 0 2em; + /* max-width: 66ch; */ +} + +nav { + width: 20vw; + float: right; + overflow: scroll; + padding: 0 1em 3em 1em; + margin-left: 1em; + position: sticky; + top: 1em; + max-height: 100vh; +} + +nav a { + color: var(--secondary-label); +} + +nav ul a { + color: var(--tertiary-label); +} + +nav ol { + padding: 0; +} + +nav ul { + padding: 0; + margin-bottom: 1em; + font: var(--callout); +} + +nav ol > li > a { + font: var(--headline); + font-size: smaller; + display: block; + margin: 0.5em 0; +} + +nav li { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +main section { + margin-bottom: 2em; + padding-bottom: 1em; + border-bottom: 1px var(--separator) solid; +} + +main section:last-of-type { + margin-bottom: 0; + border-bottom: none; +} + +/* main :matches(h1, h2, h3) { + position: sticky; + top: 0px; +} + +main h1 { + z-index: 1; +} + +main h2 { + z-index: 2; +} + +main h3 { + z-index: 3; +} */ + +blockquote { + border-left: 4px var(--separator) solid; + padding-left: 2em; + margin-left: 0; + font-size: smaller; + color: var(--secondary-label); + --link: var(--secondary-label); +} + +blockquote a { + text-decoration: underline; +} + +article { + padding: 2em 0 1em 0; +} + +article > .summary { + margin-bottom: 2em; + padding-bottom: 1em; + border-bottom: 1px var(--separator) solid; +} + +article > .summary:last-child { + border-bottom: none; +} + +.parameters th { + text-align: right; +} + +.parameters td { + color: var(--secondary-label); +} + +.parameters th + td { + text-align: center; +} + +dl { + padding-top: 1em; +} + +dt { + font: var(--headline); +} + +dd { + margin-left: 2em; + margin-bottom: 1em; +} + +dd p { + margin-top: 0; +} + +.highlight { + background: var(--secondary-system-background); + border-radius: 8px; + font-size: smaller; + overflow-x: scroll; + white-space: pre-line; + padding: 1em; + padding-left: 3em; + text-indent: -2em; + margin-bottom: 2em; +} + +.highlight .p { + white-space: nowrap; +} + +.highlight .placeholder { + color: var(--label); +} + +.highlight a { + text-decoration: underline; + color: var(--placeholder-text); +} + +.highlight .literal, +.highlight .keyword, +.highlight .attribute { + color: var(--system-purple); +} + +.highlight .number { + color: var(--system-blue); +} + +.highlight .declaration { + color: var(--system-teal); +} + +.highlight .type { + color: var(--system-indigo); +} + +.highlight .directive { + color: var(--system-orange); +} + +.highlight .comment { + color: var(--system-gray); +} + +main summary:hover { + text-decoration: underline; +} + +figure { + margin: 2em 0; + padding: 1em 0; +} + +figure svg { + max-width: 100%; + height: auto !important; + margin: 0 auto; + display: block; +} + +h1 small { + font-size: 0.5em; + line-height: 1.5; + display: block; + font-weight: normal; + color: var(--quaternary-label); +} + +p code, +dd code, +li code { + font-size: smaller; + color: var(--secondary-label); +} + +a code { + text-decoration: underline; +} + +section > [role="article"][class], +nav li[class], +dl dt[class] { + background-image: var(--background-image); + background-size: 1em; + background-repeat: no-repeat; + background-position: left 0.25em; + padding-left: 3em; +} + +dl dt[class] { + background-position-y: 0.125em; +} + +section > [role="article"] { + margin-bottom: 1em; + padding-bottom: 1em; + border-bottom: 1px var(--separator) solid; + padding-left: 2em !important; +} + +section > [role="article"]:last-of-type { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +nav li[class], +dl dt[class] { + list-style: none; + text-indent: -1em; + margin-bottom: 0.5em; +} + +nav li[class] { + padding-left: 2.5em; +} + +.case, +.enumeration_case { + --background-image: var(--icon-case); + --link: var(--system-teal); +} + +.class { + --background-image: var(--icon-class); + --link: var(--system-indigo); +} + +.enumeration { + --background-image: var(--icon-enumeration); + --link: var(--system-orange); +} + +.extension { + --background-image: var(--icon-extension); + --link: var(--system-orange); +} + +.function { + --background-image: var(--icon-function); + --link: var(--system-green); +} + +.method, +.initializer { + --background-image: var(--icon-method); + --link: var(--system-blue); +} + +.property { + --background-image: var(--icon-property); + --link: var(--system-teal); +} + +.protocol { + --background-image: var(--icon-protocol); + --link: var(--system-pink); +} + +.structure { + --background-image: var(--icon-structure); + --link: var(--system-purple); +} + +.typealias { + --background-image: var(--icon-typealias); + --link: var(--system-green); +} + +.variable { + --background-image: var(--icon-variable); + --link: var(--system-green); +} + +.unknown { + --link: var(--quaternary-label); + color: var(--link); +} + +"""# diff --git a/Sources/swift-doc/Supporting Types/Component.swift b/Sources/swift-doc/Supporting Types/Component.swift index 8bf7fa22..d940b7db 100644 --- a/Sources/swift-doc/Supporting Types/Component.swift +++ b/Sources/swift-doc/Supporting Types/Component.swift @@ -1,11 +1,13 @@ import CommonMarkBuilder +import HypertextLiteral -public protocol Component: BlockConvertible { - var body: Fragment { get } +public protocol Component: BlockConvertible, HypertextLiteralConvertible { + var fragment: Fragment { get } + var html: HypertextLiteral.HTML { get } } extension Component { public var blockValue: [Block & Node] { - return body.blockValue + return fragment.blockValue } } diff --git a/Sources/swift-doc/Supporting Types/Components/ConformingTypes.swift b/Sources/swift-doc/Supporting Types/Components/ConformingTypes.swift deleted file mode 100644 index f1a34c73..00000000 --- a/Sources/swift-doc/Supporting Types/Components/ConformingTypes.swift +++ /dev/null @@ -1,41 +0,0 @@ -import CommonMarkBuilder -import SwiftDoc -import SwiftMarkup -import SwiftSemantics - -struct ConformingTypes: Component { - var symbol: Symbol - var module: Module - - init(to symbol: Symbol, in module: Module) { - precondition(symbol.declaration is Protocol) - self.symbol = symbol - self.module = module - } - - // MARK: - Component - - var body: Fragment { - guard symbol.declaration is Protocol else { return Fragment { "" } } - let conformingTypes = module.interface.typesConforming(to: symbol) - guard !conformingTypes.isEmpty else { return Fragment { "" }} - - return Fragment { - Section { - Heading { "Conforming Types" } - - Fragment { - #""" - \#(conformingTypes.map { type in - if type.declaration is Unknown { - return "`\(type.id)`" - } else { - return "[`\(type.id)`](\(path(for: type.id)))" - } - }.joined(separator: ", ")) - """# - } - } - } - } -} diff --git a/Sources/swift-doc/Supporting Types/Components/Declaration.swift b/Sources/swift-doc/Supporting Types/Components/Declaration.swift new file mode 100644 index 00000000..94acc437 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Components/Declaration.swift @@ -0,0 +1,33 @@ +import CommonMarkBuilder +import SwiftDoc +import SwiftMarkup +import SwiftSemantics +import HypertextLiteral +import SwiftSyntaxHighlighter +import Xcode + +struct Declaration: Component { + var symbol: Symbol + var module: Module + + init(of symbol: Symbol, in module: Module) { + self.symbol = symbol + self.module = module + } + + // MARK: - Component + + var fragment: Fragment { + Fragment { + CodeBlock("swift") { + symbol.declaration.trimmingCharacters(in: .whitespacesAndNewlines) + } + } + } + + var html: HypertextLiteral.HTML { + var html = try! SwiftSyntaxHighlighter.highlight(source: symbol.declaration, using: Xcode.self) + html = linkCodeElements(of: html, for: symbol, in: module) + return HTML(html) + } +} diff --git a/Sources/swift-doc/Supporting Types/Components/Documentation.swift b/Sources/swift-doc/Supporting Types/Components/Documentation.swift index 27156682..132ffc89 100644 --- a/Sources/swift-doc/Supporting Types/Components/Documentation.swift +++ b/Sources/swift-doc/Supporting Types/Components/Documentation.swift @@ -1,17 +1,24 @@ +import Foundation import SwiftDoc +import SwiftSemantics import SwiftMarkup import CommonMarkBuilder +import HypertextLiteral +import SwiftSyntaxHighlighter +import Xcode struct Documentation: Component { var symbol: Symbol + var module: Module - init(for symbol: Symbol) { + init(for symbol: Symbol, in module: Module) { self.symbol = symbol + self.module = module } // MARK: - Component - var body: Fragment { + var fragment: Fragment { guard let documentation = symbol.documentation else { return Fragment { "" } } return Fragment { @@ -30,9 +37,7 @@ struct Documentation: Component { Fragment { "\(documentation.summary!)" } } - CodeBlock("swift") { - "\(symbol.declaration)".trimmingCharacters(in: .whitespacesAndNewlines) - } + Declaration(of: symbol, in: module) ForEach(in: documentation.discussionParts) { part in if part is SwiftMarkup.Documentation.Callout { @@ -77,6 +82,121 @@ struct Documentation: Component { } } + var html: HypertextLiteral.HTML { + guard let documentation = symbol.documentation else { return "" } + + var fragments: [HypertextLiteralConvertible] = [] + + fragments.append(Declaration(of: symbol, in: module)) + + if let summary = documentation.summary { + fragments.append(#""" +
+ \#(commonmark: summary) +
+ """# as HypertextLiteral.HTML) + } + + if !documentation.discussionParts.isEmpty { + fragments.append(#""" +
+ \#(documentation.discussionParts.compactMap { part -> HypertextLiteral.HTML? in + if let part = part as? SwiftMarkup.Documentation.Callout { + return Callout(part).html + } else if let part = part as? String { + if part.starts(with: "```"), + let codeBlock = (try? CommonMark.Document(part))?.children.compactMap({ $0 as? CodeBlock }).first, + (codeBlock.fenceInfo ?? "") == "" || + codeBlock.fenceInfo?.compare("swift", options: .caseInsensitive) == .orderedSame, + let source = codeBlock.literal + { + var html = try! SwiftSyntaxHighlighter.highlight(source: source, using: Xcode.self) + html = linkCodeElements(of: html, for: symbol, in: module) + return HTML(html) + } else { + var html = (try! CommonMark.Document(part)).render(format: .html, options: [.unsafe]) + html = linkCodeElements(of: html, for: symbol, in: module) + return HTML(html) + } + } else { + return nil + } + }) +
+ """# as HypertextLiteral.HTML) + } + + if !documentation.parameters.isEmpty { + let typedParameters: [(name: String, type: String?, description: String)] = documentation.parameters.map { entry in + let type: String? + switch symbol.api { + case let function as Function: + type = function.signature.input.first(where: { $0.firstName == entry.name || $0.secondName == entry.name })?.type + case let initializer as Initializer: + type = initializer.parameters.first(where: { $0.firstName == entry.name || $0.secondName == entry.name })?.type + case let `subscript` as Subscript: + type = `subscript`.indices.first(where: { $0.firstName == entry.name || $0.secondName == entry.name })?.type + default: + type = nil + } + + return (entry.name, type, entry.description) + } + + fragments.append(#""" +

Parameters

+ + + + + + + + + + + \#(typedParameters.map { entry -> HypertextLiteral.HTML in + let typeCell: HypertextLiteral.HTML + if let type = entry.type { + typeCell = #""# as HypertextLiteral.HTML + } else { + typeCell = "" as HypertextLiteral.HTML + } + + return #""" + + + \#(typeCell) + + + """# as HypertextLiteral.HTML + }) + +
\#(softbreak(type))
\#(softbreak(entry.name))\#(commonmark: entry.description)
+ """# as HypertextLiteral.HTML) + } + + if let `throws` = documentation.throws { + fragments.append(#""" +

Throws

+ \#(commonmark: `throws`) + """# as HypertextLiteral.HTML) + } + + if let `returns` = documentation.returns { + fragments.append(#""" +

Returns

+ \#(commonmark: `returns`) + """# as HypertextLiteral.HTML) + } + + return #""" + \#(fragments.map { $0.html }) + """# + } +} + +extension Documentation { struct Callout: Component { var callout: SwiftMarkup.Documentation.Callout @@ -86,12 +206,20 @@ struct Documentation: Component { // MARK: - Component - var body: Fragment { + var fragment: Fragment { Fragment { """ > \(callout.delimiter.rawValue.capitalized): \(callout.content) """ } } + + var html: HypertextLiteral.HTML { + return #""" + + """# + } } } diff --git a/Sources/swift-doc/Supporting Types/Components/Inheritance.swift b/Sources/swift-doc/Supporting Types/Components/Inheritance.swift deleted file mode 100644 index e5a6e624..00000000 --- a/Sources/swift-doc/Supporting Types/Components/Inheritance.swift +++ /dev/null @@ -1,58 +0,0 @@ -import CommonMarkBuilder -import SwiftDoc -import SwiftMarkup -import SwiftSemantics -import Foundation - -extension StringBuilder { - // MARK: buildIf - - public static func buildIf(_ string: String?) -> String { - return string ?? "" - } - - // MARK: buildEither - - public static func buildEither(first: String) -> String { - return first - } - - public static func buildEither(second: String) -> String { - return second - } -} - -struct Inheritance: Component { - var module: Module - var symbol: Symbol - - init(of symbol: Symbol, in module: Module) { - self.module = module - self.symbol = symbol - } - - // MARK: - Component - - var body: Fragment { - let inheritedTypes = module.interface.typesInherited(by: symbol) + module.interface.typesConformed(by: symbol) - guard !inheritedTypes.isEmpty else { return Fragment { "" } } - - return Fragment { - Section { - Heading { "Inheritance" } - - Fragment { - #""" - \#(inheritedTypes.map { type in - if type.declaration is Unknown { - return "`\(type.id)`" - } else { - return "[`\(type.id)`](\(path(for: type.id)))" - } - }.joined(separator: ", ")) - """# - } - } - } - } -} diff --git a/Sources/swift-doc/Supporting Types/Components/Members.swift b/Sources/swift-doc/Supporting Types/Components/Members.swift index e3278ac9..9692433e 100644 --- a/Sources/swift-doc/Supporting Types/Components/Members.swift +++ b/Sources/swift-doc/Supporting Types/Components/Members.swift @@ -2,36 +2,48 @@ import CommonMarkBuilder import SwiftDoc import SwiftMarkup import SwiftSemantics +import HypertextLiteral struct Members: Component { var symbol: Symbol var module: Module + var members: [Symbol] + + var typealiases: [Symbol] + var cases: [Symbol] + var initializers: [Symbol] + var properties: [Symbol] + var methods: [Symbol] + var genericallyConstrainedMembers: [[GenericRequirement] : [Symbol]] + init(of symbol: Symbol, in module: Module) { self.symbol = symbol self.module = module - } - - // MARK: - Component + self.members = module.interface.members(of: symbol).filter { $0.extension?.genericRequirements.isEmpty != false } - var body: Fragment { - let members = module.interface.members(of: symbol).filter { $0.extension?.genericRequirements.isEmpty != false } - guard !members.isEmpty else { return Fragment { "" } } - - let typealiases = members.filter { $0.declaration is Typealias } - let cases = members.filter { $0.declaration is Enumeration.Case } - let initializers = members.filter { $0.declaration is Initializer } - let properties = members.filter { $0.declaration is Variable } - let methods = members.filter { $0.declaration is Function } - let genericallyConstrainedMembers = Dictionary(grouping: members) { $0.`extension`?.genericRequirements ?? [] }.filter { !$0.key.isEmpty } + self.typealiases = members.filter { $0.api is Typealias } + self.cases = members.filter { $0.api is Enumeration.Case } + self.initializers = members.filter { $0.api is Initializer } + self.properties = members.filter { $0.api is Variable } + self.methods = members.filter { $0.api is Function } + self.genericallyConstrainedMembers = Dictionary(grouping: members) { $0.`extension`?.genericRequirements ?? [] }.filter { !$0.key.isEmpty } + } - let sections: [(title: String, members: [Symbol])] = [ - (symbol.declaration is Protocol ? "Associated Types" : "Nested Type Aliases", typealiases), + var sections: [(title: String, members: [Symbol])] { + return [ + (symbol.api is Protocol ? "Associated Types" : "Nested Type Aliases", typealiases), ("Enumeration Cases", cases), ("Initializers", initializers), ("Properties", properties), ("Methods", methods) ].filter { !$0.members.isEmpty } + } + + // MARK: - Component + + var fragment: Fragment { + guard !members.isEmpty else { return Fragment { "" } } return Fragment { ForEach(in: sections) { section -> BlockConvertible in @@ -39,7 +51,7 @@ struct Members: Component { Heading { section.title } ForEach(in: section.members) { member in Heading { member.name } - Documentation(for: member) + Documentation(for: member, in: module) } } } @@ -55,7 +67,7 @@ struct Members: Component { Section { ForEach(in: members) { member in Heading { member.name } - Documentation(for: member) + Documentation(for: member, in: module) } } } @@ -64,4 +76,51 @@ struct Members: Component { } } } + + var html: HypertextLiteral.HTML { + return #""" + \#(sections.map { section -> HypertextLiteral.HTML in + #""" +
+

\#(section.title)

+ + \#(section.members.map { member -> HypertextLiteral.HTML in + let descriptor = String(describing: type(of: symbol.api)).lowercased() + + return #""" +
+

+ \#(softbreak(member.name)) +

+ \#(Documentation(for: member, in: module).html) +
+ """# + }) +
+ """# + }) + + \#((genericallyConstrainedMembers.isEmpty ? "" : + #""" +
+

Generically Constrained Members

+ + \#(genericallyConstrainedMembers.map { (requirements, members) -> HypertextLiteral.HTML in + #""" +
+

where \#(requirements.map { softbreak($0.description) }.joined(separator: ", "))

+ \#(members.map { member -> HypertextLiteral.HTML in + #""" +

\#(softbreak(member.name))

+ \#(Documentation(for: member, in: module).html) + """# + }) +
+ """# + }) +
+ """# + ) as HypertextLiteral.HTML) + """# + } } diff --git a/Sources/swift-doc/Supporting Types/Components/NestedTypes.swift b/Sources/swift-doc/Supporting Types/Components/NestedTypes.swift deleted file mode 100644 index a30ea908..00000000 --- a/Sources/swift-doc/Supporting Types/Components/NestedTypes.swift +++ /dev/null @@ -1,40 +0,0 @@ -import CommonMarkBuilder -import SwiftDoc -import SwiftMarkup -import SwiftSemantics - -struct NestedTypes: Component { - var symbol: Symbol - var module: Module - - init(of symbol: Symbol, in module: Module) { - precondition(symbol.declaration is Type) - self.symbol = symbol - self.module = module - } - - // MARK: - Component - - var body: Fragment { - let nestedTypes = module.interface.members(of: symbol).filter { $0.declaration is Type } - guard !nestedTypes.isEmpty else { return Fragment { "" }} - - return Fragment { - Section { - Heading { "Nested Types" } - - Fragment { - #""" - \#(nestedTypes.map { type in - if type.declaration is Unknown { - return "`\(type.id)`" - } else { - return "[`\(type.id)`](\(path(for: type.id)))" - } - }.joined(separator: ", ")) - """# - } - } - } - } -} diff --git a/Sources/swift-doc/Supporting Types/Components/Relationships.swift b/Sources/swift-doc/Supporting Types/Components/Relationships.swift new file mode 100644 index 00000000..9615dc49 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Components/Relationships.swift @@ -0,0 +1,127 @@ +import CommonMarkBuilder +import SwiftDoc +import SwiftMarkup +import SwiftSemantics +import Foundation +import HypertextLiteral +import GraphViz +import DOT + +extension StringBuilder { + // MARK: buildIf + + public static func buildIf(_ string: String?) -> String { + return string ?? "" + } + + // MARK: buildEither + + public static func buildEither(first: String) -> String { + return first + } + + public static func buildEither(second: String) -> String { + return second + } +} + +struct Relationships: Component { + var module: Module + var symbol: Symbol + var inheritedTypes: [Symbol] + + init(of symbol: Symbol, in module: Module) { + self.module = module + self.symbol = symbol + self.inheritedTypes = module.interface.typesInherited(by: symbol) + module.interface.typesConformed(by: symbol) + } + + + var sections: [(title: String, symbols: [Symbol])] { + return [ + ("Member Of", [module.interface.relationshipsBySubject[symbol.id]?.filter { $0.predicate == .memberOf }.first?.object].compactMap { $0 }), + ("Nested Types", module.interface.members(of: symbol).filter { $0.api is Type }), + ("Superclass", module.interface.typesInherited(by: symbol)), + ("Subclasses", module.interface.typesInheriting(from: symbol)), + ("Conforms To", module.interface.typesConformed(by: symbol)), + ("Types Conforming to \(softbreak(symbol.id.description))", module.interface.typesConforming(to: symbol)), + ].filter { !$0.symbols.isEmpty } + } + + // MARK: - Component + + var fragment: Fragment { + guard !inheritedTypes.isEmpty else { return Fragment { "" } } + + return Fragment { + Section { + Heading { "Inheritance" } + + Fragment { + #""" + \#(inheritedTypes.map { type in + if type.api is Unknown { + return "`\(type.id)`" + } else { + return "[`\(type.id)`](\(path(for: type)))" + } + }.joined(separator: ", ")) + """# + } + } + } + } + + var html: HypertextLiteral.HTML { + var graph = symbol.graph(in: module) + guard !graph.edges.isEmpty else { return "" } + + graph.aspectRatio = 0.125 + graph.center = true + graph.overlap = "compress" + + let algorithm: LayoutAlgorithm = graph.nodes.count > 3 ? .neato : .dot + var svg: HypertextLiteral.HTML? + + do { + svg = try HypertextLiteral.HTML(String(data: graph.render(using: algorithm, to: .svg), encoding: .utf8) ?? "") + } catch { + print(error) + } + + return #""" +
+ +
+ \#(svg ?? "") + + +
+ \#(sections.compactMap { (heading, symbols) -> HypertextLiteral.HTML? in + guard !symbols.isEmpty else { return nil } + + let partitioned = symbols.filter { !($0.api is Unknown) } + symbols.filter { ($0.api is Unknown) } + + return #""" +

\#(unsafeUnescaped: heading)

+
+ \#(partitioned.map { symbol -> HypertextLiteral.HTML in + let descriptor = String(describing: type(of: symbol.api)).lowercased() + if symbol.api is Unknown { + return #""" +
\#(symbol.id)
+ """# + } else { + return #""" +
\#(symbol.id)
+
\#(commonmark: symbol.documentation?.summary ?? "")
+ """# + } + }) +
+ """# + }) +
+ """# + } +} diff --git a/Sources/swift-doc/Supporting Types/Components/Requirements.swift b/Sources/swift-doc/Supporting Types/Components/Requirements.swift index aec203c5..bf96ce78 100644 --- a/Sources/swift-doc/Supporting Types/Components/Requirements.swift +++ b/Sources/swift-doc/Supporting Types/Components/Requirements.swift @@ -2,6 +2,7 @@ import CommonMarkBuilder import SwiftDoc import SwiftMarkup import SwiftSemantics +import HypertextLiteral struct Requirements: Component { var symbol: Symbol @@ -14,7 +15,7 @@ struct Requirements: Component { // MARK: - Component - var body: Fragment { + var fragment: Fragment { let sections: [(title: String, requirements: [Symbol])] = [ ("Requirements", module.interface.requirements(of: symbol)), ("Optional Requirements", module.interface.optionalRequirements(of: symbol)) @@ -27,10 +28,16 @@ struct Requirements: Component { Heading { section.title } ForEach(in: section.requirements) { requirement in Heading { requirement.name } - Documentation(for: requirement) + Documentation(for: requirement, in: module) } } } } } + + var html: HypertextLiteral.HTML { + return #""" + + """# + } } diff --git a/Sources/swift-doc/Supporting Types/Helpers.swift b/Sources/swift-doc/Supporting Types/Helpers.swift new file mode 100644 index 00000000..92962c96 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Helpers.swift @@ -0,0 +1,96 @@ +import Foundation +import SwiftDoc +import HTML + +public func linkCodeElements(of html: String, for symbol: Symbol, in module: Module) -> String { + let document = try! Document(string: html.description)! + for element in document.search(xpath: "//code | //pre/code//span[contains(@class,'type')]") { + guard let name = element.content else { continue } + + if let candidates = module.interface.symbolsGroupedByQualifiedName[name], + candidates.count == 1, + let candidate = candidates.filter({ $0 != symbol }).first + { + let a = Element(name: "a") + a["href"] = "/" + path(for: candidate) + element.wrap(inside: a) + } + } + + return document.root?.description ?? html +} + +public func sidebar(for html: String) -> String { + let toc = Element(name: "ol") + + let document = try! Document(string: html.description)! + for h2 in document.search(xpath: "//section/h2") { + guard let section = h2.parent as? Element else { continue } + + let li = Element(name: "li") + + var className: String? = nil + switch section["id"]?.lowercased() { + case "initializers": + className = "initializer" + case "enumeration cases": + className = "case" + case "methods": + className = "method" + case "properties": + className = "property" + case "nested type aliases": + className = "typealias" + default: + break + } + + if let id = section["id"] { + let a = Element(name: "a") + a["href"] = "#\(id)" + a.content = h2.text + li.insert(child: a) + } else { + li.content = h2.text + } + + + let nestedItems = section.search(xpath: "./h3 | ./div/h3").compactMap { summary -> Element? in + guard let article = summary.parent as? Element else { return nil } + + let li = Element(name: "li") + + if let className = className { + li["class"] = className + } + + let a = Element(name: "a") + a["href"] = "#\(article["id"]!)" + a.content = summary.text + + li.insert(child: a) + return li + } + + if !nestedItems.isEmpty { + let ul = Element(name: "ul") + nestedItems.forEach { ul.insert(child: $0) } + li.insert(child: ul) + } + + + toc.insert(child: li) + } + + return toc.description +} + +fileprivate let pattern = #"(?:([a-z]{2,})([A-Z]+))"# +fileprivate let regex = try! NSRegularExpression(pattern: pattern, options: []) + +public func softbreak(_ string: String) -> String { + let string = string.replacingOccurrences(of: ".", with: ".\u{200B}") + .replacingOccurrences(of: ":", with: ":\u{200B}") + + return regex.stringByReplacingMatches(in: string, options: [], range: NSRange(string.startIndex.. HTML { + let html = page.html + + return #""" + + + + + + \#(page.module.name) - \#(page.title) + + + +
+ + + \#(page.module.name) + + Documentation + + Beta +
+ + + + + +
+
+ \#(html) +
+
+ +
+ \#(FooterPage().html) +
+ + + + """# +} diff --git a/Sources/swift-doc/Supporting Types/Page.swift b/Sources/swift-doc/Supporting Types/Page.swift index e323b730..6ed37aa8 100644 --- a/Sources/swift-doc/Supporting Types/Page.swift +++ b/Sources/swift-doc/Supporting Types/Page.swift @@ -2,21 +2,44 @@ import Foundation import SwiftDoc import SwiftMarkup import SwiftSemantics -import CommonMark import struct SwiftSemantics.Protocol +import CommonMark +import HypertextLiteral -protocol Page { - var body: Document { get } +protocol Page: HypertextLiteralConvertible { + var module: Module { get } + var title: String { get } + var document: CommonMark.Document { get } + var html: HypertextLiteral.HTML { get } +} + +extension Page { + var module: Module { fatalError("unimplemented") } + var title: String { fatalError("unimplemented") } } extension Page { - func write(to url: URL) throws { - let data = body.render(format: .commonmark).data(using: .utf8) + func write(to url: URL, format: SwiftDoc.Generate.Format) throws { + let data: Data? + switch format { + case .commonmark: + data = document.render(format: .commonmark).data(using: .utf8) + case .html: + data = layout(self).description.data(using: .utf8) + } + + let fileManager = FileManager.default + try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: [.posixPermissions: 0o744]) + try data?.write(to: url) - try FileManager.default.setAttributes([.posixPermissions: 0o744], ofItemAtPath: url.path) + try fileManager.setAttributes([.posixPermissions: 0o744], ofItemAtPath: url.path) } } +func path(for symbol: Symbol) -> String { + return path(for: symbol.id.description) +} + func path(for identifier: CustomStringConvertible) -> String { return "\(identifier)".replacingOccurrences(of: ".", with: "_") } diff --git a/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift b/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift index 31c0531e..bc17928b 100644 --- a/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift @@ -1,23 +1,43 @@ import Foundation import CommonMarkBuilder +import HypertextLiteral fileprivate let dateFormatter: DateFormatter = { + var dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none + return dateFormatter +}() + +fileprivate let timestampDateFormatter: DateFormatter = { var dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" return dateFormatter }() -struct FooterPage: Page { +fileprivate let href = "https://github.com/SwiftDocOrg/swift-doc" +struct FooterPage: Page { // MARK: - Page - var body: Document { - let timestamp = dateFormatter.string(from: Date()) + var document: CommonMark.Document { + let timestamp = timestampDateFormatter.string(from: Date()) return Document { Fragment { - "Generated at \(timestamp) using [swift-doc](https://github.com/SwiftDocOrg/swift-doc)." + "Generated at \(timestamp) using [swift-doc](\(href))." } } } + + var html: HypertextLiteral.HTML { + let timestamp = timestampDateFormatter.string(from: Date()) + let dateString = dateFormatter.string(from: Date()) + + return #""" +

+ Generated on using swift-doc. +

+ """# + } } diff --git a/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift b/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift index 60a5353c..8c655a11 100644 --- a/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift @@ -1,6 +1,7 @@ import SwiftSemantics import SwiftDoc import CommonMarkBuilder +import HypertextLiteral struct GlobalPage: Page { let module: Module @@ -14,17 +15,43 @@ struct GlobalPage: Page { } // MARK: - Page + + var title: String { + return name + } - var body: Document { + var document: CommonMark.Document { return Document { Heading { name } Section { ForEach(in: symbols) { symbol in Heading { symbol.id.description } - Documentation(for: symbol) + Documentation(for: symbol, in: module) } } } } + + var html: HypertextLiteral.HTML { + let description: String + + let descriptions = Set(symbols.map { String(describing: type(of: $0.api)) }) + if descriptions.count == 1 { + description = descriptions.first! + } else { + description = "Global" + } + + return #""" +

+ \#(description) + \#(softbreak(name)) +

+ + \#(symbols.map { symbol in + Documentation(for: symbol, in: module).html + }) + """# + } } diff --git a/Sources/swift-doc/Supporting Types/Pages/HomePage.swift b/Sources/swift-doc/Supporting Types/Pages/HomePage.swift index 91c75994..26b974f2 100644 --- a/Sources/swift-doc/Supporting Types/Pages/HomePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/HomePage.swift @@ -1,34 +1,33 @@ import CommonMarkBuilder import SwiftDoc import SwiftSemantics +import HypertextLiteral struct HomePage: Page { var module: Module + var classes: [Symbol] = [] + var enumerations: [Symbol] = [] + var structures: [Symbol] = [] + var protocols: [Symbol] = [] + var operatorNames: Set = [] + var globalTypealiasNames: Set = [] + var globalFunctionNames: Set = [] + var globalVariableNames: Set = [] + init(module: Module) { self.module = module - } - - // MARK: - Page - - var body: Document { - var typeNames: Set = [] - var protocolNames: Set = [] - var operatorNames: Set = [] - var globalTypealiasNames: Set = [] - var globalFunctionNames: Set = [] - var globalVariableNames: Set = [] for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { - switch symbol.declaration { + switch symbol.api { case is Class: - typeNames.insert(symbol.id.description) + classes.append(symbol) case is Enumeration: - typeNames.insert(symbol.id.description) + enumerations.append(symbol) case is Structure: - typeNames.insert(symbol.id.description) - case let `protocol` as Protocol: - protocolNames.insert(`protocol`.name) + structures.append(symbol) + case is Protocol: + protocols.append(symbol) case let `typealias` as Typealias: globalTypealiasNames.insert(`typealias`.name) case let `operator` as Operator: @@ -43,6 +42,18 @@ struct HomePage: Page { continue } } + } + + // MARK: - Page + + var title: String { + return module.name + } + + var document: CommonMark.Document { + let types = classes + enumerations + structures + let typeNames = Set(types.map { $0.id.description }) + let protocolNames = Set(protocols.map { $0.id.description }) return Document { ForEach(in: [ @@ -74,7 +85,7 @@ struct HomePage: Page { Heading { heading } List(of: names.sorted()) { name in - Link(urlString: path(for: name), text: name) + Link(urlString: path(for: name), text: softbreak(name)) } } } @@ -83,7 +94,37 @@ struct HomePage: Page { } } - var lines: [String] { - body.description.split(separator: "\n", omittingEmptySubsequences: false).map { String($0) } + var html: HypertextLiteral.HTML { + return #""" + \#([ + ("Classes", classes), + ("Structures", structures), + ("Enumerations", enumerations), + ("Protocols", protocols), + ].compactMap { (heading, symbols) -> HypertextLiteral.HTML? in + guard !symbols.isEmpty else { return nil } + + return #""" +
+

\#(heading)

+
+ \#(symbols.sorted().map { symbol -> HypertextLiteral.HTML in + let descriptor = String(describing: type(of: symbol.api)).lowercased() + return #""" +
+ + \#(softbreak(symbol.id.description)) + +
+
+ \#(commonmark: symbol.documentation?.summary ?? "") +
+ """# as HypertextLiteral.HTML + }) +
+
+ """# as HypertextLiteral.HTML + }) + """# } } diff --git a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift index a74558c3..4ac2961f 100644 --- a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift @@ -1,26 +1,23 @@ import SwiftSemantics import SwiftDoc import CommonMarkBuilder +import HypertextLiteral struct SidebarPage: Page { var module: Module + var typeNames: Set = [] + var protocolNames: Set = [] + var operatorNames: Set = [] + var globalTypealiasNames: Set = [] + var globalFunctionNames: Set = [] + var globalVariableNames: Set = [] + init(module: Module) { self.module = module - } - - // MARK: - Page - - var body: Document { - var typeNames: Set = [] - var protocolNames: Set = [] - var operatorNames: Set = [] - var globalTypealiasNames: Set = [] - var globalFunctionNames: Set = [] - var globalVariableNames: Set = [] for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { - switch symbol.declaration { + switch symbol.api { case is Class: typeNames.insert(symbol.id.description) case is Enumeration: @@ -43,7 +40,11 @@ struct SidebarPage: Page { continue } } + } + + // MARK: - Page + var document: CommonMark.Document { return Document { ForEach(in: ( [ @@ -58,17 +59,23 @@ struct SidebarPage: Page { // FIXME: This should be an HTML block Fragment { #""" -
+
\#(section.title) """# } List(of: section.names.sorted()) { name in - Link(urlString: path(for: name), text: name) + Link(urlString: "/" + path(for: name), text: name) } Fragment { "
" } } } } + + var html: HypertextLiteral.HTML { + #""" + \#(document) + """# + } } diff --git a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift index d1f75c83..6ed5f348 100644 --- a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift @@ -1,35 +1,46 @@ import SwiftSemantics import SwiftDoc import CommonMarkBuilder +import HypertextLiteral struct TypePage: Page { let module: Module let symbol: Symbol init(module: Module, symbol: Symbol) { - precondition(symbol.declaration is Type) + precondition(symbol.api is Type) self.module = module self.symbol = symbol } // MARK: - Page - var body: Document { - return Document { - Heading { symbol.id.description } - - Documentation(for: symbol) - - Inheritance(of: symbol, in: module) + var title: String { + return symbol.id.description + } - if symbol.declaration is Protocol { - ConformingTypes(to: symbol, in: module) - } else if symbol.declaration is Type { - NestedTypes(of: symbol, in: module) - } + var document: CommonMark.Document { + return CommonMark.Document { + Heading { symbol.id.description } + Documentation(for: symbol, in: module) + Relationships(of: symbol, in: module) Members(of: symbol, in: module) Requirements(of: symbol, in: module) } } + + var html: HypertextLiteral.HTML { + return #""" +

+ \#(String(describing: type(of: symbol.api))) + \#(softbreak(symbol.id.description)) +

+ + \#(Documentation(for: symbol, in: module).html) + \#(Relationships(of: symbol, in: module).html) + \#(Members(of: symbol, in: module).html) + \#(Requirements(of: symbol, in: module).html) + """# + } } diff --git a/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift b/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift index 0ca2b7a5..a342fa0a 100644 --- a/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift @@ -1,23 +1,39 @@ import SwiftSemantics import SwiftDoc import CommonMarkBuilder +import HypertextLiteral struct TypealiasPage: Page { let module: Module let symbol: Symbol init(module: Module, symbol: Symbol) { - precondition(symbol.declaration is Typealias) + precondition(symbol.api is Typealias) self.module = module self.symbol = symbol } // MARK: - Page - var body: Document { + var title: String { + return symbol.id.description + } + + var document: CommonMark.Document { Document { Heading { symbol.id.description } - Documentation(for: symbol) + Documentation(for: symbol, in: module) } } + + var html: HypertextLiteral.HTML { + #""" +

+ \#(String(describing: type(of: symbol.api))) + \#(softbreak(symbol.id.description)) +

+ + \#(Documentation(for: symbol, in: module).html) + """# + } } diff --git a/Tests/SwiftDocTests/SourceFileTests.swift b/Tests/SwiftDocTests/SourceFileTests.swift index 6346117c..a3ffb714 100644 --- a/Tests/SwiftDocTests/SourceFileTests.swift +++ b/Tests/SwiftDocTests/SourceFileTests.swift @@ -59,18 +59,18 @@ final class SourceFileTests: XCTestCase { XCTAssertEqual(sourceFile.symbols.count, 12) for symbol in sourceFile.symbols { - XCTAssert(symbol.isPublic, "\(symbol.declaration) isn't public") + XCTAssert(symbol.isPublic, "\(symbol.api) isn't public") } do { let `protocol` = sourceFile.symbols[0] - XCTAssert(`protocol`.declaration is Protocol) + XCTAssert(`protocol`.api is Protocol) XCTAssertEqual(`protocol`.documentation?.summary, "Protocol") do { let function = sourceFile.symbols[1] - XCTAssert(function.declaration is Function) + XCTAssert(function.api is Function) XCTAssertEqual(function.context.count, 1) XCTAssert(function.context.first is Symbol) @@ -82,7 +82,7 @@ final class SourceFileTests: XCTestCase { do { let property = sourceFile.symbols[2] - XCTAssert(property.declaration is Variable) + XCTAssert(property.api is Variable) XCTAssertEqual(property.context.count, 1) XCTAssert(property.context.first is Symbol) @@ -94,13 +94,13 @@ final class SourceFileTests: XCTestCase { do { let enumeration = sourceFile.symbols[3] - XCTAssert(enumeration.declaration is Enumeration) + XCTAssert(enumeration.api is Enumeration) XCTAssertEqual(enumeration.documentation?.summary, "Enumeration") do { let `case` = sourceFile.symbols[4] - XCTAssert(`case`.declaration is Enumeration.Case) + XCTAssert(`case`.api is Enumeration.Case) XCTAssertEqual(`case`.context.count, 1) XCTAssert(`case`.context.first is Symbol) @@ -112,7 +112,7 @@ final class SourceFileTests: XCTestCase { do { let structure = sourceFile.symbols[5] - XCTAssert(structure.declaration is Structure) + XCTAssert(structure.api is Structure) XCTAssertEqual(structure.documentation?.summary, "Structure") } @@ -120,7 +120,7 @@ final class SourceFileTests: XCTestCase { do { let function = sourceFile.symbols[6] - XCTAssert(function.declaration is Function) + XCTAssert(function.api is Function) XCTAssertEqual(function.context.count, 1) XCTAssert(function.context.first is Extension) @@ -132,7 +132,7 @@ final class SourceFileTests: XCTestCase { do { let property = sourceFile.symbols[7] - XCTAssert(property.declaration is Variable) + XCTAssert(property.api is Variable) XCTAssertEqual(property.context.count, 1) XCTAssert(property.context.first is Extension) @@ -144,13 +144,13 @@ final class SourceFileTests: XCTestCase { do { let `class` = sourceFile.symbols[8] - XCTAssert(`class`.declaration is Class) + XCTAssert(`class`.api is Class) XCTAssertEqual(`class`.documentation?.summary, "Class") do { let function = sourceFile.symbols[9] - XCTAssert(function.declaration is Function) + XCTAssert(function.api is Function) XCTAssertEqual(function.context.count, 1) XCTAssert(function.context.first is Symbol) @@ -162,7 +162,7 @@ final class SourceFileTests: XCTestCase { do { let property = sourceFile.symbols[10] - XCTAssert(property.declaration is Variable) + XCTAssert(property.api is Variable) XCTAssertEqual(property.context.count, 1) XCTAssert(property.context.first is Symbol) @@ -174,8 +174,8 @@ final class SourceFileTests: XCTestCase { do { let `class` = sourceFile.symbols[11] - XCTAssert(`class`.declaration is Class) - XCTAssertEqual((`class`.declaration as? Class)?.inheritance, ["C"]) + XCTAssert(`class`.api is Class) + XCTAssertEqual((`class`.api as? Class)?.inheritance, ["C"]) XCTAssertEqual(`class`.documentation?.summary, "Subclass") } }