diff --git a/Sources/ToucanSDK/Extensions/Dictionary+Extensions.swift b/Sources/ToucanSDK/Extensions/Dictionary+Extensions.swift index cf3f1586..28194a9c 100644 --- a/Sources/ToucanSDK/Extensions/Dictionary+Extensions.swift +++ b/Sources/ToucanSDK/Extensions/Dictionary+Extensions.swift @@ -104,8 +104,15 @@ extension Dictionary where Key == String, Value == Any { /// /// - Parameter keyPath: The key path string, where keys are separated by dots. /// - Returns: The string at the specified key path - func string(_ keyPath: String) -> String? { - value(keyPath, as: String.self) + func string( + _ keyPath: String, + allowingEmptyValue: Bool = false + ) -> String? { + let result = value(keyPath, as: String.self) + if allowingEmptyValue { + return result + } + return result.emptyToNil } /// Retrieves the integer associated with the given key path. diff --git a/Sources/ToucanSDK/Mustache/MustacheToHTMLRenderer.swift b/Sources/ToucanSDK/Mustache/MustacheToHTMLRenderer.swift index e9643ffc..29565933 100644 --- a/Sources/ToucanSDK/Mustache/MustacheToHTMLRenderer.swift +++ b/Sources/ToucanSDK/Mustache/MustacheToHTMLRenderer.swift @@ -41,10 +41,8 @@ public struct MustacheToHTMLRenderer { self.library = MustacheLibrary(templates: templates) self.ids = Array(templates.keys) - logger.trace("Templates url: `\(templatesUrl.absoluteString)`") - logger.trace("Template overrides url: `\(overridesUrl.absoluteString)`") logger.trace( - "Available templates: \(ids.map { "`\($0)`" }.joined(separator: ", "))" + "Available templates: \(ids.sorted().map { "`\($0)`" }.joined(separator: ", "))" ) } diff --git a/Sources/ToucanSDK/Source/Config.swift b/Sources/ToucanSDK/Source/Config.swift index a6ad2b2a..b441f1bb 100644 --- a/Sources/ToucanSDK/Source/Config.swift +++ b/Sources/ToucanSDK/Source/Config.swift @@ -8,11 +8,49 @@ struct Config { struct Location { + + enum Keys { + static let folder = "folder" + } + let folder: String + + init(folder: String) { + self.folder = folder + } + + init?(_ dict: [String: Any]) { + guard let folder = dict.string(Keys.folder) else { + return nil + } + self.folder = folder + } } + // MARK: - + struct Site { + enum Keys { + static let baseUrl = "baseUrl" + static let title = "title" + static let description = "description" + static let language = "language" + static let dateFormat = "dateFormat" + static let noindex = "noindex" + static let hreflang = "hreflang" + + static let allKeys: [String] = [ + Keys.baseUrl, + Keys.title, + Keys.description, + Keys.language, + Keys.dateFormat, + Keys.noindex, + Keys.hreflang, + ] + } + struct Hreflang: Codable { let lang: String let url: String @@ -22,32 +60,261 @@ struct Config { let title: String let description: String let language: String? - let dateFormat: String? - let noindex: Bool? - let hreflang: [Hreflang]? + let dateFormat: String + let noindex: Bool + let hreflang: [Hreflang] let userDefined: [String: Any] + + init( + baseUrl: String, + title: String, + description: String, + language: String?, + dateFormat: String, + noindex: Bool, + hreflang: [Hreflang], + userDefined: [String: Any] + ) { + self.baseUrl = baseUrl + self.title = title + self.description = description + self.language = language + self.dateFormat = dateFormat + self.noindex = noindex + self.hreflang = hreflang + self.userDefined = userDefined + } + + init(_ dict: [String: Any]) { + self.baseUrl = + dict.string(Keys.baseUrl) + ?? Config.defaults.site.baseUrl + + self.title = + dict.string(Keys.title) + ?? Config.defaults.site.title + + self.description = + dict.string(Keys.description) + ?? Config.defaults.site.description + + self.language = dict.string(Keys.language) + + self.dateFormat = + dict.string(Keys.dateFormat) + ?? Config.defaults.site.dateFormat + + self.noindex = + dict.bool(Keys.noindex) + ?? Config.defaults.site.noindex + + self.hreflang = dict.array(Keys.hreflang, as: Hreflang.self) + self.userDefined = dict.filter { !Keys.allKeys.contains($0.key) } + } } + // MARK: - + struct Themes { + + enum Keys { + static let use = "use" + static let assets = "assets" + static let templates = "templates" + static let types = "types" + static let overrides = "overrides" + } + let use: String let folder: String - let templates: Location let assets: Location + let templates: Location + let types: Location let overrides: Location + + init( + use: String, + folder: String, + assets: Config.Location, + templates: Config.Location, + types: Config.Location, + overrides: Config.Location + ) { + self.use = use + self.folder = folder + self.assets = assets + self.templates = templates + self.types = types + self.overrides = overrides + } + + init(_ dict: [String: Any]) { + self.use = + dict.string(Keys.use) + ?? Config.defaults.themes.use + + self.folder = + dict.string(Location.Keys.folder) + ?? Config.defaults.themes.folder + + let assets = dict.dict(Keys.assets) + self.assets = + Location(assets) + ?? Config.defaults.themes.assets + + let templates = dict.dict(Keys.templates) + self.templates = + Location(templates) + ?? Config.defaults.themes.templates + + let overrides = dict.dict(Keys.overrides) + self.overrides = + Location(overrides) + ?? Config.defaults.themes.overrides + + let types = dict.dict(Keys.types) + self.types = + Location(types) + ?? Config.defaults.themes.types + } } - struct Content { + // MARK: - + + struct Contents { + + enum Keys { + static let dateFormat = "dateFormat" + static let assets = "assets" + } + let folder: String let dateFormat: String let assets: Location + + init( + folder: String, + dateFormat: String, + assets: Config.Location + ) { + self.folder = folder + self.dateFormat = dateFormat + self.assets = assets + } + + init(_ dict: [String: Any]) { + self.folder = + dict.string(Location.Keys.folder) + ?? Config.defaults.contents.folder + + self.dateFormat = + dict.string(Keys.dateFormat) + ?? Config.defaults.contents.dateFormat + + let assets = dict.dict(Keys.assets) + self.assets = + Location(assets) + ?? Config.defaults.themes.assets + } } - struct Types { + // MARK: - + + struct Transformers { + + enum Keys { + static let pipelines = "pipelines" + } + + struct Pipeline { + let types: [String] + let run: [String] + let render: Bool + } + let folder: String + let pipelines: [Pipeline] + + init( + folder: String, + pipelines: [Pipeline] + ) { + self.folder = folder + self.pipelines = pipelines + } + + init(_ dict: [String: Any]) { + self.folder = + dict.string(Location.Keys.folder) + ?? Config.defaults.transformers.folder + + self.pipelines = dict.array(Keys.pipelines, as: Pipeline.self) + } } - var site: Site + // MARK: - + + enum Keys { + static let site = "site" + static let themes = "themes" + static let contents = "contents" + static let transformers = "transformers" + } + + let site: Site let themes: Themes - let types: Types - let content: Content + let contents: Contents + let transformers: Transformers + + init( + site: Site, + themes: Themes, + contents: Contents, + transformers: Transformers + ) { + self.site = site + self.themes = themes + self.contents = contents + self.transformers = transformers + } + + init(_ dict: [String: Any]) { + self.site = .init(dict.dict(Keys.site)) + self.themes = .init(dict.dict(Keys.themes)) + self.contents = .init(dict.dict(Keys.contents)) + self.transformers = .init(dict.dict(Keys.transformers)) + } +} + +extension Config { + + static let `defaults` = Config( + site: .init( + baseUrl: "http://localhost:3000/", + title: "", + description: "", + language: nil, + dateFormat: "MMMM dd, yyyy", + noindex: false, + hreflang: [], + userDefined: [:] + ), + themes: .init( + use: "default", + folder: "themes", + assets: .init(folder: "assets"), + templates: .init(folder: "templates"), + types: .init(folder: "types"), + overrides: .init(folder: "overrides") + ), + contents: .init( + folder: "contents", + dateFormat: "yyyy-MM-dd HH:mm:ss", + assets: .init(folder: "assets") + ), + transformers: .init( + folder: "transformers", + pipelines: [] + ) + ) } diff --git a/Sources/ToucanSDK/Source/ConfigLoader.swift b/Sources/ToucanSDK/Source/ConfigLoader.swift index 496b6a95..35029b30 100644 --- a/Sources/ToucanSDK/Source/ConfigLoader.swift +++ b/Sources/ToucanSDK/Source/ConfigLoader.swift @@ -9,84 +9,6 @@ import Foundation import FileManagerKit import Logging -private extension Config { - - enum Keys { - static let site = "site" - static let themes = "themes" - static let content = "content" - static let types = "types" - } - -} - -private extension Config.Location { - - enum Keys { - static let folder = "folder" - } -} - -private extension Config.Site { - - enum Keys: String, CaseIterable { - case baseUrl - case title - case description - case language - case dateFormat - case noindex - case hreflang - } - - enum Defaults { - static let baseUrl = "http://localhost:3000/" - static let title = "" - static let description = "" - static let dateFormat = "MMMM dd, yyyy" - static let noindex = false - } -} - -private extension Config.Themes { - - enum Keys { - static let use = "use" - static let templates = "templates" - static let assets = "assets" - static let overrides = "overrides" - } - - enum Defaults { - static let use = "default" - static let folder = "themes" - static let templatesFolder = "templates" - static let assetsFolder = "assets" - static let overridesFolder = "template_overrides" - } -} - -private extension Config.Types { - - enum Defaults { - static let typesFolder = "types" - } -} - -private extension Config.Content { - - enum Keys { - static let dateFormat = "dateFormat" - static let assets = "assets" - } - - enum Defaults { - static let dateFormat = "yyyy-MM-dd HH:mm:ss" - static let contentFolder = "content" - static let assetsFolder = "assets" - } -} - public struct ConfigLoader { /// An enumeration representing possible errors that can occur while loading the configuration. @@ -120,133 +42,10 @@ public struct ConfigLoader { do { let contents = try FileLoader.yaml.loadContents(at: configUrl) let yaml = try contents.decodeYaml() - return dictToConfig(yaml) + return .init(yaml) } catch FileLoader.Error.missing(let url) { throw Error.missing(url) } } - - func dictToConfig( - _ yaml: [String: Any] - ) -> Config { - // MARK: - site - let site = yaml.dict(Config.Keys.site) - - /// set base url to default value - var baseUrl = Config.Site.Defaults.baseUrl.ensureTrailingSlash() - /// load base url from YAML - if let value = site.string(Config.Site.Keys.baseUrl.rawValue) { - baseUrl = value.ensureTrailingSlash() - } - /// override base url with input - if let value = self.baseUrl { - baseUrl = value.ensureTrailingSlash() - } - - let title = - site.string(Config.Site.Keys.title.rawValue) - ?? Config.Site.Defaults.title - - let description = - site.string(Config.Site.Keys.description.rawValue) - ?? Config.Site.Defaults.description - - let language = site.string(Config.Site.Keys.language.rawValue) - - let dateFormat = - site.string(Config.Site.Keys.dateFormat.rawValue) - ?? Config.Site.Defaults.dateFormat - - let noindex = - site.bool(Config.Site.Keys.noindex.rawValue) - ?? Config.Site.Defaults.noindex - - let hreflang = site.array( - Config.Site.Keys.hreflang.rawValue, - as: Config.Site.Hreflang.self - ) - - let userDefined = site.filter { - !Config.Site.Keys.allCases.map(\.rawValue).contains($0.key) - } - - // MARK: - themes - - let themes = yaml.dict(Config.Keys.themes) - - let use = - themes.string(Config.Themes.Keys.use) ?? Config.Themes.Defaults.use - - let folder = - themes.string(Config.Location.Keys.folder) - ?? Config.Themes.Defaults.folder - - let templates = themes.dict(Config.Themes.Keys.templates) - let templatesFolder = - templates.string(Config.Location.Keys.folder) - ?? Config.Themes.Defaults.templatesFolder - - let assets = themes.dict(Config.Themes.Keys.assets) - let assetsFolder = - assets.string(Config.Location.Keys.folder) - ?? Config.Themes.Defaults.assetsFolder - - let overrides = themes.dict(Config.Themes.Keys.overrides) - let overridesFolder = - overrides.string(Config.Location.Keys.folder) - ?? Config.Themes.Defaults.overridesFolder - - // MARK: - types - - let types = yaml.dict(Config.Keys.types) - let typesFolder = - types.string(Config.Location.Keys.folder) - ?? Config.Types.Defaults.typesFolder - - // MARK: - content - - let content = yaml.dict(Config.Keys.content) - let contentFolder = - content.string(Config.Location.Keys.folder) - ?? Config.Content.Defaults.contentFolder - - let contentDateFormat = - content.string(Config.Content.Keys.dateFormat) - ?? Config.Content.Defaults.dateFormat - - let contentAssets = content.dict(Config.Content.Keys.assets) - let contentAssetsFolder = - contentAssets - .string(Config.Location.Keys.folder) - ?? Config.Content.Defaults.assetsFolder - - // MARK: - config - - return .init( - site: .init( - baseUrl: baseUrl, - title: title, - description: description, - language: language, - dateFormat: dateFormat, - noindex: noindex, - hreflang: hreflang, - userDefined: userDefined - ), - themes: .init( - use: use, - folder: folder, - templates: .init(folder: templatesFolder), - assets: .init(folder: assetsFolder), - overrides: .init(folder: overridesFolder) - ), - types: .init(folder: typesFolder), - content: .init( - folder: contentFolder, - dateFormat: contentDateFormat, - assets: .init(folder: contentAssetsFolder) - ) - ) - } } diff --git a/Sources/ToucanSDK/Source/ContentType.swift b/Sources/ToucanSDK/Source/ContentType.swift index fe657543..3adc3823 100644 --- a/Sources/ToucanSDK/Source/ContentType.swift +++ b/Sources/ToucanSDK/Source/ContentType.swift @@ -109,7 +109,7 @@ extension ContentType { id: "page", rss: nil, location: nil, - template: "pages.single.page", + template: "pages.default", pagination: nil, properties: [:], relations: nil, @@ -131,7 +131,7 @@ extension ContentType { id: "pagination", rss: nil, location: nil, - template: "pages.single.page", + template: "pages.default", pagination: nil, properties: nil, relations: nil, diff --git a/Sources/ToucanSDK/Source/ContentTypeLoader.swift b/Sources/ToucanSDK/Source/ContentTypeLoader.swift index 700ea423..3d784bc1 100644 --- a/Sources/ToucanSDK/Source/ContentTypeLoader.swift +++ b/Sources/ToucanSDK/Source/ContentTypeLoader.swift @@ -28,7 +28,13 @@ struct ContentTypeLoader { /// - Throws: An error if the content types could not be loaded. /// - Returns: An array of `ContentType` objects. func load() throws -> [ContentType] { - let typesUrl = sourceUrl.appendingPathComponent(config.types.folder) + // TODO: use theme override url to load additional / updated types + let typesUrl = + sourceUrl + .appendingPathComponent(config.themes.folder) + .appendingPathComponent(config.themes.use) + .appendingPathComponent(config.themes.types.folder) + let contents = try fileLoader.findContents(at: typesUrl) logger.debug("Loading content type: `\(sourceUrl.absoluteString)`.") diff --git a/Sources/ToucanSDK/Source/PageBundleLoader.swift b/Sources/ToucanSDK/Source/PageBundleLoader.swift index 828c0552..7bd03a81 100644 --- a/Sources/ToucanSDK/Source/PageBundleLoader.swift +++ b/Sources/ToucanSDK/Source/PageBundleLoader.swift @@ -92,8 +92,8 @@ public struct PageBundleLoader { } /// helper - private var contentUrl: URL { - sourceUrl.appendingPathComponent(config.content.folder) + private var contentsUrl: URL { + sourceUrl.appendingPathComponent(config.contents.folder) } /// Loads all the page bundles. @@ -113,7 +113,7 @@ public struct PageBundleLoader { var result: [PageBundleLocation] = [] let p = path.joined(separator: "/") - let url = contentUrl.appendingPathComponent(p) + let url = contentsUrl.appendingPathComponent(p) if containsIndexFile(name: indexName, at: url) { result.append( @@ -227,7 +227,7 @@ public struct PageBundleLoader { guard let date = frontMatter.date( Keys.publication.rawValue, - format: config.content.dateFormat + format: config.contents.dateFormat ) else { return now @@ -238,7 +238,7 @@ public struct PageBundleLoader { func expiration(frontMatter: [String: Any]) -> Date? { frontMatter.date( Keys.expiration.rawValue, - format: config.content.dateFormat + format: config.contents.dateFormat ) } @@ -268,7 +268,7 @@ public struct PageBundleLoader { contentType: ContentType ) -> String { frontMatter.string(Keys.template.rawValue).emptyToNil ?? contentType - .template ?? ContentType.default.template ?? "pages.single.page" + .template ?? ContentType.default.template ?? "pages.default" } func output(frontMatter: [String: Any]) -> String? { @@ -331,7 +331,7 @@ public struct PageBundleLoader { func loadPageBundle( at location: PageBundleLocation ) throws -> PageBundle? { - let dirUrl = contentUrl.appendingPathComponent(location.path) + let dirUrl = contentsUrl.appendingPathComponent(location.path) let metadata: Logger.Metadata = [ "slug": "\(location.slug)" diff --git a/Sources/ToucanSDK/Toucan.swift b/Sources/ToucanSDK/Toucan.swift index 0960eb62..55898f30 100644 --- a/Sources/ToucanSDK/Toucan.swift +++ b/Sources/ToucanSDK/Toucan.swift @@ -72,6 +72,7 @@ public struct Toucan { // TODO: output url is completely wiped, check if it's safe to delete everything try resetOutputDirectory() + /// not sure if we still need absolute url support... let themeUrl: URL if source.config.themes.folder.hasPrefix("/") { themeUrl = URL(fileURLWithPath: source.config.themes.folder) @@ -81,21 +82,23 @@ public struct Toucan { themeUrl = inputUrl .appendingPathComponent(source.config.themes.folder) - .appendingPathComponent(source.config.themes.use) } - let themeAssetsUrl = + let currentThemeUrl = themeUrl + .appendingPathComponent(source.config.themes.use) + + let themeAssetsUrl = + currentThemeUrl .appendingPathComponent(source.config.themes.assets.folder) let themeTemplatesUrl = - themeUrl + currentThemeUrl .appendingPathComponent(source.config.themes.templates.folder) let themeOverrideUrl = - inputUrl + themeUrl .appendingPathComponent(source.config.themes.overrides.folder) - .appendingPathComponent(source.config.themes.use) let themeOverrideAssetsUrl = themeOverrideUrl @@ -105,6 +108,29 @@ public struct Toucan { themeOverrideUrl .appendingPathComponent(source.config.themes.templates.folder) + logger.trace( + "Themes location url: `\(themeUrl.absoluteString)`" + ) + logger.trace( + "Current theme url: `\(currentThemeUrl.absoluteString)`" + ) + logger.trace( + "Current theme assets url: `\(themeAssetsUrl.absoluteString)`" + ) + logger.trace( + "Current theme templates url: `\(themeTemplatesUrl.absoluteString)`" + ) + + logger.trace( + "Theme override url: `\(themeOverrideUrl.absoluteString)`" + ) + logger.trace( + "Theme override assets url: `\(themeOverrideAssetsUrl.absoluteString)`" + ) + logger.trace( + "Theme override templates url: `\(themeOverrideTemplatesUrl.absoluteString)`" + ) + // theme assets try fileManager.copyRecursively( from: themeAssetsUrl,