diff --git a/Package.swift b/Package.swift index f2dcd0a..d5b30c7 100644 --- a/Package.swift +++ b/Package.swift @@ -7,22 +7,26 @@ let package = Package( platforms : [ .macOS(.v10_14), .iOS(.v11) ], products : [ - .library (name: "DocCHTMLExporter", targets: [ "DocCHTMLExporter" ]), - .executable(name: "docc2html", targets: [ "docc2html" ]) + .library (name: "DocCHTMLExporter" , targets: [ "DocCHTMLExporter" ]), + .library (name: "DocCStaticExporter" , targets: [ "DocCStaticExporter" ]), + .executable(name: "docc2html" , targets: [ "docc2html" ]) ], dependencies: [ - .package(url: "https://github.com/AlwaysRightInstitute/mustache.git", - from: "1.0.1"), - .package(url: "https://github.com/DoccZz/DocCArchive.git", from: "0.1.0"), - .package(url: "https://github.com/apple/swift-log.git", - from: "1.4.0") + .package(url : "https://github.com/AlwaysRightInstitute/mustache.git", + from : "1.0.1"), + .package(url : "https://github.com/DoccZz/DocCArchive.git", + from : "0.1.0"), + .package(url : "https://github.com/apple/swift-log.git", + from : "1.4.0") ], targets: [ .target(name : "DocCHTMLExporter", dependencies : [ "DocCArchive", "mustache", "Logging" ]), + .target(name : "DocCStaticExporter", + dependencies : [ "DocCArchive", "DocCHTMLExporter", "Logging" ]), .target(name : "docc2html", - dependencies : [ "DocCHTMLExporter", "Logging" ]) + dependencies : [ "DocCStaticExporter", "Logging" ]) ] ) diff --git a/Sources/DocCHTMLExporter/BuildNavigation.swift b/Sources/DocCHTMLExporter/BuildNavigation.swift index fd33929..2b6e5a6 100644 --- a/Sources/DocCHTMLExporter/BuildNavigation.swift +++ b/Sources/DocCHTMLExporter/BuildNavigation.swift @@ -57,10 +57,19 @@ fileprivate extension DocCArchive.Document { let idURL = ref.identifierURL assert(idURL != nil) - let hierarchy = String(repeating: "../", count: idx + 1) - let relURL = hierarchy - + (idURL?.appendingPathExtension("html").lastPathComponent - ?? "index.html") + let relURL : String = { + let needsStepUp = !context.indexLinks || context.isIndex + let hierarchy = + String(repeating: "../", count: idx + (needsStepUp ? 1 : 0)) + if context.indexLinks { + return hierarchy + "index.html" + } + else { + return hierarchy + + (idURL?.appendingPathExtension("html").lastPathComponent + ?? "index.html") + } + }() items.insert( NavigationItem(title: tr.title, isCurrent: false, link: relURL), at: 0 @@ -84,7 +93,7 @@ fileprivate extension DocCArchive.DocumentFolder { // FIXME: Here we somehow need to figure out the real title, is this // NOT in the JSON? Feels weird. NavigationItem(title: title, isCurrent: false, - link: String(repeating: "../", count: idx + 1) + link: String(repeating: "../", count: idx) .appending("index.html")) }.reversed() ) diff --git a/Sources/DocCHTMLExporter/DZRenderingContext.swift b/Sources/DocCHTMLExporter/DZRenderingContext.swift index d087741..9f2534d 100644 --- a/Sources/DocCHTMLExporter/DZRenderingContext.swift +++ b/Sources/DocCHTMLExporter/DZRenderingContext.swift @@ -15,7 +15,7 @@ import mustache * The object is used for rendering a single document. Not intended to be * invoked multiple times. */ -public final class DZRenderingContext { +open class DZRenderingContext { public static let defaultStyleSheet = stylesheet @@ -87,6 +87,9 @@ public final class DZRenderingContext { let pathToRoot : String let references : [ String : DocCArchive.Reference ] + let indexLinks : Bool + let isIndex : Bool + let dataFolderPathes : Set let traits : Set = [ .light, .retina ] @@ -110,6 +113,9 @@ public final class DZRenderingContext { */ public init(pathToRoot : String, references : [ String : DocCArchive.Reference ], + isIndex : Bool, + dataFolderPathes : Set, + indexLinks : Bool, templates : Templates? = nil, labels : Labels? = nil, moduleToExternalURL : [ String : URL ]? = nil, @@ -117,6 +123,9 @@ public final class DZRenderingContext { { self.pathToRoot = pathToRoot self.references = references + self.isIndex = isIndex + self.dataFolderPathes = dataFolderPathes + self.indexLinks = indexLinks self.templates = templates ?? Templates() self.labels = labels ?? Labels() self.moduleToExternalURL = moduleToExternalURL ?? ModuleToExternalURL @@ -126,13 +135,38 @@ public final class DZRenderingContext { // MARK: - URLs - func makeRelativeToRoot(_ url: String) -> String { + + private func makeRelativeToRoot(_ url: String) -> String { if url.hasPrefix("/") { return pathToRoot + url.dropFirst() } return pathToRoot + url } - func makeRelativeToRoot(_ url: URL) -> String { + private func makeRelativeToRoot(_ url: URL) -> String { return makeRelativeToRoot(url.path) } + + func linkToResource(_ url: String) -> String { + return makeRelativeToRoot(url) + } + + func linkToDocument(_ identifierURL: URL) -> String { + // Note: This is not very clever yet, it essentially goes up the chain + // using `../..` and then appends the "absolute" path, like + // `../../documentation/SwiftBlocksUI/index.html`. + // Note: Those can have path extensions, e.g. "color-swift.property" + assert(identifierURL.scheme == "doc") + assert(!dataFolderPathes.isEmpty) + + // This is a little hacky, but yeah. The dataFolderPathes are always + // lowercase. Technically we are supposed to use the reference URL, + // but that doesn't come w/ the `doc` scheme and is probably less safe + // to use. + let isIndexed = dataFolderPathes.contains(identifierURL.path.lowercased()) + + let url = isIndexed + ? identifierURL.appendingPathComponent("index.html") + : identifierURL.appendingPathExtension("html") + return makeRelativeToRoot(url) + } func externalDocumentBaseURL(for module: String) -> URL? { return moduleToExternalURL[module] diff --git a/Sources/DocCHTMLExporter/HTML/ReferenceHTML.swift b/Sources/DocCHTMLExporter/HTML/ReferenceHTML.swift index cb0e288..a360a89 100644 --- a/Sources/DocCHTMLExporter/HTML/ReferenceHTML.swift +++ b/Sources/DocCHTMLExporter/HTML/ReferenceHTML.swift @@ -11,13 +11,13 @@ import DocCArchive extension DocCArchive.Reference { - func generateHTML(isActive: Bool = true, in ctx: DZRenderingContext) -> String { - var idURL : URL? { return URL(string: identifier) } - + func generateHTML(isActive: Bool = true, in ctx: DZRenderingContext) + -> String + { switch self { case .topic(let ref): - return ref.generateHTML(isActive: isActive, idURL: idURL, in: ctx) + return ref.generateHTML(isActive: isActive, in: ctx) case .image(let ref): return ref.generateHTML(in: ctx) @@ -39,12 +39,12 @@ extension DocCArchive.Reference { switch self { case .topic(let ref): - guard let url = ref.url ?? idURL else { return "" } - return ctx.makeRelativeToRoot(url) + ".html" - + guard let url = idURL ?? ref.url else { return "" } + return ctx.linkToDocument(url) + case .image(let ref): guard let variant = ref.bestVariant(for: ctx.traits) else { return "" } - return ctx.makeRelativeToRoot(variant.url) + return ctx.linkToResource(variant.url) case .file(_): fatalError("unsupported file ref") @@ -102,14 +102,18 @@ extension DocCArchive.TopicReference { /** * Generate an for the topic reference. */ - func generateHTML(isActive: Bool = true, idURL: URL?, + func generateHTML(isActive: Bool = true, in ctx: DZRenderingContext) -> String { + let idURL = URL(string: identifier) + assert(idURL != nil, "topic refs should always have proper URLs") + assert(idURL?.scheme == "doc", "topic ref w/o a doc:// URL") + let activeClass = isActive ? "" : " class='inactive'" let title = self.title.htmlEscaped - let url = (self.url ?? idURL).flatMap { - ctx.makeRelativeToRoot($0.appendingPathExtension("html")) - } ?? "" + + let url = (idURL ?? self.url).flatMap(ctx.linkToDocument) ?? "" + assert(!url.isEmpty) var ms = "" if !url.isEmpty { ms += "" } @@ -151,7 +155,7 @@ extension DocCArchive.ImageReference { } }() - let url = ctx.makeRelativeToRoot(variant.url) + let url = ctx.linkToResource(variant.url) let srcset = url.htmlEscaped + (ctx.traits.contains(.retina) ? " 2x" : "") diff --git a/Sources/DocCStaticExporter/DocCFileSystemExportTarget.swift b/Sources/DocCStaticExporter/DocCFileSystemExportTarget.swift new file mode 100644 index 0000000..f2ecb05 --- /dev/null +++ b/Sources/DocCStaticExporter/DocCFileSystemExportTarget.swift @@ -0,0 +1,142 @@ +// +// DocCFileSystemExportTarget.swift +// docc2html +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import Foundation +import Logging + +open class DocCFileSystemExportTarget: DocCStaticExportTarget, + CustomStringConvertible +{ + + public let logger : Logger + public let fileManager : FileManager + public let targetPath : String + open var targetURL : URL { return URL(fileURLWithPath: targetPath) } + + public init(targetPath: String, fileManager: FileManager = .default, + logger: Logger = Logger(label: "docc2html")) + { + self.logger = logger + self.targetPath = targetPath + self.fileManager = fileManager + } + + + open func doesTargetExist() -> Bool { + return fileManager.fileExists(atPath: targetPath) + } + + + // MARK: - Copying Static Resources + + open func copyCSS(_ cssFiles: [ URL ], keepHash: Bool) throws { + guard !cssFiles.isEmpty else { return } + + let targetURL = self.targetURL.appendingPathComponent("css") + try ensureTargetDir("css") + + for css in cssFiles { + let cssContents : String + do { + cssContents = try String(contentsOf: css) + .stringByRemovingDocCDataReferences() + } + catch { + logger.error("Failed to load CSS:", css.path) + throw DocCStaticExportError + .couldNotLoadStaticResource(css, underlyingError: error) + } + + let targetName = keepHash + ? css.lastPathComponent + : css.deletingResourceHash().lastPathComponent + + let cssTargetURL = targetURL.appendingPathComponent(targetName) + + logger.trace("Copying CSS \(css.path) to \(cssTargetURL.path)") + + do { + try cssContents.write(to: cssTargetURL, atomically: false, + encoding: .utf8) + } + catch { + logger.error("Failed to save CSS:", cssTargetURL.path) + throw DocCStaticExportError + .couldNotCopyStaticResource(from: css, to: "css") + } + } + } + + public func copyRaw(_ files: [ URL ], to directory: String, + keepHash: Bool = true) throws + { + guard !files.isEmpty else { return } + + let targetURL = self.targetURL.appendingPathComponent(directory) + try ensureTargetDir(directory) + + for file in files { + let targetName = keepHash + ? file.lastPathComponent + : file.deletingResourceHash().lastPathComponent + + let fileTargetURL = targetURL.appendingPathComponent(targetName) + + logger.trace("Copying resource \(file.path) to \(fileTargetURL.path)") + + // copyItem fails if the target exists. + try? fileManager.removeItem(at: fileTargetURL) + + do { + try fileManager.copyItem(at: file, to: fileTargetURL) + } + catch { + logger.error("Failed to copy resource:", fileTargetURL.path, error) + throw DocCStaticExportError + .couldNotCopyStaticResource(from: file, to: directory) + } + } + } + + + // MARK: - Files + + open func write(_ content: String, to relativePath: String) throws { + let url = targetURL.appendingPathComponent(relativePath) + // TODO: own error + try content.write(to: url, atomically: false, encoding: .utf8) + } + + + // MARK: - Subdirectories + + open func ensureTargetDir(_ relativePath: String) throws { + var url = targetURL + if !relativePath.isEmpty { + url.appendPathComponent(relativePath) + } + + do { + try fileManager + .createDirectory(at: url, withIntermediateDirectories: true) + logger.trace("Created output subdir:", url.path) + } + catch { + logger.error("Could not create target directory:", url.path, error) + throw DocCStaticExportError + .couldNotCreateTargetDirectory(relativePath, underlyingError: error) + } + } + + + // MARK: - Description + + open var description: String { + return targetPath + } +} diff --git a/Sources/DocCStaticExporter/DocCStaticExportError.swift b/Sources/DocCStaticExporter/DocCStaticExportError.swift new file mode 100644 index 0000000..2e7e553 --- /dev/null +++ b/Sources/DocCStaticExporter/DocCStaticExportError.swift @@ -0,0 +1,22 @@ +// +// DocCStaticExportError.swift +// docc2html +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import struct Foundation.URL + +public enum DocCStaticExportError: Swift.Error { + + case targetExists(DocCStaticExportTarget) + + case expectedDocCArchive(URL) + + case couldNotLoadStaticResource(URL, underlyingError: Swift.Error) + + case couldNotCopyStaticResource(from: URL, to: String) + + case couldNotCreateTargetDirectory(String, underlyingError: Swift.Error) +} diff --git a/Sources/DocCStaticExporter/DocCStaticExportTarget.swift b/Sources/DocCStaticExporter/DocCStaticExportTarget.swift new file mode 100644 index 0000000..3d47baf --- /dev/null +++ b/Sources/DocCStaticExporter/DocCStaticExportTarget.swift @@ -0,0 +1,50 @@ +// +// DocCStaticExportTarget.swift +// docc2html +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import struct Foundation.URL + +public protocol DocCStaticExportTarget { + + /// Check whether the exporter container does already exist (to suppor the + /// (non) --force option). + func doesTargetExist() -> Bool + + /** + * Makes sure that the given relative directory exists and can be written to + * subsequently. + * + * Example: + * + * ensureTargetDir("css") + * + */ + func ensureTargetDir(_ relativePath: String) throws + + func copyCSS(_ cssFiles: [ URL ], keepHash: Bool) throws + + /** + * Copy the files from the given URLs to the directory within the target. + * + * Use an empty string for "root" + * + * Example: + * + * copyRaw(archive.userImageURLs(), to: "images") + */ + func copyRaw(_ files: [ URL ], to directory: String, keepHash: Bool) throws + + /// Write some content (e.g. a rendered page) to the given relative path + func write(_ content: String, to relativePath: String) throws +} + +public extension DocCStaticExportTarget { + + func copyRaw(_ files: [ URL ], to directory: String) throws { + try copyRaw(files, to: directory, keepHash: false) + } +} diff --git a/Sources/DocCStaticExporter/DocCStaticExporter.swift b/Sources/DocCStaticExporter/DocCStaticExporter.swift new file mode 100644 index 0000000..326d93e --- /dev/null +++ b/Sources/DocCStaticExporter/DocCStaticExporter.swift @@ -0,0 +1,207 @@ +// +// DocCStaticExporter.swift +// docc2html +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import Foundation +import Logging +import DocCArchive +import DocCHTMLExporter + +open class DocCStaticExporter { + + public struct Options: OptionSet { + public let rawValue : UInt8 + public init(rawValue: UInt8) { self.rawValue = rawValue } + + public static let force = Options(rawValue: 1 << 0) + public static let keepHash = Options(rawValue: 1 << 1) + public static let copySystemCSS = Options(rawValue: 1 << 2) + public static let buildIndex = Options(rawValue: 1 << 3) + public static let buildAPIDocs = Options(rawValue: 1 << 4) + public static let buildTutorials = Options(rawValue: 1 << 5) + } + + public let logger : Logger + public let options : Options + public let target : DocCStaticExportTarget + public let archiveURLs : [ URL ] + public let stylesheet = DZRenderingContext.defaultStyleSheet + + public var dataFolderPathes = Set() + + public init(target: DocCStaticExportTarget, archivePathes: [ String ], + options: Options, logger: Logger = Logger(label: "docc2html")) + { + self.target = target + self.archiveURLs = archivePathes.map(URL.init(fileURLWithPath:)) + self.options = options + self.logger = logger + } + + + public func export() throws { + if !target.doesTargetExist() { + try target.ensureTargetDir("") + } + else if !options.contains(.force) { + logger.error("Target directory exists (call w/ -f/--force to overwrite):", + target) + throw DocCStaticExportError.targetExists(target) + } + else { + logger.log("Existing output dir:", target) + } + + let archives = try loadArchives(archiveURLs) + + for archive in archives { + dataFolderPathes.formUnion(archive.fetchDataFolderPathes()) + } + + try copyStaticResources(of: archives) + try generatePages (of: archives) + } + + + // MARK: - Loading Archives + + func loadArchives(_ pathes: [ URL ]) throws -> [ DocCArchive ] { + var archives = [ DocCArchive ]() + for url in pathes { + do { + archives.append(try DocCArchive(contentsOf: url)) + } + catch { + logger.error("Does not look like a .doccarchive:", error) + throw DocCStaticExportError.expectedDocCArchive(url) + } + } + return archives + } + + // MARK: - Copy Static Resources + + func copyStaticResources(of archives: [ DocCArchive ]) throws { + for archive in archives { + logger.log("Copy static resources of:", archive.url.lastPathComponent) + + if options.contains(.copySystemCSS) { + let cssFiles = archive.stylesheetURLs() + if !cssFiles.isEmpty { + try target.copyCSS(archive.stylesheetURLs(), + keepHash: options.contains(.keepHash)) + } + } + + try target.copyRaw(archive.userImageURLs(), to: "images") + try target.copyRaw(archive.userVideoURLs(), to: "videos") + try target.copyRaw(archive.userDownloadURLs(), to: "downloads") + try target.copyRaw(archive.favIcons(), to: "") + + try target.copyRaw(archive.systemImageURLs(), to: "img", + keepHash: options.contains(.keepHash)) + } + + do { + try target.ensureTargetDir("css") + try target.write(stylesheet, to: "css/site.css") + } + catch { + logger.error("Failed to write custom stylesheet:", error) + } + } + + // MARK: - Generate + + func generatePages(of archives: [ DocCArchive ]) throws { + for archive in archives { + logger.log("Generate archive:", archive.url.lastPathComponent) + + if options.contains(.buildAPIDocs), + let folder = archive.documentationFolder() + { + try buildFolder(folder, into: "documentation", + buildIndex: options.contains(.buildIndex)) + } + if options.contains(.buildTutorials), + let folder = archive.tutorialsFolder() + { + try buildFolder(folder, into: "tutorials", + buildIndex: options.contains(.buildIndex)) + } + } + } + + + // MARK: - Folders + + func buildFolder(_ folder : DocCArchive.DocumentFolder, + into relativePath : String, + buildIndex : Bool) throws + { + try target.ensureTargetDir(relativePath) + + let subfolders = folder.subfolders() + + for subfolder in subfolders { + let dest = relativePath + "/" + subfolder.url.lastPathComponent + try buildFolder(subfolder, into: dest, buildIndex: buildIndex) + } + let subfolderNames = Set(subfolders.map { $0.url.lastPathComponent }) + + for pageURL in folder.pageURLs() { + do { + let document = try folder.document(at: pageURL) + let baseName = pageURL.deletingPathExtension().lastPathComponent + let pathToRoot = String(repeating: "../", count: folder.level) + let isIndexPage = subfolderNames.contains(baseName) + + if buildIndex, isIndexPage { + let htmlPath = relativePath + "/" + baseName + "/index.html" + + logger.trace("Index:", document, "\n to:", htmlPath) + + let ctx = DZRenderingContext( + pathToRoot : pathToRoot + "../", + references : document.references, + isIndex : true, + dataFolderPathes : dataFolderPathes, + indexLinks : true + ) + + let html = try ctx.buildDocument(document, in: folder) + + try target.write(html, to: htmlPath) + } + else { + let htmlPath = relativePath + "/" + + pageURL + .deletingPathExtension() // JSON + .appendingPathExtension("html") + .lastPathComponent + + logger.trace("Build:", document, "\n to:", htmlPath) + + let ctx = DZRenderingContext( + pathToRoot : pathToRoot, + references : document.references, + isIndex : false, + dataFolderPathes : dataFolderPathes, + indexLinks : buildIndex + ) + + let html = try ctx.buildDocument(document, in: folder) + + try target.write(html, to: htmlPath) + } + } + catch { + logger.error("Could not process document at:", pageURL.path, error) + } + } + } +} diff --git a/Sources/DocCStaticExporter/Utilities/Console.swift b/Sources/DocCStaticExporter/Utilities/Console.swift new file mode 100644 index 0000000..1676ada --- /dev/null +++ b/Sources/DocCStaticExporter/Utilities/Console.swift @@ -0,0 +1,72 @@ +// +// Console.swift +// Macro +// +// Created by Helge Hess. +// Copyright © 2020 ZeeZide GmbH. All rights reserved. +// + +import struct Logging.Logger + +/** + * Just a small JavaScript like `console` shim around the Swift Logging API. + */ +public extension Logger { + + @available(*, deprecated, message: "please use `console` directly") + var logger : Logger { return self } + + @usableFromInline + internal func string(for msg: String, _ values: [ Any? ]) -> Logger.Message { + var message = msg + for value in values { + if let value = value { + message.append(" ") + if let s = value as? String { + message.append(s) + } + else if let s = value as? CustomStringConvertible { + message.append(s.description) + } + else { + message.append("\(value)") + } + } + else { + message.append(" ") + } + } + return Logger.Message(stringLiteral: message) + } + + @inlinable func error(_ msg: @autoclosure () -> String, _ values : Any?...) { + error(string(for: msg(), values)) + } + @inlinable func warn (_ msg: @autoclosure () -> String, _ values : Any?...) { + warning(string(for: msg(), values)) + } + @inlinable func log (_ msg: @autoclosure () -> String, _ values : Any?...) { + notice(string(for: msg(), values)) + } + @inlinable func info (_ msg: @autoclosure () -> String, _ values : Any?...) { + info(string(for: msg(), values)) + } + @inlinable func trace(_ msg: @autoclosure () -> String, _ values : Any?...) { + trace(string(for: msg(), values)) + } + + func dir(_ obj: Any?) { + guard let obj = obj else { + return notice("") + } + + struct StringOutputStream: TextOutputStream { + var value = "" + mutating func write(_ string: String) { value += string } + } + var out = StringOutputStream() + + dump(obj, to: &out) + return notice(Logger.Message(stringLiteral: out.value)) + } +} diff --git a/Sources/docc2html/BuildDocument.swift b/Sources/docc2html/BuildDocument.swift deleted file mode 100644 index 64524f1..0000000 --- a/Sources/docc2html/BuildDocument.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// BuildDocument.swift -// docc2html -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import Foundation -import DocCArchive -import DocCHTMLExporter - -func buildDocument(_ document : DocCArchive.Document, - in folder : DocCArchive.DocumentFolder, - to url : URL) throws -{ - console.trace("Build:", document, "\n to:", url.path) - - let ctx = DZRenderingContext( - pathToRoot: String(repeating: "../", count: folder.level), - references: document.references - ) - - let html = try ctx.buildDocument(document, in: folder) - - try html.write(to: url, atomically: false, encoding: .utf8) -} diff --git a/Sources/docc2html/BuildFolder.swift b/Sources/docc2html/BuildFolder.swift deleted file mode 100644 index d0544e1..0000000 --- a/Sources/docc2html/BuildFolder.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// BuildFolder.swift -// docc2html -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import Foundation -import DocCArchive - -func buildFolder(_ folder: DocCArchive.DocumentFolder, into url: URL) { - ensureTargetDir(url) - - for subfolder in folder.subfolders() { - let dest = url.appendingPathComponent(subfolder.url.lastPathComponent) - buildFolder(subfolder, into: dest) - } - - for pageURL in folder.pageURLs() { - do { - let htmlURL = url.appendingPathComponent( - pageURL - .deletingPathExtension() // JSON - .appendingPathExtension("html") - .lastPathComponent - ) - - console.trace("build:", pageURL.path) - let document = try folder.document(at: pageURL) - try buildDocument(document, in: folder, to: htmlURL) - } - catch { - console.error("ERROR: Could not process document at:", - pageURL.path, error) - } - } -} diff --git a/Sources/docc2html/ExitCode.swift b/Sources/docc2html/ExitCode.swift index cc6109a..3bf68c7 100644 --- a/Sources/docc2html/ExitCode.swift +++ b/Sources/docc2html/ExitCode.swift @@ -7,8 +7,9 @@ // import Foundation +import Logging +import DocCStaticExporter -// TBD: Maybe make that rather a special kind of exception enum ExitCode: Int32, Swift.Error { case notEnoughArguments = 1 case expectedDocCArchive = 2 @@ -18,11 +19,24 @@ enum ExitCode: Int32, Swift.Error { case couldNotCopyStaticResource = 6 } +extension ExitCode { + + init(_ error: DocCStaticExportError) { + switch error { + case .targetExists : self = .targetDirectoryExists + case .expectedDocCArchive : self = .expectedDocCArchive + case .couldNotLoadStaticResource : self = .couldNotLoadStaticResource + case .couldNotCopyStaticResource : self = .couldNotCopyStaticResource + case .couldNotCreateTargetDirectory: self = .couldNotCreateTargetDirectory + } + } +} + func exit(_ error: ExitCode) -> Never { exit(error.rawValue) } func exit(_ error: Swift.Error) -> Never { if let error = error as? ExitCode { exit(error.rawValue) } - console.error("Unexpected error:", error) + Logger(label: "docc2html").error("Unexpected error:", error) exit(42) } diff --git a/Sources/docc2html/Options.swift b/Sources/docc2html/Options.swift index d7a9307..60f85e1 100644 --- a/Sources/docc2html/Options.swift +++ b/Sources/docc2html/Options.swift @@ -8,6 +8,7 @@ import Foundation import Logging +import DocCStaticExporter func usage() { let tool = URL(fileURLWithPath: CommandLine.arguments.first ?? "docc2html") @@ -31,11 +32,11 @@ func usage() { ) } +/// The options for the tool itself. struct Options { - let force : Bool - let keepHash : Bool - let copySystemCSS : Bool + var exportOptions : DocCStaticExporter.Options + = [ .buildIndex, .buildAPIDocs, .buildTutorials ] let archivePathes : [ String ] let targetPath : String let logFactory : ( String ) -> LogHandler @@ -43,10 +44,12 @@ struct Options { var targetURL : URL { URL(fileURLWithPath: targetPath) } init?(argv: [ String ]) { - force = argv.contains("--force") || argv.contains("-f") - keepHash = argv.contains("--keep-hash") - copySystemCSS = argv.contains("--copy-css") - + if argv.contains("--force") || argv.contains("-f") { + exportOptions.insert(.force) + } + if argv.contains("--keep-hash") { exportOptions.insert(.keepHash ) } + if argv.contains("--copy-css") { exportOptions.insert(.copySystemCSS) } + let silent = argv.contains("--silent") || argv.contains("-s") let verbose = argv.contains("--verbose") || argv.contains("-v") diff --git a/Sources/docc2html/Utilities/Console.swift b/Sources/docc2html/Utilities/Console.swift index fb7a9d0..be5b532 100644 --- a/Sources/docc2html/Utilities/Console.swift +++ b/Sources/docc2html/Utilities/Console.swift @@ -8,8 +8,6 @@ import struct Logging.Logger -public let console = Logger(label: "μ.console") - /** * Just a small JavaScript like `console` shim around the Swift Logging API. */ diff --git a/Sources/docc2html/Utilities/CopyCSS.swift b/Sources/docc2html/Utilities/CopyCSS.swift deleted file mode 100644 index 4f07d9a..0000000 --- a/Sources/docc2html/Utilities/CopyCSS.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// CopyCSS.swift -// docc2html -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import Foundation -import Logging - -func copyCSS(_ cssFiles: [ URL ], to targetURL: URL, keepHash: Bool, - logger: Logger = console) -{ - guard !cssFiles.isEmpty else { return } - - for css in cssFiles { - let cssContents : String - do { - cssContents = try String(contentsOf: css) - .stringByRemovingDocCDataReferences() - } - catch { - logger.error("Failed to load CSS:", css.path) - exit(ExitCode.couldNotLoadStaticResource) - } - - let targetName = keepHash - ? css.lastPathComponent - : css.deletingResourceHash().lastPathComponent - - let cssTargetURL = targetURL.appendingPathComponent(targetName) - - logger.trace("Copying CSS \(css.path) to \(cssTargetURL.path)") - - do { - try cssContents.write(to: cssTargetURL, atomically: false, - encoding: .utf8) - } - catch { - logger.error("Failed to save CSS:", cssTargetURL.path) - exit(ExitCode.couldNotCopyStaticResource) - } - } -} diff --git a/Sources/docc2html/Utilities/CopyRaw.swift b/Sources/docc2html/Utilities/CopyRaw.swift deleted file mode 100644 index 823db14..0000000 --- a/Sources/docc2html/Utilities/CopyRaw.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// CopyRaw.swift -// docc2html -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import Foundation -import Logging - -func copyRaw(_ files: [ URL ], to targetURL: URL, keepHash: Bool = true, - logger: Logger = console) -{ - guard !files.isEmpty else { return } - - let fm = FileManager.default - for file in files { - let targetName = keepHash - ? file.lastPathComponent - : file.deletingResourceHash().lastPathComponent - - let fileTargetURL = targetURL.appendingPathComponent(targetName) - - logger.trace("Copying resource \(file.path) to \(fileTargetURL.path)") - - // copyItem fails if the target exists. - try? fm.removeItem(at: fileTargetURL) - - do { - try fm.copyItem(at: file, to: fileTargetURL) - } - catch { - logger.error("Failed to copy resource:", fileTargetURL.path, error) - exit(ExitCode.couldNotCopyStaticResource) - } - } -} - diff --git a/Sources/docc2html/main.swift b/Sources/docc2html/main.swift index 9ff3585..05746b4 100644 --- a/Sources/docc2html/main.swift +++ b/Sources/docc2html/main.swift @@ -7,13 +7,8 @@ // import Foundation -import DocCArchive // @DoccZz -import DocCHTMLExporter // @DoccZz/docc2html -import Logging // @apple/swift-log - -let fm = FileManager.default - -// MARK: - Parse Commandline Arguments & Usage +import Logging // @apple/swift-log +import DocCStaticExporter // @docczz/docc2html guard let options = Options(argv: CommandLine.arguments) else { usage() @@ -22,109 +17,22 @@ guard let options = Options(argv: CommandLine.arguments) else { LoggingSystem.bootstrap(options.logFactory) -func loadArchives(_ pathes: [ String ]) -> [ DocCArchive ] { - var archives = [ DocCArchive ]() - for path in pathes { - do { - let url = URL(fileURLWithPath: path) - archives.append(try DocCArchive(contentsOf: url)) - } - catch { - console.error("Does not look like a .doccarchive:", error) - exit(ExitCode.expectedDocCArchive.rawValue) - } - } - return archives -} - - -// MARK: - Create Destination Folder +let exporter = DocCStaticExporter( + target : DocCFileSystemExportTarget(targetPath: options.targetPath), + archivePathes : options.archivePathes, + options : options.exportOptions +) -@discardableResult -func ensureTargetDir(_ relativePath: String) -> URL { - var url = URL(fileURLWithPath: options.targetPath) - if !relativePath.isEmpty { url.appendPathComponent(relativePath) } - ensureTargetDir(url) - return url +do { + try exporter.export() } -func ensureTargetDir(_ url: URL) { - do { - let fm = FileManager.default - try fm.createDirectory(at: url, withIntermediateDirectories: true) - console.trace("Created output subdir:", url.path) - } - catch { - console.error("Could not create target directory:", url.path, error) - exit(ExitCode.couldNotCreateTargetDirectory.rawValue) - } +catch let error as DocCStaticExportError { + exit(ExitCode(error).rawValue) } - -if !fm.fileExists(atPath: options.targetPath) { - ensureTargetDir("") -} -else if !options.force { - console.error("Target directory exists (call w/ -f/--force to overwrite):", - options.targetPath) - exit(ExitCode.targetDirectoryExists.rawValue) +catch let error as ExitCode { + exit(error.rawValue) } -else { - console.log("Existing output dir:", options.targetPath) +catch { + exporter.logger.error("Unexpected error:", error) + exit(99) } - - -// MARK: - Copy Static Resources - -func copyStaticResources(of archives: [ DocCArchive ]) { - for archive in archives { - console.log("Copy static resources of:", archive.url.lastPathComponent) - - if options.copySystemCSS { - let cssFiles = archive.stylesheetURLs() - if !cssFiles.isEmpty { - copyCSS(archive.stylesheetURLs(), to: ensureTargetDir("css"), - keepHash: options.keepHash) - } - } - - copyRaw(archive.userImageURLs(), to: ensureTargetDir("images")) - copyRaw(archive.userVideoURLs(), to: ensureTargetDir("videos")) - copyRaw(archive.userDownloadURLs(), to: ensureTargetDir("downloads")) - copyRaw(archive.favIcons(), to: ensureTargetDir("")) - - copyRaw(archive.systemImageURLs(), to: ensureTargetDir("img"), - keepHash: options.keepHash) - } - - do { - let siteCSS = ensureTargetDir("css").appendingPathComponent("site.css") - try DZRenderingContext.defaultStyleSheet - .write(to: siteCSS, atomically: false, encoding: .utf8) - } - catch { - console.log("Failed to write custom stylesheet:", error) - } -} - -// MARK: - Generate - -func generatePages(of archives: [ DocCArchive ]) { - for archive in archives { - console.log("Generate archive:", archive.url.lastPathComponent) - - if let folder = archive.documentationFolder() { - let targetURL = options.targetURL.appendingPathComponent("documentation") - buildFolder(folder, into: targetURL) - } - if let folder = archive.tutorialsFolder() { - let targetURL = options.targetURL.appendingPathComponent("tutorials") - buildFolder(folder, into: targetURL) - } - } -} - - -// MARK: - Run - -let archives = loadArchives(options.archivePathes) -copyStaticResources(of: archives) -generatePages (of: archives)