From 57605fc8dda74a521e3d913a8df1acd5a69ef2b2 Mon Sep 17 00:00:00 2001 From: Mattt Date: Tue, 5 May 2020 12:09:00 -0700 Subject: [PATCH 1/9] Refactor to Generator models Initial implementation of DocSet generation --- Package.resolved | 9 + Package.swift | 3 +- Sources/swift-doc/Subcommands/Diagram.swift | 1 - Sources/swift-doc/Subcommands/Generate.swift | 158 +++++---------- .../Supporting Types/Generator.swift | 5 + .../Generators/CommonMarkGenerator.swift | 67 +++++++ .../Generators/DocSetGenerator.swift | 184 ++++++++++++++++++ .../Generators/HTMLGenerator.swift | 74 +++++++ .../swift-doc/Supporting Types/Helpers.swift | 5 + Sources/swift-doc/Supporting Types/Page.swift | 16 -- .../Supporting Types/Pages/TypePage.swift | 5 +- 11 files changed, 394 insertions(+), 133 deletions(-) create mode 100644 Sources/swift-doc/Supporting Types/Generator.swift create mode 100644 Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift create mode 100644 Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift create mode 100644 Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift diff --git a/Package.resolved b/Package.resolved index 144e7e7a..978eb663 100644 --- a/Package.resolved +++ b/Package.resolved @@ -37,6 +37,15 @@ "version": "0.0.3" } }, + { + "package": "SQLite.swift", + "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", + "state": { + "branch": "0.12.2", + "revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1", + "version": null + } + }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", diff --git a/Package.swift b/Package.swift index b5b63b6b..6b8b72b8 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( .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/stephencelis/SQLite.swift.git", .revision("0.12.2")), .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.0.5")), .package(url: "https://github.com/apple/swift-log.git", .upToNextMinor(from: "1.2.0")), .package(url: "https://github.com/NSHipster/swift-log-github-actions.git", .upToNextMinor(from: "0.0.1")), @@ -27,7 +28,7 @@ let package = Package( // 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", "HypertextLiteral", "Markup", "DCOV", "GraphViz", "SwiftSyntaxHighlighter", "Logging", "LoggingGitHubActions"] + dependencies: ["ArgumentParser", "SwiftDoc", "SwiftSemantics", "SwiftMarkup", "CommonMarkBuilder", "HypertextLiteral", "Markup", "DCOV", "GraphViz", "SwiftSyntaxHighlighter", "SQLite", "Logging", "LoggingGitHubActions"] ), .target( name: "DCOV", diff --git a/Sources/swift-doc/Subcommands/Diagram.swift b/Sources/swift-doc/Subcommands/Diagram.swift index 50c72443..1e01280a 100644 --- a/Sources/swift-doc/Subcommands/Diagram.swift +++ b/Sources/swift-doc/Subcommands/Diagram.swift @@ -5,7 +5,6 @@ import SwiftSemantics import GraphViz import DOT - extension SwiftDoc { struct Diagram: ParsableCommand { struct Options: ParsableArguments { diff --git a/Sources/swift-doc/Subcommands/Generate.swift b/Sources/swift-doc/Subcommands/Generate.swift index 3001b641..1c702e76 100644 --- a/Sources/swift-doc/Subcommands/Generate.swift +++ b/Sources/swift-doc/Subcommands/Generate.swift @@ -4,130 +4,60 @@ import SwiftDoc import SwiftMarkup import SwiftSemantics import struct SwiftSemantics.Protocol +import SQLite 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 - - @Option(name: .customLong("base-url"), - default: "/", - help: "The base URL used for all relative URLs in generated documents.") - var baseURL: String - } - - static var configuration = CommandConfiguration(abstract: "Generates Swift documentation") - - @OptionGroup() - var options: Options - - func run() throws { - 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 { - let format = options.format - - var pages: [String: Page] = [:] - - var globals: [String: [Symbol]] = [:] - for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { - 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: - 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: - globals[variable.name, default: []] += [symbol] - default: - continue - } + struct Generate: ParsableCommand { + enum Format: String, ExpressibleByArgument { + case commonmark + case html + case docset } - for (name, symbols) in globals { - pages[path(for: name)] = GlobalPage(module: module, name: name, symbols: symbols) - } + struct Options: ParsableArguments { + @Argument(help: "One or more paths to Swift files") + var inputs: [String] - guard !pages.isEmpty else { - logger.warning("No public API symbols were found at the specified path. No output was written.") - return - } + @Option(name: [.long, .customShort("n")], + help: "The name of the module") + var moduleName: String - if pages.count == 1, let page = pages.first?.value { - let filename: String - switch format { - case .commonmark: - filename = "Home.md" - case .html: - filename = "index.html" - } + @Option(name: .shortAndLong, + default: ".build/documentation", + help: "The path for generated output") + var output: String - let url = outputDirectoryURL.appendingPathComponent(filename) - try page.write(to: url, format: format, baseURL: options.baseURL) - } else { - switch format { - case .commonmark: - pages["Home"] = HomePage(module: module) - pages["_Sidebar"] = SidebarPage(module: module) - pages["_Footer"] = FooterPage() - case .html: - pages["Home"] = HomePage(module: module) - } + @Option(name: .shortAndLong, + default: .commonmark, + help: "The output format") + var format: Format - 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, baseURL: options.baseURL) - } + @Option(name: .customLong("base-url"), + default: "/", + help: "The base URL used for all relative URLs in generated documents.") + var baseURL: String } - if case .html = format { - let cssData = try fetchRemoteCSS() - let cssURL = outputDirectoryURL.appendingPathComponent("all.css") - try writeFile(cssData, to: cssURL) + static var configuration = CommandConfiguration(abstract: "Generates Swift documentation") + + @OptionGroup() + var options: Options + + func run() throws { + do { + let module = try Module(name: options.moduleName, paths: options.inputs) + + switch options.format { + case .commonmark: + try CommonMarkGenerator.generate(for: module, with: options) + case .html: + try HTMLGenerator.generate(for: module, with: options) + case .docset: + try DocSetGenerator.generate(for: module, with: options) + } + } catch { + logger.error("\(error)") + } } - - } catch { - logger.error("\(error)") - } } - } -} - -func fetchRemoteCSS() throws -> Data { - let url = URL(string: "https://raw.githubusercontent.com/SwiftDocOrg/swift-doc/master/Resources/all.min.css")! - return try Data(contentsOf: url) } diff --git a/Sources/swift-doc/Supporting Types/Generator.swift b/Sources/swift-doc/Supporting Types/Generator.swift new file mode 100644 index 00000000..3454de9d --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Generator.swift @@ -0,0 +1,5 @@ +import SwiftDoc + +protocol Generator { + static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws +} diff --git a/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift new file mode 100644 index 00000000..2b0fc730 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift @@ -0,0 +1,67 @@ +import Foundation +import SwiftDoc +import SwiftSemantics +import struct SwiftSemantics.Protocol + +enum CommonMarkGenerator: Generator { + static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { + assert(options.format == .commonmark) + + 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) + + var pages: [String: Page] = [:] + + var globals: [String: [Symbol]] = [:] + for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { + 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: + 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: + globals[variable.name, default: []] += [symbol] + default: + continue + } + } + + for (name, symbols) in globals { + pages[path(for: name)] = GlobalPage(module: module, name: name, symbols: symbols) + } + + guard !pages.isEmpty else { + logger.warning("No public API symbols were found at the specified path. No output was written.") + return + } + + if pages.count == 1, let page = pages.first?.value { + let filename = "Home.md" + let url = outputDirectoryURL.appendingPathComponent(filename) + try page.write(to: url, baseURL: options.baseURL) + } else { + pages["Home"] = HomePage(module: module) + pages["_Sidebar"] = SidebarPage(module: module) + pages["_Footer"] = FooterPage() + + try pages.map { $0 }.parallelForEach { + let filename = "\($0.key).md" + let url = outputDirectoryURL.appendingPathComponent(filename) + try $0.value.write(to: url, baseURL: options.baseURL) + } + } + } +} + +// MARK: - + +fileprivate extension Page { + func write(to url: URL, baseURL: String) throws { + guard let data = document.render(format: .commonmark).data(using: .utf8) else { return } + try writeFile(data, to: url) + } +} diff --git a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift new file mode 100644 index 00000000..ec21da30 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift @@ -0,0 +1,184 @@ +import Foundation +import HypertextLiteral +import SwiftDoc +import class SwiftDoc.Module +import SwiftSemantics +import struct SwiftSemantics.Protocol +import SQLite + +fileprivate typealias XML = HTML + +enum DocSetGenerator: Generator { + static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { + assert(options.format == .docset) + + let outputDirectoryURL = URL(fileURLWithPath: options.output) + try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) + + let docsetURL = outputDirectoryURL.appendingPathComponent("\(module.name).docset") + + let docsetDocumentsDirectoryURL = docsetURL.appendingPathComponent("Contents/Resources/Documents/") + try fileManager.createDirectory(at: docsetDocumentsDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) + + var options = options + options.format = .html + options.output = docsetDocumentsDirectoryURL.path + try HTMLGenerator.generate(for: module, with: options) + + let info: [String: Any] = [ + "CFBundleIdentifier": module.name.lowercased(), + "CFBundleName": module.name, + "DocSetPlatformFamily": "swift", + "isDashDocset": true, + "DashDocSetFamily": "dashtoc" + ] + + let plist = try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0) + try plist.write(to: docsetURL.appendingPathComponent("Contents/Info.plist")) + + let indexURL = docsetURL.appendingPathComponent("Contents/Resources/docSet.dsidx") + + let db = try Connection(indexURL.path) + + let searchIndex = Table("searchIndex") + + let id = Expression("id") + let name = Expression("name") + let type = Expression("type") + let path = Expression("path") + + try db.transaction { + try db.run(searchIndex.drop(ifExists: true)) + try db.run(searchIndex.create { t in + t.column(id, primaryKey: true) + t.column(name) + t.column(type) + t.column(path) + t.unique([name, type, path]) + }) + } + + try db.transaction { + for symbol in module.interface.symbols { + try db.run(searchIndex.insert(or: .ignore, + name <- symbol.id.description, + type <- symbol.entryType, + path <- symbol.route + )) + } + } + + let tokens: XML = #""" + + + \#(module.interface.topLevelSymbols.map { $0.token }) + + """# + + let tokensURL = docsetURL.appendingPathComponent("Contents/Resources/Tokens.xml") + try tokens.description.write(to: tokensURL, atomically: true, encoding: .utf8) + } +} + + +fileprivate extension Symbol { + // https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/HeaderDoc/anchors/anchors.html#//apple_ref/doc/uid/TP40001215-CH347 + var symbolType: String { + let parent = context.compactMap { $0 as? Symbol }.last?.api + let isClassOrStatic = api.modifiers.contains { $0.name == "class" } || api.modifiers.contains { $0.name == "static" } + + switch api { + case is Class: + return "cl" + case is Structure, is Enumeration: + return "tdef" + case is Protocol: + return "intf" + case is Function where parent is Protocol && isClassOrStatic: + return "intfcm" + case is Function where parent is Protocol: + return "intfm" + case is Function where parent is Type && isClassOrStatic: + return "clm" + case is Function where parent is Type: + return "instm" + case is Function, is Operator: + return "func" + case is Variable where parent is Protocol: + return "intfp" + case is Variable where parent is Type && isClassOrStatic: + return "clconst" + case is Variable where parent is Type: + return "instp" + case is Enumeration.Case: + return "econst" + default: + return "data" + } + } + + var token: XML { + let scope = context.compactMap { $0 as? Symbol }.last?.id.description + + return #""" + + + \#(id) + swift + \#(symbolType) + \#(scope ?? "") + + + """# + } + + var route: String { + if let parent = context.compactMap({ $0 as? Symbol }).last { + return path(for: parent) + "/index.html" + "#\(id.description.lowercased().replacingOccurrences(of: " ", with: "-"))" + } else { + return path(for: self) + "/index.html" + } + } +} + +fileprivate extension Symbol { + // https://kapeli.com/docsets#supportedentrytypes + var entryType: String { + let parent = context.compactMap { $0 as? Symbol }.last?.api + + switch api { + case is Class: + return "Class" + case is Initializer: + return "Constructor" + case is Enumeration: + return "Enum" + case is Enumeration.Case: + return "Value" + case let type as Type where type.inheritance.contains(where: { $0.hasSuffix("Error") }): + return "Error" + case is Function where parent is Type: + return "Method" + case is Function: + return "Function" + case is Variable where parent is Type: + return "Property" + case let variable as Variable where variable.keyword == "let": + return "Constant" + case is Variable: + return "Variable" + case is Operator: + return "Operator" + case is PrecedenceGroup: + return "Procedure" // FIXME: no direct matching entry type + case is Protocol: + return "Protocol" + case is Structure: + return "Struct" + case is Type, is AssociatedType: + return "Type" + default: + return "Entry" + } + } +} diff --git a/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift new file mode 100644 index 00000000..21d4fcbe --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift @@ -0,0 +1,74 @@ +import Foundation +import SwiftDoc +import SwiftSemantics +import struct SwiftSemantics.Protocol + +enum HTMLGenerator: Generator { + static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { + assert(options.format == .html) + + let outputDirectoryURL = URL(fileURLWithPath: options.output) + try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) + + var pages: [String: Page] = [:] + + var globals: [String: [Symbol]] = [:] + for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { + 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: + 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: + globals[variable.name, default: []] += [symbol] + default: + continue + } + } + + for (name, symbols) in globals { + pages[path(for: name)] = GlobalPage(module: module, name: name, symbols: symbols) + } + + guard !pages.isEmpty else { + logger.warning("No public API symbols were found at the specified path. No output was written.") + return + } + + let cssData = try fetchRemoteCSS() + let cssURL = outputDirectoryURL.appendingPathComponent("all.css") + try writeFile(cssData, to: cssURL) + + if pages.count == 1, let page = pages.first?.value { + let filename = "index.html" + let url = outputDirectoryURL.appendingPathComponent(filename) + try page.write(to: url, baseURL: options.baseURL) + } else { + pages["Home"] = HomePage(module: module) + + try pages.map { $0 }.parallelForEach { + let filename: String + if $0.key == "Home" { + filename = "index.html" + } else { + filename = "\($0.key)/index.html" + } + + let url = outputDirectoryURL.appendingPathComponent(filename) + try $0.value.write(to: url, baseURL: options.baseURL) + } + } + } +} + +// MARK: - + +fileprivate extension Page { + func write(to url: URL, baseURL: String) throws { + let data = layout(self, baseURL: baseURL).description.data(using: .utf8) + guard let filedata = data else { return } + try writeFile(filedata, to: url) + } +} diff --git a/Sources/swift-doc/Supporting Types/Helpers.swift b/Sources/swift-doc/Supporting Types/Helpers.swift index 695509b7..f95f1350 100644 --- a/Sources/swift-doc/Supporting Types/Helpers.swift +++ b/Sources/swift-doc/Supporting Types/Helpers.swift @@ -94,3 +94,8 @@ public func softbreak(_ string: String) -> String { return regex.stringByReplacingMatches(in: string, options: [], range: NSRange(string.startIndex.. Data { + let url = URL(string: "https://raw.githubusercontent.com/SwiftDocOrg/swift-doc/master/Resources/all.min.css")! + return try Data(contentsOf: url) +} diff --git a/Sources/swift-doc/Supporting Types/Page.swift b/Sources/swift-doc/Supporting Types/Page.swift index 639ddeae..7117ee3f 100644 --- a/Sources/swift-doc/Supporting Types/Page.swift +++ b/Sources/swift-doc/Supporting Types/Page.swift @@ -18,22 +18,6 @@ extension Page { var title: String { fatalError("unimplemented") } } -extension Page { - func write(to url: URL, format: SwiftDoc.Generate.Format, baseURL: String) throws { - let data: Data? - switch format { - case .commonmark: - data = document.render(format: .commonmark).data(using: .utf8) - case .html: - data = layout(self, baseURL: baseURL).description.data(using: .utf8) - } - - guard let filedata = data else { return } - - try writeFile(filedata, to: url) - } -} - func path(for symbol: Symbol) -> String { return path(for: symbol.id.description) } diff --git a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift index 6ed5f348..5c5b14ff 100644 --- a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift @@ -31,9 +31,12 @@ struct TypePage: Page { } var html: HypertextLiteral.HTML { + let typeName = String(describing: type(of: symbol.api)) + return #""" +

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

From 9ec7357ddd517f6af672328ba9dd4e928fcc1ccb Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 8 May 2020 05:40:27 -0700 Subject: [PATCH 2/9] Install sqlite3 on Linux --- .github/workflows/ci.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87d8c6de..fc44c632 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: +on: push: branches: [master] pull_request: @@ -30,6 +30,6 @@ jobs: - name: Install System Dependencies run: | apt-get update - apt-get install -y libxml2-dev + apt-get install -y libxml2-dev sqlite3 - name: Build and Test run: swift test -c release --enable-test-discovery diff --git a/Dockerfile b/Dockerfile index 932b67dd..401ad1af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ FROM swift:5.2 as builder WORKDIR /swiftdoc COPY . . -RUN apt-get -qq update && apt-get install -y libxml2-dev && rm -r /var/lib/apt/lists/* +RUN apt-get -qq update && apt-get install -y libxml2-dev sqlite3 && rm -r /var/lib/apt/lists/* RUN mkdir -p /build/lib && cp -R /usr/lib/swift/linux/*.so* /build/lib RUN make install prefix=/build FROM ubuntu:18.04 -RUN apt-get -qq update && apt-get install -y libatomic1 libxml2-dev && rm -r /var/lib/apt/lists/* +RUN apt-get -qq update && apt-get install -y libatomic1 libxml2-dev sqlite3 && rm -r /var/lib/apt/lists/* COPY --from=builder /build/bin/swift-doc /usr/bin COPY --from=builder /build/lib/* /usr/lib/ ENTRYPOINT ["swift-doc"] From 56d585cac6d4ef3a4e48218f506625b1e5da6a1f Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 8 May 2020 05:48:47 -0700 Subject: [PATCH 3/9] Add dashIndexFilePath key to Info.plist --- .../Supporting Types/Generators/DocSetGenerator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift index ec21da30..40bcd8b4 100644 --- a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift @@ -30,7 +30,8 @@ enum DocSetGenerator: Generator { "CFBundleName": module.name, "DocSetPlatformFamily": "swift", "isDashDocset": true, - "DashDocSetFamily": "dashtoc" + "DashDocSetFamily": "dashtoc", + "dashIndexFilePath": "index.html" ] let plist = try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0) From c3961c9a2cfc65864e208e8ac6733d93cf048863 Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 8 May 2020 05:52:15 -0700 Subject: [PATCH 4/9] Categorize initializers and subscripts as methods --- .../Supporting Types/Generators/DocSetGenerator.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift index 40bcd8b4..b6d71784 100644 --- a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift @@ -151,7 +151,7 @@ fileprivate extension Symbol { case is Class: return "Class" case is Initializer: - return "Constructor" + return "Method" case is Enumeration: return "Enum" case is Enumeration.Case: @@ -176,6 +176,8 @@ fileprivate extension Symbol { return "Protocol" case is Structure: return "Struct" + case is Subscript: + return "Method" case is Type, is AssociatedType: return "Type" default: From 3ec64bd7a64f27dd5eafb13f9bacc74e0ca99631 Mon Sep 17 00:00:00 2001 From: Mattt Date: Fri, 8 May 2020 06:36:02 -0700 Subject: [PATCH 5/9] Install libsqlite3-dev instead of sqlite3 --- .github/workflows/ci.yml | 2 +- Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc44c632..95beda3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,6 @@ jobs: - name: Install System Dependencies run: | apt-get update - apt-get install -y libxml2-dev sqlite3 + apt-get install -y libxml2-dev libsqlite3-dev - name: Build and Test run: swift test -c release --enable-test-discovery diff --git a/Dockerfile b/Dockerfile index 401ad1af..6ac85dff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ FROM swift:5.2 as builder WORKDIR /swiftdoc COPY . . -RUN apt-get -qq update && apt-get install -y libxml2-dev sqlite3 && rm -r /var/lib/apt/lists/* +RUN apt-get -qq update && apt-get install -y libxml2-dev libsqlite3-dev && rm -r /var/lib/apt/lists/* RUN mkdir -p /build/lib && cp -R /usr/lib/swift/linux/*.so* /build/lib RUN make install prefix=/build FROM ubuntu:18.04 -RUN apt-get -qq update && apt-get install -y libatomic1 libxml2-dev sqlite3 && rm -r /var/lib/apt/lists/* +RUN apt-get -qq update && apt-get install -y libatomic1 libxml2-dev libsqlite3-dev && rm -r /var/lib/apt/lists/* COPY --from=builder /build/bin/swift-doc /usr/bin COPY --from=builder /build/lib/* /usr/lib/ ENTRYPOINT ["swift-doc"] From 20db37296ccea5ca01e57d9b62d12faadfc1a637 Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 27 Jul 2020 12:30:00 -0700 Subject: [PATCH 6/9] Skip hidden files during source file enumeration --- Sources/SwiftDoc/Module.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftDoc/Module.swift b/Sources/SwiftDoc/Module.swift index 0b09eb8d..9ec6c229 100644 --- a/Sources/SwiftDoc/Module.swift +++ b/Sources/SwiftDoc/Module.swift @@ -22,7 +22,7 @@ public final class Module { let fileManager = FileManager.default for path in paths { let directory = URL(fileURLWithPath: path) - guard let directoryEnumerator = fileManager.enumerator(at: directory, includingPropertiesForKeys: nil) else { continue } + guard let directoryEnumerator = fileManager.enumerator(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { continue } for case let url as URL in directoryEnumerator { var isDirectory: ObjCBool = false guard url.pathExtension == "swift", From c51df724c23dc2a8f88df4d4351c7133895019f3 Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 27 Jul 2020 13:01:31 -0700 Subject: [PATCH 7/9] Fix routing for symbol documentation --- .../Generators/DocSetGenerator.swift | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift index d4e4c3d1..bb2546b1 100644 --- a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift @@ -64,7 +64,7 @@ enum DocSetGenerator: Generator { try db.run(searchIndex.insert(or: .ignore, name <- symbol.id.description, type <- symbol.entryType, - path <- symbol.route + path <- symbol.route(with: options.baseURL) )) } } @@ -118,31 +118,6 @@ fileprivate extension Symbol { } } - var token: XML { - let scope = context.compactMap { $0 as? Symbol }.last?.id.description - - return #""" - - - \#(id) - swift - \#(symbolType) - \#(scope ?? "") - - - """# - } - - var route: String { - if let parent = context.compactMap({ $0 as? Symbol }).last { - return path(for: parent, with: "") + "/index.html" + "#\(id.description.lowercased().replacingOccurrences(of: " ", with: "-"))" - } else { - return path(for: self, with: "") + "/index.html" - } - } -} - -fileprivate extension Symbol { // https://kapeli.com/docsets#supportedentrytypes var entryType: String { let parent = context.compactMap { $0 as? Symbol }.last?.api @@ -184,4 +159,27 @@ fileprivate extension Symbol { return "Entry" } } + + var token: XML { + let scope = context.compactMap { $0 as? Symbol }.last?.id.description + + return #""" + + + \#(id) + swift + \#(symbolType) + \#(scope ?? "") + + + """# + } + + func route(with baseURL: String) -> String { + if let parent = context.compactMap({ $0 as? Symbol }).last { + return path(for: parent, with: baseURL) + "/index.html" + "#\(id.description.lowercased().replacingOccurrences(of: " ", with: "-"))" + } else { + return path(for: self, with: baseURL) + "/index.html" + } + } } From 60605603444afbe76f3c3a494749baf2aada726c Mon Sep 17 00:00:00 2001 From: Mattt Date: Mon, 27 Jul 2020 13:01:44 -0700 Subject: [PATCH 8/9] Remove unused code --- Sources/swift-doc/Supporting Types/Page.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Sources/swift-doc/Supporting Types/Page.swift b/Sources/swift-doc/Supporting Types/Page.swift index 5af9351b..248b971a 100644 --- a/Sources/swift-doc/Supporting Types/Page.swift +++ b/Sources/swift-doc/Supporting Types/Page.swift @@ -19,22 +19,6 @@ extension Page { var title: String { fatalError("unimplemented") } } -extension Page { - 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) - } - - guard let filedata = data else { return } - - try writeFile(filedata, to: url) - } -} - func writeFile(_ data: Data, to url: URL) throws { let fileManager = FileManager.default try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: [.posixPermissions: 0o744]) From d23ca26d686bef38fda45dcd73717d80d8c69a43 Mon Sep 17 00:00:00 2001 From: Mattt Date: Tue, 13 Oct 2020 10:22:20 -0700 Subject: [PATCH 9/9] Checkpoint --- Sources/SwiftDoc/Helpers.swift | 42 ++-- Sources/SwiftDoc/Identifier.swift | 85 ++++++++ Sources/SwiftDoc/SipHasher.swift | 193 ++++++++++++++++++ Sources/SwiftDoc/Symbol.swift | 9 +- .../Extensions/Foundation+Extensions.swift | 19 ++ .../Extensions/SwiftDoc+Extensions.swift | 4 +- Sources/swift-doc/Subcommands/Coverage.swift | 2 +- Sources/swift-doc/Subcommands/Diagram.swift | 2 +- Sources/swift-doc/Subcommands/Generate.swift | 18 +- Sources/swift-doc/Supporting Types/Base.swift | 29 +++ .../Components/Abstract.swift | 12 +- .../Components/Declaration.swift | 6 +- .../Components/Documentation.swift | 20 +- .../Supporting Types/Components/Members.swift | 12 +- .../Components/Relationships.swift | 10 +- .../Components/Requirements.swift | 8 +- .../Supporting Types/Generator.swift | 3 +- .../Generators/CommonMarkGenerator.swift | 69 ++++--- .../Generators/DocSetGenerator.swift | 55 +++-- .../Generators/HTMLGenerator.swift | 114 +++++++++-- .../swift-doc/Supporting Types/Helpers.swift | 36 ++-- .../swift-doc/Supporting Types/Layout.swift | 16 +- Sources/swift-doc/Supporting Types/Page.swift | 20 +- .../Supporting Types/Pages/FooterPage.swift | 20 +- .../Supporting Types/Pages/GlobalPage.swift | 59 +++--- .../Supporting Types/Pages/HomePage.swift | 61 +++--- .../Supporting Types/Pages/SidebarPage.swift | 33 +-- .../Supporting Types/Pages/TypePage.swift | 32 +-- .../Pages/TypealiasPage.swift | 19 +- .../swift-doc/Supporting Types/Router.swift | 96 +++++++++ Sources/swift-doc/main.swift | 4 +- .../SourceFile+Extensions.swift} | 10 +- Tests/SwiftDocTests/InterfaceTypeTests.swift | 4 +- Tests/SwiftDocTests/NestedTypesTests.swift | 14 +- .../OperatorIdentifierTests.swift | 140 +++++++++++++ Tests/SwiftDocTests/PathTests.swift | 82 ++++---- Tests/SwiftDocTests/SourceFileTests.swift | 5 +- 37 files changed, 1025 insertions(+), 338 deletions(-) create mode 100644 Sources/SwiftDoc/SipHasher.swift create mode 100644 Sources/swift-doc/Extensions/Foundation+Extensions.swift create mode 100644 Sources/swift-doc/Supporting Types/Base.swift create mode 100644 Sources/swift-doc/Supporting Types/Router.swift rename Tests/SwiftDocTests/{Helpers/temporaryFile.swift => Extensions/SourceFile+Extensions.swift} (61%) create mode 100644 Tests/SwiftDocTests/OperatorIdentifierTests.swift diff --git a/Sources/SwiftDoc/Helpers.swift b/Sources/SwiftDoc/Helpers.swift index 7a19bb28..580c3c51 100644 --- a/Sources/SwiftDoc/Helpers.swift +++ b/Sources/SwiftDoc/Helpers.swift @@ -1,22 +1,22 @@ import Foundation - -public func route(for symbol: Symbol) -> String { - return route(for: symbol.id) -} - -public func route(for name: CustomStringConvertible) -> String { - return name.description.replacingOccurrences(of: ".", with: "_") -} - -public func path(for symbol: Symbol, with baseURL: String) -> String { - return path(for: route(for: symbol), with: baseURL) -} - -public func path(for identifier: CustomStringConvertible, with baseURL: String) -> String { - let url = URL(string: baseURL)?.appendingPathComponent("\(identifier)") ?? URL(string: "\(identifier)") - guard let string = url?.absoluteString else { - fatalError("Unable to construct path for \(identifier) with baseURL \(baseURL)") - } - - return string -} +// +//public func route(for symbol: Symbol) -> String { +// return route(for: symbol.id) +//} +// +//public func route(for name: CustomStringConvertible) -> String { +// return name.description.replacingOccurrences(of: ".", with: "_") +//} +// +//public func path(for symbol: Symbol, with baseURL: String) -> String { +// return path(for: route(for: symbol), with: baseURL) +//} +// +//public func path(for identifier: CustomStringConvertible, with baseURL: String) -> String { +// let url = URL(string: baseURL)?.appendingPathComponent("\(identifier)") ?? URL(string: "\(identifier)") +// guard let string = url?.absoluteString else { +// fatalError("Unable to construct path for \(identifier) with baseURL \(baseURL)") +// } +// +// return string +//} diff --git a/Sources/SwiftDoc/Identifier.swift b/Sources/SwiftDoc/Identifier.swift index bd9bd2d2..c1c0ded8 100644 --- a/Sources/SwiftDoc/Identifier.swift +++ b/Sources/SwiftDoc/Identifier.swift @@ -1,6 +1,51 @@ +import SwiftSemantics + public struct Identifier: Hashable { public let pathComponents: [String] public let name: String + public let checksum: String + + public init(symbol: Symbol) { + self.pathComponents = symbol.context.compactMap { + ($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType + } + + self.name = { + switch symbol.api { + case let function as Function where function.isOperator: + var components = symbol.api.nonAccessModifiers.map { $0.name } + if components.isEmpty { + components.append("infix") + } + + components.append(function.identifier) + return components.joined(separator: " ") + case let `operator` as Operator: + var components = symbol.api.nonAccessModifiers.map { $0.name } + if components.isEmpty { + components.append("infix") + } + + components.append(`operator`.name) + return components.joined(separator: " ") + default: + return symbol.api.name + } + }() + + var hasher = SipHasher() + var declaration = "\(symbol.api)" + print(declaration) + withUnsafeBytes(of: &declaration) { hasher.append($0) } + let hashValue = hasher.finalize() + + self.checksum = String(UInt(bitPattern: hashValue), radix: 32, uppercase: false) + print(checksum) + } + + public var escaped: String { + description.escaped + } public func matches(_ string: String) -> Bool { (pathComponents + CollectionOfOne(name)).reversed().starts(with: string.split(separator: ".").map { String($0) }.reversed()) @@ -14,3 +59,43 @@ extension Identifier: CustomStringConvertible { (pathComponents + CollectionOfOne(name)).joined(separator: ".") } } + +// MARK: - + +fileprivate let replacements: [Character: String] = [ + "-": "minus", + ".": "dot", + "!": "bang", + "?": "quest", + "*": "star", + "/": "slash", + "&": "amp", + "%": "percent", + "^": "caret", + "+": "plus", + "<": "lt", + "=": "equals", + ">": "gt", + "|": "bar", + "~": "tilde" +] + +fileprivate extension String { + var escaped: String { + zip(indices, self).reduce(into: "") { (result, element) in + let (cursor, character) = element + if let replacement = replacements[character] { + result.append(contentsOf: replacement) + if cursor != index(before: endIndex) { + result.append("-") + } + } else if character == " " { + result.append("-") + } else if !character.isPunctuation, + !character.isWhitespace + { + result.append(character) + } + } + } +} diff --git a/Sources/SwiftDoc/SipHasher.swift b/Sources/SwiftDoc/SipHasher.swift new file mode 100644 index 00000000..d6a371dd --- /dev/null +++ b/Sources/SwiftDoc/SipHasher.swift @@ -0,0 +1,193 @@ +// +// SipHasher.swift +// SipHash +// +// Created by Károly Lőrentey on 2016-03-08. +// Copyright © 2016-2017 Károly Lőrentey. + +private func rotateLeft(_ value: UInt64, by amount: UInt64) -> UInt64 { + return (value << amount) | (value >> (64 - amount)) +} + +/// An implementation of the [SipHash-2-4](https://131002.net/siphash) hashing algorithm, +/// suitable for use in projects outside the Swift standard library. +/// (The Swift stdlib already includes SipHash; unfortunately its API is not public.) +/// +/// SipHash was invented by Jean-Philippe Aumasson and Daniel J. Bernstein. +public struct SipHasher { + /// The number of compression rounds. + private static let c = 2 + /// The number of finalization rounds. + private static let d = 4 + + /// Word 0 of the internal state, initialized to ASCII encoding of "somepseu". + var v0: UInt64 = 0x736f6d6570736575 + /// Word 1 of the internal state, initialized to ASCII encoding of "dorandom". + var v1: UInt64 = 0x646f72616e646f6d + /// Word 2 of the internal state, initialized to ASCII encoding of "lygenera". + var v2: UInt64 = 0x6c7967656e657261 + /// Word 3 of the internal state, initialized to ASCII encoding of "tedbytes". + var v3: UInt64 = 0x7465646279746573 + + /// The current partial word, not yet mixed in with the internal state. + var pendingBytes: UInt64 = 0 + /// The number of bytes that are currently pending in `tailBytes`. Guaranteed to be between 0 and 7. + var pendingByteCount = 0 + /// The number of bytes collected so far, or -1 if the hash value has already been finalized. + var byteCount = 0 + + //MARK: Initializers + + /// Initialize a new instance with the default key, generated randomly the first time this initializer is called. + public init() { + self.init(k0: 0, k1: 1) + } + + /// Initialize a new instance with the specified key. + /// + /// - Parameter k0: The low 64 bits of the secret key. + /// - Parameter k1: The high 64 bits of the secret key. + public init(k0: UInt64, k1: UInt64) { + v0 ^= k0 + v1 ^= k1 + v2 ^= k0 + v3 ^= k1 + } + + private mutating func sipRound() { + v0 = v0 &+ v1 + v1 = rotateLeft(v1, by: 13) + v1 ^= v0 + v0 = rotateLeft(v0, by: 32) + v2 = v2 &+ v3 + v3 = rotateLeft(v3, by: 16) + v3 ^= v2 + v0 = v0 &+ v3 + v3 = rotateLeft(v3, by: 21) + v3 ^= v0 + v2 = v2 &+ v1 + v1 = rotateLeft(v1, by: 17) + v1 ^= v2 + v2 = rotateLeft(v2, by: 32) + } + + mutating func compressWord(_ m: UInt64) { + v3 ^= m + for _ in 0 ..< SipHasher.c { + sipRound() + } + v0 ^= m + } + + mutating func _finalize() -> UInt64 { + precondition(byteCount >= 0) + pendingBytes |= UInt64(byteCount) << 56 + byteCount = -1 + + compressWord(pendingBytes) + + v2 ^= 0xff + for _ in 0 ..< SipHasher.d { + sipRound() + } + + return v0 ^ v1 ^ v2 ^ v3 + } + + //MARK: Appending data + + /// Add all bytes in `buffer` to this hash. + /// + /// - Requires: `finalize()` hasn't been called on this instance yet. + public mutating func append(_ buffer: UnsafeRawBufferPointer) { + precondition(byteCount >= 0) + + // Use the first couple of bytes to complete the pending word. + var i = 0 + if pendingByteCount > 0 { + let readCount = min(buffer.count, 8 - pendingByteCount) + var m: UInt64 = 0 + switch readCount { + case 7: + m |= UInt64(buffer[6]) << 48 + fallthrough + case 6: + m |= UInt64(buffer[5]) << 40 + fallthrough + case 5: + m |= UInt64(buffer[4]) << 32 + fallthrough + case 4: + m |= UInt64(buffer[3]) << 24 + fallthrough + case 3: + m |= UInt64(buffer[2]) << 16 + fallthrough + case 2: + m |= UInt64(buffer[1]) << 8 + fallthrough + case 1: + m |= UInt64(buffer[0]) + default: + precondition(readCount == 0) + } + pendingBytes |= m << UInt64(pendingByteCount << 3) + pendingByteCount += readCount + i += readCount + + if pendingByteCount == 8 { + compressWord(pendingBytes) + pendingBytes = 0 + pendingByteCount = 0 + } + } + + let left = (buffer.count - i) & 7 + let end = (buffer.count - i) - left + while i < end { + var m: UInt64 = 0 + withUnsafeMutableBytes(of: &m) { p in + p.copyMemory(from: .init(rebasing: buffer[i ..< i + 8])) + } + compressWord(UInt64(littleEndian: m)) + i += 8 + } + + switch left { + case 7: + pendingBytes |= UInt64(buffer[i + 6]) << 48 + fallthrough + case 6: + pendingBytes |= UInt64(buffer[i + 5]) << 40 + fallthrough + case 5: + pendingBytes |= UInt64(buffer[i + 4]) << 32 + fallthrough + case 4: + pendingBytes |= UInt64(buffer[i + 3]) << 24 + fallthrough + case 3: + pendingBytes |= UInt64(buffer[i + 2]) << 16 + fallthrough + case 2: + pendingBytes |= UInt64(buffer[i + 1]) << 8 + fallthrough + case 1: + pendingBytes |= UInt64(buffer[i]) + default: + precondition(left == 0) + } + pendingByteCount = left + + byteCount += buffer.count + } + + //MARK: Finalization + + /// Finalize this hash and return the hash value. + /// + /// - Requires: `finalize()` hasn't been called on this instance yet. + public mutating func finalize() -> Int { + return Int(truncatingIfNeeded: _finalize()) + } +} diff --git a/Sources/SwiftDoc/Symbol.swift b/Sources/SwiftDoc/Symbol.swift index c41f5933..c50f3312 100644 --- a/Sources/SwiftDoc/Symbol.swift +++ b/Sources/SwiftDoc/Symbol.swift @@ -1,6 +1,7 @@ import SwiftMarkup import SwiftSyntax import SwiftSemantics +import HTMLEntities public final class Symbol { public typealias ID = Identifier @@ -23,14 +24,10 @@ public final class Symbol { } public var name: String { - return api.name + id.name } - public private(set) lazy var id: ID = { - Identifier(pathComponents: context.compactMap { - ($0 as? Symbol)?.name ?? ($0 as? Extension)?.extendedType - }, name: name) - }() + public private(set) lazy var id: ID = { Identifier(symbol: self) }() public var isPublic: Bool { if api.modifiers.contains(where: { $0.name == "public" || $0.name == "open" }) { diff --git a/Sources/swift-doc/Extensions/Foundation+Extensions.swift b/Sources/swift-doc/Extensions/Foundation+Extensions.swift new file mode 100644 index 00000000..78990b22 --- /dev/null +++ b/Sources/swift-doc/Extensions/Foundation+Extensions.swift @@ -0,0 +1,19 @@ +import Foundation + +extension URLComponents { + mutating func appendPathComponent(_ component: String) { + if let _ = scheme, path.isEmpty { path = "/" } + + var pathComponents = path.split(separator: "/") + pathComponents.append(contentsOf: component.split(separator: "/")) + path = (scheme == nil ? "" : "/") + pathComponents.joined(separator: "/") + } +} + +extension URL { + func path(relativeTo another: URL) -> String { + let pathComponents = self.pathComponents, otherPathComponents = another.pathComponents + guard pathComponents.starts(with: otherPathComponents) else { return path } + return pathComponents.suffix(pathComponents.count - otherPathComponents.count).joined(separator: "/") + } +} diff --git a/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift b/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift index 9222248a..f5fbac48 100644 --- a/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift +++ b/Sources/swift-doc/Extensions/SwiftDoc+Extensions.swift @@ -36,7 +36,7 @@ extension Symbol { return node } - func graph(in module: Module, baseURL: String) -> Graph { + func graph(in module: Module) -> Graph { var graph = Graph(directed: true) let relationships = module.interface.relationships.filter { @@ -47,7 +47,7 @@ extension Symbol { var symbolNode = self.node if !(api is Unknown) { - symbolNode.href = path(for: self, with: baseURL) +// symbolNode.href = path(for: self) } symbolNode.strokeWidth = 3.0 diff --git a/Sources/swift-doc/Subcommands/Coverage.swift b/Sources/swift-doc/Subcommands/Coverage.swift index 4fbf4d7c..35ee764d 100644 --- a/Sources/swift-doc/Subcommands/Coverage.swift +++ b/Sources/swift-doc/Subcommands/Coverage.swift @@ -3,7 +3,7 @@ import Foundation import DCOV import SwiftDoc -extension SwiftDoc { +extension SwiftDocCommand { struct Coverage: ParsableCommand { struct Options: ParsableArguments { @Argument(help: "One or more paths to Swift files") diff --git a/Sources/swift-doc/Subcommands/Diagram.swift b/Sources/swift-doc/Subcommands/Diagram.swift index 1e01280a..70753ebc 100644 --- a/Sources/swift-doc/Subcommands/Diagram.swift +++ b/Sources/swift-doc/Subcommands/Diagram.swift @@ -5,7 +5,7 @@ import SwiftSemantics import GraphViz import DOT -extension SwiftDoc { +extension SwiftDocCommand { struct Diagram: ParsableCommand { struct Options: ParsableArguments { @Argument(help: "One or more paths to Swift files") diff --git a/Sources/swift-doc/Subcommands/Generate.swift b/Sources/swift-doc/Subcommands/Generate.swift index 7109decd..a13345df 100644 --- a/Sources/swift-doc/Subcommands/Generate.swift +++ b/Sources/swift-doc/Subcommands/Generate.swift @@ -10,7 +10,7 @@ import SQLite import FoundationNetworking #endif -extension SwiftDoc { +extension SwiftDocCommand { struct Generate: ParsableCommand { enum Format: String, ExpressibleByArgument { case commonmark @@ -37,9 +37,13 @@ extension SwiftDoc { var format: Format @Option(name: .customLong("base-url"), - default: "/", - help: "The base URL used for all relative URLs in generated documents.") - var baseURL: String + default: URL(fileURLWithPath: "/"), parsing: .next, help: "The base URL used for all relative URLs in generated documents.", transform: { string in + return URL(fileURLWithPath: string) + }) + var baseURL: URL + + @Flag(default: false, inversion: .prefixedNo) + var inlineCSS: Bool } static var configuration = CommandConfiguration(abstract: "Generates Swift documentation") @@ -53,11 +57,11 @@ extension SwiftDoc { switch options.format { case .commonmark: - try CommonMarkGenerator.generate(for: module, with: options) + try CommonMarkGenerator(with: options).generate(for: module) case .html: - try HTMLGenerator.generate(for: module, with: options) + try HTMLGenerator(with: options).generate(for: module) case .docset: - try DocSetGenerator.generate(for: module, with: options) + try DocSetGenerator(with: options).generate(for: module) } } catch { logger.error("\(error)") diff --git a/Sources/swift-doc/Supporting Types/Base.swift b/Sources/swift-doc/Supporting Types/Base.swift new file mode 100644 index 00000000..b87f782c --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Base.swift @@ -0,0 +1,29 @@ +//import Foundation +//import ArgumentParser +// +//enum Base { +// case none +// case rootDirectory +// case currentDirectory +// case externalURL(URL) +//} +// +//extension Base: ExpressibleByArgument { +// init?(argument: String) { +// switch argument { +// case "": +// self = .none +// case "/": +// self = .rootDirectory +// case ".", "./": +// self = .currentDirectory +// default: +// if let url = URL(string: argument) { +// self = .externalURL(url) +// } else { +// +// return nil +// } +// } +// } +//} diff --git a/Sources/swift-doc/Supporting Types/Components/Abstract.swift b/Sources/swift-doc/Supporting Types/Components/Abstract.swift index e09454e8..f4dfe416 100644 --- a/Sources/swift-doc/Supporting Types/Components/Abstract.swift +++ b/Sources/swift-doc/Supporting Types/Components/Abstract.swift @@ -6,11 +6,11 @@ import HypertextLiteral struct Abstract: Component { var symbol: Symbol - let baseURL: String + var router: Router - init(for symbol: Symbol, baseURL: String) { + init(for symbol: Symbol, with router: @escaping Router) { self.symbol = symbol - self.baseURL = baseURL + self.router = router } // MARK: - Component @@ -21,7 +21,7 @@ struct Abstract: Component { List.Item { Fragment { #""" - [\#(symbol.id)](\#(path(for: symbol, with: baseURL))): + [\#(symbol.id)](\#(router(symbol))): \#(summary) """# } @@ -31,7 +31,7 @@ struct Abstract: Component { return Fragment { List.Item { Paragraph { - Link(urlString: path(for: symbol, with: baseURL), text: symbol.id.description) + Link(urlString: router(symbol), text: symbol.id.description) } } } @@ -43,7 +43,7 @@ struct Abstract: Component { return #"""
- + \#(softbreak(symbol.id.description))
diff --git a/Sources/swift-doc/Supporting Types/Components/Declaration.swift b/Sources/swift-doc/Supporting Types/Components/Declaration.swift index 2c1433d1..6232397e 100644 --- a/Sources/swift-doc/Supporting Types/Components/Declaration.swift +++ b/Sources/swift-doc/Supporting Types/Components/Declaration.swift @@ -9,12 +9,10 @@ import Xcode struct Declaration: Component { var symbol: Symbol var module: Module - let baseURL: String - init(of symbol: Symbol, in module: Module, baseURL: String) { + init(of symbol: Symbol, in module: Module) { self.symbol = symbol self.module = module - self.baseURL = baseURL } // MARK: - Component @@ -29,7 +27,7 @@ struct Declaration: Component { var html: HypertextLiteral.HTML { var html = try! SwiftSyntaxHighlighter.highlight(source: symbol.declaration, using: Xcode.self) - html = linkCodeElements(of: html, for: symbol, in: module, with: baseURL) +// 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 ae533dd2..842b8dc4 100644 --- a/Sources/swift-doc/Supporting Types/Components/Documentation.swift +++ b/Sources/swift-doc/Supporting Types/Components/Documentation.swift @@ -10,12 +10,10 @@ import Xcode struct Documentation: Component { var symbol: Symbol var module: Module - let baseURL: String - init(for symbol: Symbol, in module: Module, baseURL: String) { + init(for symbol: Symbol, in module: Module) { self.symbol = symbol self.module = module - self.baseURL = baseURL } // MARK: - Component @@ -39,10 +37,10 @@ struct Documentation: Component { Fragment { "\(documentation.summary!)" } } - Declaration(of: symbol, in: module, baseURL: baseURL) + Declaration(of: symbol, in: module) ForEach(in: documentation.discussionParts) { part in - DiscussionPart(part, for: symbol, in: module, baseURL: baseURL) + DiscussionPart(part, for: symbol, in: module) } if !documentation.parameters.isEmpty { @@ -85,7 +83,7 @@ struct Documentation: Component { var fragments: [HypertextLiteralConvertible] = [] - fragments.append(Declaration(of: symbol, in: module, baseURL: baseURL)) + fragments.append(Declaration(of: symbol, in: module)) if let summary = documentation.summary { fragments.append(#""" @@ -99,7 +97,7 @@ struct Documentation: Component { fragments.append(#"""
\#(documentation.discussionParts.compactMap { part -> HTML? in - DiscussionPart(part, for: symbol, in: module, baseURL: baseURL).html + DiscussionPart(part, for: symbol, in: module).html })
"""# as HypertextLiteral.HTML) @@ -180,13 +178,11 @@ extension Documentation { var symbol: Symbol var module: Module var part: SwiftMarkup.DiscussionPart - let baseURL: String - init(_ part: SwiftMarkup.DiscussionPart, for symbol: Symbol, in module: Module, baseURL: String) { + init(_ part: SwiftMarkup.DiscussionPart, for symbol: Symbol, in module: Module) { self.part = part self.symbol = symbol self.module = module - self.baseURL = baseURL } // MARK: - Component @@ -242,11 +238,11 @@ extension Documentation { let source = codeBlock.literal { var html = try! SwiftSyntaxHighlighter.highlight(source: source, using: Xcode.self) - html = linkCodeElements(of: html, for: symbol, in: module, with: baseURL) +// html = linkCodeElements(of: html, for: symbol, in: module, with: baseURL) return HTML(html) } else { var html = codeBlock.render(format: .html, options: [.unsafe]) - html = linkCodeElements(of: html, for: symbol, in: module, with: baseURL) +// html = linkCodeElements(of: html, for: symbol, in: module, with: baseURL) return HTML(html) } case .heading(let heading): diff --git a/Sources/swift-doc/Supporting Types/Components/Members.swift b/Sources/swift-doc/Supporting Types/Components/Members.swift index d1af9950..67caef7a 100644 --- a/Sources/swift-doc/Supporting Types/Components/Members.swift +++ b/Sources/swift-doc/Supporting Types/Components/Members.swift @@ -7,7 +7,6 @@ import HypertextLiteral struct Members: Component { var symbol: Symbol var module: Module - let baseURL: String var members: [Symbol] @@ -18,10 +17,9 @@ struct Members: Component { var methods: [Symbol] var genericallyConstrainedMembers: [[GenericRequirement] : [Symbol]] - init(of symbol: Symbol, in module: Module, baseURL: String) { + init(of symbol: Symbol, in module: Module) { self.symbol = symbol self.module = module - self.baseURL = baseURL self.members = module.interface.members(of: symbol).filter { $0.extension?.genericRequirements.isEmpty != false } @@ -58,7 +56,7 @@ struct Members: Component { Heading { Code { member.name } } - Documentation(for: member, in: module, baseURL: baseURL) +// Documentation(for: member, in: module) } } } @@ -75,7 +73,7 @@ struct Members: Component { Section { ForEach(in: members) { member in Heading { member.name } - Documentation(for: member, in: module, baseURL: baseURL) +// Documentation(for: member, in: module) } } } @@ -100,7 +98,7 @@ struct Members: Component {

\#(softbreak(member.name))

- \#(Documentation(for: member, in: module, baseURL: baseURL).html) + \#(Documentation(for: member, in: module).html) """# }) @@ -120,7 +118,7 @@ struct Members: Component { \#(members.map { member -> HypertextLiteral.HTML in #"""

\#(softbreak(member.name))

- \#(Documentation(for: member, in: module, baseURL: baseURL).html) + \#(Documentation(for: member, in: module).html) """# }) diff --git a/Sources/swift-doc/Supporting Types/Components/Relationships.swift b/Sources/swift-doc/Supporting Types/Components/Relationships.swift index 96e59a76..d0df0a40 100644 --- a/Sources/swift-doc/Supporting Types/Components/Relationships.swift +++ b/Sources/swift-doc/Supporting Types/Components/Relationships.swift @@ -28,18 +28,16 @@ extension StringBuilder { struct Relationships: Component { var module: Module var symbol: Symbol - let baseURL: String var inheritedTypes: [Symbol] - init(of symbol: Symbol, in module: Module, baseURL: String) { + 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) - self.baseURL = baseURL } var graphHTML: HypertextLiteral.HTML? { - var graph = symbol.graph(in: module, baseURL: baseURL) + var graph = symbol.graph(in: module) guard !graph.edges.isEmpty else { return nil } graph.aspectRatio = 0.125 @@ -82,7 +80,7 @@ struct Relationships: Component { if type.api is Unknown { return "`\(type.id)`" } else { - return "[`\(type.id)`](\(path(for: type, with: baseURL)))" + return "[`\(type.id)`](\(""/* TODO: path(for: type, with: baseURL))*/)" } }.joined(separator: ", ")) """# @@ -120,7 +118,7 @@ struct Relationships: Component { """# } else { return #""" -
\#(symbol.id)
+
\#(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 afd8cff8..3e585b33 100644 --- a/Sources/swift-doc/Supporting Types/Components/Requirements.swift +++ b/Sources/swift-doc/Supporting Types/Components/Requirements.swift @@ -7,12 +7,10 @@ import HypertextLiteral struct Requirements: Component { var symbol: Symbol var module: Module - let baseURL: String - init(of symbol: Symbol, in module: Module, baseURL: String) { + init(of symbol: Symbol, in module: Module) { self.symbol = symbol self.module = module - self.baseURL = baseURL } var sections: [(title: String, requirements: [Symbol])] { @@ -33,7 +31,7 @@ struct Requirements: Component { Heading { section.title } ForEach(in: section.requirements) { requirement in Heading { requirement.name } - Documentation(for: requirement, in: module, baseURL: baseURL) +// Documentation(for: requirement, in: module) } } } @@ -55,7 +53,7 @@ struct Requirements: Component {

\#(softbreak(member.name))

- \#(Documentation(for: member, in: module, baseURL: baseURL).html) + \#(Documentation(for: member, in: module).html) """# }) diff --git a/Sources/swift-doc/Supporting Types/Generator.swift b/Sources/swift-doc/Supporting Types/Generator.swift index 3454de9d..88f2f845 100644 --- a/Sources/swift-doc/Supporting Types/Generator.swift +++ b/Sources/swift-doc/Supporting Types/Generator.swift @@ -1,5 +1,6 @@ import SwiftDoc protocol Generator { - static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws + var options: SwiftDocCommand.Generate.Options { get } + func generate(for module: Module) throws } diff --git a/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift index cc4997b1..560cd9d7 100644 --- a/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/CommonMarkGenerator.swift @@ -1,10 +1,33 @@ import Foundation +import CommonMark import SwiftDoc import SwiftSemantics import struct SwiftSemantics.Protocol -enum CommonMarkGenerator: Generator { - static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { +final class CommonMarkGenerator: Generator { + var router: Router + var options: SwiftDocCommand.Generate.Options + + init(with options: SwiftDocCommand.Generate.Options) { + self.options = options + self.router = { routable in + switch routable { + case let symbol as Symbol: + var urlComponents = URLComponents() + if symbol.id.pathComponents.isEmpty { + urlComponents.appendPathComponent(symbol.id.escaped) + } else { + symbol.id.pathComponents.forEach { urlComponents.appendPathComponent($0) } + urlComponents.fragment = symbol.id.escaped.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! + } + return urlComponents.path + default: + return "\(routable)" + } + } + } + + func generate(for module: Module) throws { assert(options.format == .commonmark) let module = try Module(name: options.moduleName, paths: options.inputs) @@ -12,15 +35,15 @@ enum CommonMarkGenerator: Generator { let outputDirectoryURL = URL(fileURLWithPath: options.output) try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) - var pages: [String: Page] = [:] + var pages: [String: Page & CommonMarkRenderable] = [:] var globals: [String: [Symbol]] = [:] for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { switch symbol.api { case is Class, is Enumeration, is Structure, is Protocol: - pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: options.baseURL) - case let `typealias` as Typealias: - pages[route(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol, baseURL: options.baseURL) + pages[router(symbol)] = TypePage(for: symbol, in: module) + case is Typealias: + pages[router(symbol)] = TypealiasPage(for: symbol, in: module) case let function as Function where !function.isOperator: globals[function.name, default: []] += [symbol] case let variable as Variable: @@ -31,7 +54,7 @@ enum CommonMarkGenerator: Generator { } for (name, symbols) in globals { - pages[route(for: name)] = GlobalPage(module: module, name: name, symbols: symbols, baseURL: options.baseURL) + pages[router(symbols.first!)] = GlobalPage(for: symbols, named: name, in: module) } guard !pages.isEmpty else { @@ -40,28 +63,28 @@ enum CommonMarkGenerator: Generator { } if pages.count == 1, let page = pages.first?.value { - let filename = "Home.md" - let url = outputDirectoryURL.appendingPathComponent(filename) - try page.write(to: url, baseURL: options.baseURL) + pages = ["Home": page] } else { - pages["Home"] = HomePage(module: module, baseURL: options.baseURL) - pages["_Sidebar"] = SidebarPage(module: module, baseURL: options.baseURL) - pages["_Footer"] = FooterPage(baseURL: options.baseURL) + pages["Home"] = HomePage(module: module) + pages["_Sidebar"] = SidebarPage(module: module) + pages["_Footer"] = FooterPage() + } - try pages.map { $0 }.parallelForEach { - let filename = "\($0.key).md" - let url = outputDirectoryURL.appendingPathComponent(filename) - try $0.value.write(to: url, baseURL: options.baseURL) - } + try pages.map { $0 }.parallelForEach { + try write(page: $0.value, to: $0.key) } } + + private func write(page: Page & CommonMarkRenderable, to route: String) throws { + guard let data = try page.render(with: self).render(format: .commonmark).data(using: .utf8) else { fatalError("Unable to render page \(page)") } + let filename = "\(route).md" + let url = URL(fileURLWithPath: options.output).appendingPathComponent(filename) + try data.write(to: url) + } } // MARK: - -fileprivate extension Page { - func write(to url: URL, baseURL: String) throws { - guard let data = document.render(format: .commonmark).data(using: .utf8) else { return } - try writeFile(data, to: url) - } +protocol CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document } diff --git a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift index bb2546b1..f3ad6f59 100644 --- a/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/DocSetGenerator.swift @@ -8,10 +8,17 @@ import SQLite fileprivate typealias XML = HTML -enum DocSetGenerator: Generator { - static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { +final class DocSetGenerator: Generator { + var options: SwiftDocCommand.Generate.Options + + init(with options: SwiftDocCommand.Generate.Options) { + self.options = options + } + + func generate(for module: Module) throws { assert(options.format == .docset) + let outputDirectoryURL = URL(fileURLWithPath: options.output) try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) @@ -20,10 +27,31 @@ enum DocSetGenerator: Generator { let docsetDocumentsDirectoryURL = docsetURL.appendingPathComponent("Contents/Resources/Documents/") try fileManager.createDirectory(at: docsetDocumentsDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) - var options = options + var options = self.options options.format = .html options.output = docsetDocumentsDirectoryURL.path - try HTMLGenerator.generate(for: module, with: options) +// options.inlineCSS = true + + + let generator = HTMLGenerator(with: options) + + let apple_refRouter: Router = { symbol in + "//apple_ref/swift/\(symbol.entryType)/\(symbol.id.checksum)" + } + + generator.router = apple_refRouter + let pages = try generator.pages(for: module) +// try generator.generate(for: module) + +// for page in pages.values { +// guard let symbol = page.symbol, +// let page = page as? HTMLRenderable +// else { continue } +// +// try page.render(with: generator).description +// +// } + let info: [String: Any] = [ "CFBundleIdentifier": module.name.lowercased(), @@ -59,12 +87,20 @@ enum DocSetGenerator: Generator { }) } + + + +// FlatRouter(suffix: "/index.html") + +// router + try db.transaction { for symbol in module.interface.symbols { + print(apple_refRouter(symbol)) try db.run(searchIndex.insert(or: .ignore, name <- symbol.id.description, type <- symbol.entryType, - path <- symbol.route(with: options.baseURL) + path <- apple_refRouter(symbol) )) } } @@ -81,7 +117,6 @@ enum DocSetGenerator: Generator { } } - fileprivate extension Symbol { // https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/HeaderDoc/anchors/anchors.html#//apple_ref/doc/uid/TP40001215-CH347 var symbolType: String { @@ -174,12 +209,4 @@ fileprivate extension Symbol { """# } - - func route(with baseURL: String) -> String { - if let parent = context.compactMap({ $0 as? Symbol }).last { - return path(for: parent, with: baseURL) + "/index.html" + "#\(id.description.lowercased().replacingOccurrences(of: " ", with: "-"))" - } else { - return path(for: self, with: baseURL) + "/index.html" - } - } } diff --git a/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift b/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift index d857ba9a..b94d7467 100644 --- a/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift +++ b/Sources/swift-doc/Supporting Types/Generators/HTMLGenerator.swift @@ -2,23 +2,41 @@ import Foundation import SwiftDoc import SwiftSemantics import struct SwiftSemantics.Protocol +import HypertextLiteral -enum HTMLGenerator: Generator { - static func generate(for module: Module, with options: SwiftDoc.Generate.Options) throws { - assert(options.format == .html) +final class HTMLGenerator: Generator { + var router: Router + var options: SwiftDocCommand.Generate.Options - let outputDirectoryURL = URL(fileURLWithPath: options.output) - try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) + init(with options: SwiftDocCommand.Generate.Options) { + self.options = options + self.router = { routable in + switch routable { + case let symbol as Symbol: + var urlComponents = URLComponents() + if symbol.id.pathComponents.isEmpty { + urlComponents.appendPathComponent(symbol.id.escaped) + } else { + symbol.id.pathComponents.forEach { urlComponents.appendPathComponent($0) } + urlComponents.fragment = symbol.id.escaped.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! + } + return urlComponents.path + default: + return "\(routable)" + } + } + } + func pages(for module: Module) throws -> [String: Page] { var pages: [String: Page] = [:] var globals: [String: [Symbol]] = [:] for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { switch symbol.api { case is Class, is Enumeration, is Structure, is Protocol: - pages[route(for: symbol)] = TypePage(module: module, symbol: symbol, baseURL: options.baseURL) - case let `typealias` as Typealias: - pages[route(for: `typealias`.name)] = TypealiasPage(module: module, symbol: symbol, baseURL: options.baseURL) + pages[router(symbol)] = TypePage(for: symbol, in: module) + case is Typealias: + pages[router(symbol)] = TypealiasPage(for: symbol, in: module) case let function as Function where !function.isOperator: globals[function.name, default: []] += [symbol] case let variable as Variable: @@ -29,24 +47,48 @@ enum HTMLGenerator: Generator { } for (name, symbols) in globals { - pages[route(for: name)] = GlobalPage(module: module, name: name, symbols: symbols, baseURL: options.baseURL) + pages[router(symbols.first!)] = GlobalPage(for: symbols, named: name, in: module) } + return pages + } + + func generate(for module: Module) throws { + assert(options.format == .html) + + let outputDirectoryURL = URL(fileURLWithPath: options.output) + try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: fileAttributes) + + var pages = try self.pages(for: module) + guard !pages.isEmpty else { logger.warning("No public API symbols were found at the specified path. No output was written.") return } - let cssData = try fetchRemoteCSS() - let cssURL = outputDirectoryURL.appendingPathComponent("all.css") - try writeFile(cssData, to: cssURL) + if !options.inlineCSS { + let cssURL = outputDirectoryURL.appendingPathComponent("all.css") + try writeFile(css, to: cssURL) + } + + if pages.count == 1, let page = pages.first?.value { + pages = ["Home": page] + } else { + pages["Home"] = HomePage(module: module) + pages["_Sidebar"] = SidebarPage(module: module) + pages["_Footer"] = FooterPage() + } + + try pages.map { $0 }.parallelForEach { +// try write(page: $0.value, to: $0.key) + } if pages.count == 1, let page = pages.first?.value { let filename = "index.html" let url = outputDirectoryURL.appendingPathComponent(filename) - try page.write(to: url, baseURL: options.baseURL) + try page.write(to: url, with: options) } else { - pages["Home"] = HomePage(module: module, baseURL: options.baseURL) + pages["Home"] = HomePage(module: module) try pages.map { $0 }.parallelForEach { let filename: String @@ -57,18 +99,52 @@ enum HTMLGenerator: Generator { } let url = outputDirectoryURL.appendingPathComponent(filename) - try $0.value.write(to: url, baseURL: options.baseURL) + try $0.value.write(to: url, with: options) } } } + + func write(page: Page & HTMLRenderable, to url: URL) throws { + + } } // MARK: - fileprivate extension Page { - func write(to url: URL, baseURL: String) throws { - let data = layout(self).description.data(using: .utf8) - guard let filedata = data else { return } - try writeFile(filedata, to: url) + func write(to url: URL, with options: SwiftDocCommand.Generate.Options) throws { +// var html = layout(self, with: options).description +// if let range = html.range(of: "") { +// html.insert(contentsOf: """ +// +// +// """, at: range.lowerBound) +// } +// +// let data = html.data(using: .utf8) +// guard let filedata = data else { return } +// try writeFile(filedata, to: url) + } +} + +fileprivate class Stylesheet { + static var css: Data! = { + let url = URL(string: "https://raw.githubusercontent.com/SwiftDocOrg/swift-doc/master/Resources/all.min.css")! + return try! Data(contentsOf: url) + }() +} + +fileprivate var _css: Data? +fileprivate var css: Data { + if let css = _css { + return css + } else { + let url = URL(string: "https://raw.githubusercontent.com/SwiftDocOrg/swift-doc/master/Resources/all.min.css")! + _css = try! Data(contentsOf: url) + return _css! } } + +protocol HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HypertextLiteral.HTML +} diff --git a/Sources/swift-doc/Supporting Types/Helpers.swift b/Sources/swift-doc/Supporting Types/Helpers.swift index a3521007..14dc0328 100644 --- a/Sources/swift-doc/Supporting Types/Helpers.swift +++ b/Sources/swift-doc/Supporting Types/Helpers.swift @@ -3,21 +3,22 @@ import SwiftDoc import HTML public func linkCodeElements(of html: String, for symbol: Symbol, in module: Module, with baseURL: String) -> 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, with: baseURL) - element.wrap(inside: a) - } - } - - return document.root?.description ?? html + return html +// 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, with: baseURL) +// element.wrap(inside: a) +// } +// } +// +// return document.root?.description ?? html } public func sidebar(for html: String) -> String { @@ -78,8 +79,3 @@ public func softbreak(_ string: String) -> String { return regex.stringByReplacingMatches(in: string, options: [], range: NSRange(string.startIndex.. Data { - let url = URL(string: "https://raw.githubusercontent.com/SwiftDocOrg/swift-doc/master/Resources/all.min.css")! - return try Data(contentsOf: url) -} diff --git a/Sources/swift-doc/Supporting Types/Layout.swift b/Sources/swift-doc/Supporting Types/Layout.swift index 16429c4f..921edf7b 100644 --- a/Sources/swift-doc/Supporting Types/Layout.swift +++ b/Sources/swift-doc/Supporting Types/Layout.swift @@ -2,8 +2,8 @@ import SwiftDoc import HypertextLiteral import Foundation -func layout(_ page: Page) -> HTML { - let html = page.html +func layout(_ page: Page & HTMLRenderable, with generator: HTMLGenerator) throws -> HTML { + let html = try page.render(with: generator) return #""" @@ -11,14 +11,16 @@ func layout(_ page: Page) -> HTML { - \#(page.module.name) - \#(page.title) - + \#(generator.options.moduleName) - \#(page.title) + \#(generator.options.inlineCSS ? "" : + #""# + )
- + - \#(page.module.name) + \#(generator.options.moduleName) Documentation @@ -45,7 +47,7 @@ func layout(_ page: Page) -> HTML {
- \#(FooterPage(baseURL: page.baseURL).html) + \#(FooterPage().render(with: generator))
diff --git a/Sources/swift-doc/Supporting Types/Page.swift b/Sources/swift-doc/Supporting Types/Page.swift index 248b971a..198bdf9a 100644 --- a/Sources/swift-doc/Supporting Types/Page.swift +++ b/Sources/swift-doc/Supporting Types/Page.swift @@ -6,17 +6,12 @@ import struct SwiftSemantics.Protocol import CommonMark import HypertextLiteral -protocol Page: HypertextLiteralConvertible { - var module: Module { get } - var baseURL: String { get } +protocol Page { +// var module: Module { get } +// var generator: Generator { 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") } +// var document: CommonMark.Document { get } +// var html: HypertextLiteral.HTML { get } } func writeFile(_ data: Data, to url: URL) throws { @@ -26,3 +21,8 @@ func writeFile(_ data: Data, to url: URL) throws { try data.write(to: url) try fileManager.setAttributes([.posixPermissions: 0o744], ofItemAtPath: url.path) } + + +extension Page { + var symbol: Symbol? { nil } +} diff --git a/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift b/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift index 88ff0628..02a77a30 100644 --- a/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/FooterPage.swift @@ -18,31 +18,29 @@ fileprivate let timestampDateFormatter: DateFormatter = { fileprivate let href = "https://github.com/SwiftDocOrg/swift-doc" struct FooterPage: Page { - let baseURL: String - - init(baseURL: String) { - self.baseURL = baseURL - } - - // MARK: - Page + let title: String = "Footer" +} - var document: CommonMark.Document { +extension FooterPage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { let timestamp = timestampDateFormatter.string(from: Date()) return Document { Fragment { - "Generated at \(timestamp) using [swift-doc](\(href)) \(SwiftDoc.configuration.version)." + "Generated at \(timestamp) using [swift-doc](\(href)) \(SwiftDocCommand.configuration.version)." } } } +} - var html: HypertextLiteral.HTML { +extension FooterPage { + func render(with generator: HTMLGenerator) -> HypertextLiteral.HTML { let timestamp = timestampDateFormatter.string(from: Date()) let dateString = dateFormatter.string(from: Date()) return #"""

- Generated on using swift-doc \#(SwiftDoc.configuration.version). + Generated on using swift-doc \#(SwiftDocCommand.configuration.version).

"""# } diff --git a/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift b/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift index cd8dad0f..c0006025 100644 --- a/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/GlobalPage.swift @@ -7,13 +7,11 @@ struct GlobalPage: Page { let module: Module let name: String let symbols: [Symbol] - let baseURL: String - init(module: Module, name: String, symbols: [Symbol], baseURL: String) { - self.module = module + init(for symbols: [Symbol], named name: String, in module: Module) { self.name = name self.symbols = symbols - self.baseURL = baseURL + self.module = module } // MARK: - Page @@ -21,35 +19,40 @@ struct GlobalPage: Page { var title: String { return name } - - var document: CommonMark.Document { - return Document { +} + +extension GlobalPage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { + Document { ForEach(in: symbols) { symbol in Heading { symbol.id.description } - Documentation(for: symbol, in: module, baseURL: baseURL) +// 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, baseURL: baseURL).html - }) - """# +extension GlobalPage: HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HTML { + return "" +// 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 425c8d89..e3d7e8eb 100644 --- a/Sources/swift-doc/Supporting Types/Pages/HomePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/HomePage.swift @@ -5,7 +5,6 @@ import HypertextLiteral struct HomePage: Page { var module: Module - let baseURL: String var classes: [Symbol] = [] var enumerations: [Symbol] = [] @@ -16,9 +15,8 @@ struct HomePage: Page { var globalFunctions: [Symbol] = [] var globalVariables: [Symbol] = [] - init(module: Module, baseURL: String) { + init(module: Module) { self.module = module - self.baseURL = baseURL for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { switch symbol.api { @@ -51,9 +49,12 @@ struct HomePage: Page { var title: String { return module.name } +} + - var document: CommonMark.Document { - return Document { +extension HomePage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { + Document { ForEach(in: [ ("Types", classes + enumerations + structures), ("Protocols", protocols), @@ -66,35 +67,39 @@ struct HomePage: Page { Heading { heading } List(of: symbols.sorted()) { symbol in - Abstract(for: symbol, baseURL: baseURL).fragment + Abstract(for: symbol, with: generator.router).fragment } } } } } +} - var html: HypertextLiteral.HTML { - return #""" - \#([ - ("Classes", classes), - ("Structures", structures), - ("Enumerations", enumerations), - ("Protocols", protocols), - ("Typealiases", globalTypealiases), - ("Functions", globalFunctions), - ("Variables", globalVariables) - ].compactMap { (heading, symbols) -> HypertextLiteral.HTML? in - guard !symbols.isEmpty else { return nil } - return #""" -
-

\#(heading)

-
- \#(symbols.sorted().map { Abstract(for: $0, baseURL: baseURL).html }) -
-
- """# - }) - """# +extension HomePage: HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HTML { + return "" +// #""" +// \#([ +// ("Classes", classes), +// ("Structures", structures), +// ("Enumerations", enumerations), +// ("Protocols", protocols), +// ("Typealiases", globalTypealiases), +// ("Functions", globalFunctions), +// ("Variables", globalVariables) +// ].compactMap { (heading, symbols) -> HypertextLiteral.HTML? in +// guard !symbols.isEmpty else { return nil } +// +// return #""" +//
+//

\#(heading)

+//
+// \#(symbols.sorted().map { Abstract(for: $0).html }) +//
+//
+// """# +// }) +// """# } } diff --git a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift index 7b71908b..d3b70fcc 100644 --- a/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/SidebarPage.swift @@ -4,9 +4,6 @@ import CommonMarkBuilder import HypertextLiteral struct SidebarPage: Page { - var module: Module - let baseURL: String - var typeNames: Set = [] var protocolNames: Set = [] var operatorNames: Set = [] @@ -14,10 +11,9 @@ struct SidebarPage: Page { var globalFunctionNames: Set = [] var globalVariableNames: Set = [] - init(module: Module, baseURL: String) { - self.module = module - self.baseURL = baseURL + var title: String { "Sidebar" } + init(module: Module) { for symbol in module.interface.topLevelSymbols.filter({ $0.isPublic }) { switch symbol.api { case is Class: @@ -43,11 +39,11 @@ struct SidebarPage: Page { } } } +} - // MARK: - Page - - var document: CommonMark.Document { - return Document { +extension SidebarPage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { + Document { ForEach(in: ( [ ("Types", typeNames), @@ -66,18 +62,23 @@ struct SidebarPage: Page { """# } - List(of: section.names.sorted()) { name in - Link(urlString: path(for: name, with: baseURL), text: name) - } +// List(of: section.names.sorted()) { name in +// Link(urlString: generator.route(for: name), text: name) +// } Fragment { "" } } } } +} + +extension SidebarPage: HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HTML { + var options = generator.options + options.format = .commonmark - var html: HypertextLiteral.HTML { - #""" - \#(document) + return #""" + \#(try render(with: CommonMarkGenerator(with: options))) """# } } diff --git a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift index 4b7cc2df..29c3d54f 100644 --- a/Sources/swift-doc/Supporting Types/Pages/TypePage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/TypePage.swift @@ -6,13 +6,11 @@ import HypertextLiteral struct TypePage: Page { let module: Module let symbol: Symbol - let baseURL: String - init(module: Module, symbol: Symbol, baseURL: String) { + init(for symbol: Symbol, in module: Module) { precondition(symbol.api is Type) - self.module = module self.symbol = symbol - self.baseURL = baseURL + self.module = module } // MARK: - Page @@ -20,19 +18,23 @@ struct TypePage: Page { var title: String { return symbol.id.description } +} - var document: CommonMark.Document { +extension TypePage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { return CommonMark.Document { Heading { symbol.id.description } - Documentation(for: symbol, in: module, baseURL: baseURL) - Relationships(of: symbol, in: module, baseURL: baseURL) - Members(of: symbol, in: module, baseURL: baseURL) - Requirements(of: symbol, in: module, baseURL: baseURL) + Documentation(for: symbol, in: module) +// Relationships(of: symbol, in: module +// Members(of: symbol, in: module) +// Requirements(of: symbol, in: module) } } +} - var html: HypertextLiteral.HTML { +extension TypePage: HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HTML { let typeName = String(describing: type(of: symbol.api)) return #""" @@ -42,10 +44,12 @@ struct TypePage: Page { \#(softbreak(symbol.id.description)) - \#(Documentation(for: symbol, in: module, baseURL: baseURL).html) - \#(Relationships(of: symbol, in: module, baseURL: baseURL).html) - \#(Members(of: symbol, in: module, baseURL: baseURL).html) - \#(Requirements(of: symbol, in: module, baseURL: baseURL).html) """# +// +// \#(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 4c806727..ccd202cd 100644 --- a/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift +++ b/Sources/swift-doc/Supporting Types/Pages/TypealiasPage.swift @@ -6,13 +6,11 @@ import HypertextLiteral struct TypealiasPage: Page { let module: Module let symbol: Symbol - let baseURL: String - init(module: Module, symbol: Symbol, baseURL: String) { + init(for symbol: Symbol, in module: Module) { precondition(symbol.api is Typealias) self.module = module self.symbol = symbol - self.baseURL = baseURL } // MARK: - Page @@ -20,22 +18,27 @@ struct TypealiasPage: Page { var title: String { return symbol.id.description } +} - var document: CommonMark.Document { +extension TypealiasPage: CommonMarkRenderable { + func render(with generator: CommonMarkGenerator) throws -> Document { Document { Heading { symbol.id.description } - Documentation(for: symbol, in: module, baseURL: baseURL) +// Documentation(for: symbol, in: module) } } +} - var html: HypertextLiteral.HTML { +extension TypealiasPage: HTMLRenderable { + func render(with generator: HTMLGenerator) throws -> HTML { #"""

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

- - \#(Documentation(for: symbol, in: module, baseURL: baseURL).html) """# + +// \#(Documentation(for: symbol, in: module).html) +// """# } } diff --git a/Sources/swift-doc/Supporting Types/Router.swift b/Sources/swift-doc/Supporting Types/Router.swift new file mode 100644 index 00000000..b70b5c82 --- /dev/null +++ b/Sources/swift-doc/Supporting Types/Router.swift @@ -0,0 +1,96 @@ +import SwiftDoc +import Foundation +import SwiftSemantics +import struct SwiftSemantics.Protocol + + +typealias Router = (Symbol) -> String +// +//protocol Router { +// func callAsFunction(_ symbol: Symbol) -> String +// func callAsFunction(_ string: String) -> String +//} +// +//struct FlatRouter: Router { +// var baseURL: URL? +// var suffix: String? +// +// init(baseURL: URL? = nil, suffix: String? = nil) { +// self.baseURL = baseURL +// self.suffix = suffix +// } +// +// func callAsFunction(_ string: String) -> String { +// let suffix = self.suffix ?? "" +// if var url = baseURL { +// url.appendPathComponent(string) +// return (url.isFileURL ? url.relativePath : url.absoluteString) + suffix +// } else { +// return string + suffix +// } +// } +// +// func callAsFunction(_ symbol: Symbol) -> String { +// if symbol.id.pathComponents.isEmpty { +// return callAsFunction(symbol.id.description) +// } else { +// return callAsFunction(symbol.id.pathComponents.joined(separator: "_") + "#\(symbol.name)") +// } +// } +//} +// +//struct DocSetRouter: Router { +// func callAsFunction(_ symbol: Symbol) -> String { +// let identifier = symbol.id.description.replacingOccurrences(of: ".", with: "_") +// return "//apple_ref/swift/\(symbol.entryType)/\(identifier.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)!)" +// } +// +// func callAsFunction(_ string: String) -> String { +// return "" +// } +//} + +// +//fileprivate extension Symbol { +// // https://kapeli.com/docsets#supportedentrytypes +// var entryType: String { +// let parent = context.compactMap { $0 as? Symbol }.last?.api +// +// switch api { +// case is Class: +// return "Class" +// case is Initializer: +// return "Method" +// case is Enumeration: +// return "Enum" +// case is Enumeration.Case: +// return "Value" +// case let type as Type where type.inheritance.contains(where: { $0.hasSuffix("Error") }): +// return "Error" +// case is Function where parent is Type: +// return "Method" +// case is Function: +// return "Function" +// case is Variable where parent is Type: +// return "Property" +// case let variable as Variable where variable.keyword == "let": +// return "Constant" +// case is Variable: +// return "Variable" +// case is Operator: +// return "Operator" +// case is PrecedenceGroup: +// return "Procedure" // FIXME: no direct matching entry type +// case is Protocol: +// return "Protocol" +// case is Structure: +// return "Struct" +// case is Subscript: +// return "Method" +// case is Type, is AssociatedType: +// return "Type" +// default: +// return "Entry" +// } +// } +//} diff --git a/Sources/swift-doc/main.swift b/Sources/swift-doc/main.swift index b9ac8fb7..5dd94748 100644 --- a/Sources/swift-doc/main.swift +++ b/Sources/swift-doc/main.swift @@ -19,7 +19,7 @@ let fileAttributes: [FileAttributeKey : Any] = [.posixPermissions: 0o744] var standardOutput = FileHandle.standardOutput var standardError = FileHandle.standardError -struct SwiftDoc: ParsableCommand { +struct SwiftDocCommand: ParsableCommand { static var configuration = CommandConfiguration( commandName: "swift doc", abstract: "A utility for generating documentation for Swift code.", @@ -28,4 +28,4 @@ struct SwiftDoc: ParsableCommand { ) } -SwiftDoc.main() +SwiftDocCommand.main() diff --git a/Tests/SwiftDocTests/Helpers/temporaryFile.swift b/Tests/SwiftDocTests/Extensions/SourceFile+Extensions.swift similarity index 61% rename from Tests/SwiftDocTests/Helpers/temporaryFile.swift rename to Tests/SwiftDocTests/Extensions/SourceFile+Extensions.swift index 453910bb..f36153e7 100644 --- a/Tests/SwiftDocTests/Helpers/temporaryFile.swift +++ b/Tests/SwiftDocTests/Extensions/SourceFile+Extensions.swift @@ -1,6 +1,7 @@ import Foundation +import SwiftDoc -func temporaryFile(path: String? = nil, contents: String) throws -> URL { +fileprivate func temporaryFile(path: String? = nil, contents: String) throws -> URL { let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o766]) @@ -11,3 +12,10 @@ func temporaryFile(path: String? = nil, contents: String) throws -> URL { return temporaryFileURL } + +extension SourceFile: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + let url = try! temporaryFile(contents: value) + try! self.init(file: url, relativeTo: url.deletingLastPathComponent()) + } +} diff --git a/Tests/SwiftDocTests/InterfaceTypeTests.swift b/Tests/SwiftDocTests/InterfaceTypeTests.swift index 54c26cce..6f8dbc07 100644 --- a/Tests/SwiftDocTests/InterfaceTypeTests.swift +++ b/Tests/SwiftDocTests/InterfaceTypeTests.swift @@ -8,7 +8,7 @@ import SwiftSyntax final class InterfaceTypeTests: XCTestCase { func testPrivateInheritance() throws { - let source = #""" + let sourceFile: SourceFile = #""" public class A { } class B : A { } @@ -16,8 +16,6 @@ final class InterfaceTypeTests: XCTestCase { public class C : A { } """# - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) let module = Module(name: "Module", sourceFiles: [sourceFile]) // `class A` diff --git a/Tests/SwiftDocTests/NestedTypesTests.swift b/Tests/SwiftDocTests/NestedTypesTests.swift index ab2f18c8..65a28484 100644 --- a/Tests/SwiftDocTests/NestedTypesTests.swift +++ b/Tests/SwiftDocTests/NestedTypesTests.swift @@ -7,7 +7,7 @@ import SwiftSyntax final class NestedTypesTests: XCTestCase { func testNestedTypes() throws { - let source = #""" + let sourceFile: SourceFile = #""" public class C { } extension C { @@ -21,8 +21,6 @@ final class NestedTypesTests: XCTestCase { } """# - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) let module = Module(name: "Module", sourceFiles: [sourceFile]) XCTAssertEqual(sourceFile.symbols.count, 4) @@ -66,18 +64,16 @@ final class NestedTypesTests: XCTestCase { ) } - #if false // Disabling tests for `swift-doc` code, executable targers are not testable. + #if false // Disabling tests for `swift-doc` code, executable targets are not testable. func testRelationshipsSectionWithNestedTypes() throws { - let source = #""" + let sourceFile: SourceFile = #""" public class C { public enum E { } } """# - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) let module = Module(name: "Module", sourceFiles: [sourceFile]) // `class C` @@ -96,7 +92,7 @@ final class NestedTypesTests: XCTestCase { } func testNoRelationshipsSection() throws { - let source = #""" + let sourceFile: SourceFile = #""" public class C { } @@ -104,8 +100,6 @@ final class NestedTypesTests: XCTestCase { } """# - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) let module = Module(name: "Module", sourceFiles: [sourceFile]) // `class C` diff --git a/Tests/SwiftDocTests/OperatorIdentifierTests.swift b/Tests/SwiftDocTests/OperatorIdentifierTests.swift new file mode 100644 index 00000000..432e3ed8 --- /dev/null +++ b/Tests/SwiftDocTests/OperatorIdentifierTests.swift @@ -0,0 +1,140 @@ +import XCTest +import SwiftSemantics +import SwiftDoc + +final class OperatorIdentifiersTests: XCTestCase { + func testComparisonOperators() throws { + let sourceFile: SourceFile = #""" + func ==(lhs: (), rhs: ()) -> Bool { return true } + func !=(lhs: (), rhs: ()) -> Bool { return false } + func ===(lhs: (), rhs: ()) -> Bool { return true } + func !==(lhs: (), rhs: ()) -> Bool { return false } + func <(lhs: (), rhs: ()) -> Bool { return true } + func <=(lhs: (), rhs: ()) -> Bool { return true } + func >=(lhs: (), rhs: ()) -> Bool { return true } + func >(lhs: (), rhs: ()) -> Bool { return true } + func ~=(lhs: (), rhs: ()) -> Bool { return true } + """# + + let operators = sourceFile.symbols.filter { ($0.api as? Function)?.isOperator == true } + XCTAssertEqual(operators.count, 9) + + do { + let symbol = operators[0] + XCTAssertEqual(symbol.id.name, "infix ==") + XCTAssertEqual(symbol.id.escaped, "infix-equals-equals") + } + + do { + let symbol = operators[1] + XCTAssertEqual(symbol.id.name, "infix !=") + XCTAssertEqual(symbol.id.escaped, "infix-bang-equals") + } + + do { + let symbol = operators[2] + XCTAssertEqual(symbol.id.name, "infix ===") + XCTAssertEqual(symbol.id.escaped, "infix-equals-equals-equals") + } + + do { + let symbol = operators[3] + XCTAssertEqual(symbol.id.name, "infix !==") + XCTAssertEqual(symbol.id.escaped, "infix-bang-equals-equals") + } + + do { + let symbol = operators[4] + XCTAssertEqual(symbol.id.name, "infix <") + XCTAssertEqual(symbol.id.escaped, "infix-lt") + } + + do { + let symbol = operators[5] + XCTAssertEqual(symbol.id.name, "infix <=") + XCTAssertEqual(symbol.id.escaped, "infix-lt-equals") + } + + do { + let symbol = operators[6] + XCTAssertEqual(symbol.id.name, "infix >=") + XCTAssertEqual(symbol.id.escaped, "infix-gt-equals") + } + + do { + let symbol = operators[7] + XCTAssertEqual(symbol.id.name, "infix >") + XCTAssertEqual(symbol.id.escaped, "infix-gt") + } + + do { + let symbol = operators[8] + XCTAssertEqual(symbol.id.name, "infix ~=") + XCTAssertEqual(symbol.id.escaped, "infix-tilde-equals") + } + } + + func testPrefixOperators() throws { + let sourceFile: SourceFile = #""" + prefix func + (number: Number) -> Number { + return number >= 0 ? number : number.negated + } + + prefix func - (number: Number) -> Number { + return number.negated + } + """# + + let operators = sourceFile.symbols.filter { ($0.api as? Function)?.isOperator == true } + XCTAssertEqual(operators.count, 2) + + + do { + let symbol = operators[0] + XCTAssertEqual(symbol.id.name, "prefix +") + XCTAssertEqual(symbol.id.escaped, "prefix-plus") + } + + do { + let symbol = operators[1] + XCTAssertEqual(symbol.id.name, "prefix -") + XCTAssertEqual(symbol.id.escaped, "prefix-minus") + } + } + + func testInfixOperators() throws { + let sourceFile: SourceFile = #""" + infix func ?? (lhs: T?, rhs: T?) -> T? { + if let lhs = lhs { return lhs } else { return rhs } + } + """# + + let operators = sourceFile.symbols.filter { ($0.api as? Function)?.isOperator == true } + XCTAssertEqual(operators.count, 1) + + + do { + let symbol = operators[0] + XCTAssertEqual(symbol.id.name, "infix ??") + XCTAssertEqual(symbol.id.escaped, "infix-quest-quest") + } + } + + func testSuffixOperators() throws { + let sourceFile: SourceFile = #""" + postfix func ° (value: Double) -> Temperature { + return Temperature(degrees: value) + } + """# + + let operators = sourceFile.symbols.filter { ($0.api as? Function)?.isOperator == true } + XCTAssertEqual(operators.count, 1) + + + do { + let symbol = operators[0] + XCTAssertEqual(symbol.id.name, "postfix °") + XCTAssertEqual(symbol.id.escaped, "postfix-°") + } + } +} diff --git a/Tests/SwiftDocTests/PathTests.swift b/Tests/SwiftDocTests/PathTests.swift index a33804d7..26a809f9 100644 --- a/Tests/SwiftDocTests/PathTests.swift +++ b/Tests/SwiftDocTests/PathTests.swift @@ -3,45 +3,45 @@ import XCTest import SwiftDoc final class PathTests: XCTestCase { - func testEmptyBaseURL() { - XCTAssertEqual(path(for: "Class", with: ""), "Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: ""), "(lhs:rhs:)") - } - - func testRootDirectoryBaseURL() { - XCTAssertEqual(path(for: "Class", with: "/"), "/Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/"), "/(lhs:rhs:)") - } - - func testCurrentDirectoryBaseURL() { - XCTAssertEqual(path(for: "Class", with: "./"), "./Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "./"), "./(lhs:rhs:)") - } - - func testNestedSubdirectoryBaseURL() { - XCTAssertEqual(path(for: "Class", with: "/path/to/directory"), "/path/to/directory/Class") - XCTAssertEqual(path(for: "Class", with: "/path/to/directory/"), "/path/to/directory/Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/path/to/directory"), "/path/to/directory/(lhs:rhs:)") - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/path/to/directory/"), "/path/to/directory/(lhs:rhs:)") - } - - func testDomainBaseURL() { - XCTAssertEqual(path(for: "Class", with: "https://example.com"), "https://example.com/Class") - XCTAssertEqual(path(for: "Class", with: "https://example.com/"), "https://example.com/Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com"), "https://example.com/(lhs:rhs:)") - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/"), "https://example.com/(lhs:rhs:)") - } - - func testDomainSubdirectoryBaseURL() { - XCTAssertEqual(path(for: "Class", with: "https://example.com/docs"), "https://example.com/docs/Class") - XCTAssertEqual(path(for: "Class", with: "https://example.com/docs/"), "https://example.com/docs/Class") - - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/docs"), "https://example.com/docs/(lhs:rhs:)") - XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/docs/"), "https://example.com/docs/(lhs:rhs:)") - } +// func testEmptyBaseURL() { +// XCTAssertEqual(path(for: "Class", with: ""), "Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: ""), "(lhs:rhs:)") +// } +// +// func testRootDirectoryBaseURL() { +// XCTAssertEqual(path(for: "Class", with: "/"), "/Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/"), "/(lhs:rhs:)") +// } +// +// func testCurrentDirectoryBaseURL() { +// XCTAssertEqual(path(for: "Class", with: "./"), "./Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "./"), "./(lhs:rhs:)") +// } +// +// func testNestedSubdirectoryBaseURL() { +// XCTAssertEqual(path(for: "Class", with: "/path/to/directory"), "/path/to/directory/Class") +// XCTAssertEqual(path(for: "Class", with: "/path/to/directory/"), "/path/to/directory/Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/path/to/directory"), "/path/to/directory/(lhs:rhs:)") +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "/path/to/directory/"), "/path/to/directory/(lhs:rhs:)") +// } +// +// func testDomainBaseURL() { +// XCTAssertEqual(path(for: "Class", with: "https://example.com"), "https://example.com/Class") +// XCTAssertEqual(path(for: "Class", with: "https://example.com/"), "https://example.com/Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com"), "https://example.com/(lhs:rhs:)") +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/"), "https://example.com/(lhs:rhs:)") +// } +// +// func testDomainSubdirectoryBaseURL() { +// XCTAssertEqual(path(for: "Class", with: "https://example.com/docs"), "https://example.com/docs/Class") +// XCTAssertEqual(path(for: "Class", with: "https://example.com/docs/"), "https://example.com/docs/Class") +// +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/docs"), "https://example.com/docs/(lhs:rhs:)") +// XCTAssertEqual(path(for: "(lhs:rhs:)", with: "https://example.com/docs/"), "https://example.com/docs/(lhs:rhs:)") +// } } diff --git a/Tests/SwiftDocTests/SourceFileTests.swift b/Tests/SwiftDocTests/SourceFileTests.swift index dd647f24..3c1eb2ee 100644 --- a/Tests/SwiftDocTests/SourceFileTests.swift +++ b/Tests/SwiftDocTests/SourceFileTests.swift @@ -7,7 +7,7 @@ import SwiftSyntax final class SourceFileTests: XCTestCase { func testSourceFile() throws { - let source = #""" + let sourceFile: SourceFile = #""" import Foundation /// Protocol @@ -50,9 +50,6 @@ final class SourceFileTests: XCTestCase { public final class SC: C {} """# - let url = try temporaryFile(contents: source) - let sourceFile = try SourceFile(file: url, relativeTo: url.deletingLastPathComponent()) - XCTAssertEqual(sourceFile.imports.count, 1) XCTAssertEqual(sourceFile.imports.first?.pathComponents, ["Foundation"])