diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..706eede --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91a8975 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Brian Dinsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..a482468 --- /dev/null +++ b/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "BrianPublishTheme", + products: [ + .library( + name: "BrianPublishTheme", + targets: ["BrianPublishTheme"]), + ], + dependencies: [ + .package(url: "https://github.com/johnsundell/publish.git", from: "0.2.0") + ], + targets: [ + .target( + name: "BrianPublishTheme", + dependencies: ["Publish"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c2695e --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Brian theme for Publish + +A [Publish](https://github.com/johnsundell/publish) theme available to any Publish website. + +## Installation + +To install it into your [Publish](https://github.com/johnsundell/publish) package, add it as a dependency within your `Package.swift` manifest: + +```swift +let package = Package( + ... + dependencies: [ + ... + .package(url: "https://github.com/dinsen/brianpublishtheme", from: "0.1.0") + ], + targets: [ + .target( + ... + dependencies: [ + ... + "BrianPublishTheme" + ] + ) + ] + ... +) +``` + +For more information on how to use the Swift Package Manager, check out [this article](https://www.swiftbysundell.com/articles/managing-dependencies-using-the-swift-package-manager), or [its official documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation). + +## Usage + +The theme can then be used within any publishing pipeline like this: + +```swift +import BrianPublishTheme +... +try Brian().publish(withTheme: .brian) +``` diff --git a/Resources/BrianTheme/styles.css b/Resources/BrianTheme/styles.css new file mode 100644 index 0000000..e3ab363 --- /dev/null +++ b/Resources/BrianTheme/styles.css @@ -0,0 +1,218 @@ +/** +* Publish Brian theme +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +* This file is a modified version of +* Foundation theme included in Publish +*/ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #fff; + color: #000; + font-family: Helvetica, Arial; + text-align: center; +} + +.wrapper { + max-width: 900px; + margin-left: auto; + margin-right: auto; + padding: 40px; + text-align: left; +} + +header { + background-color: #f44336; +} + +header .wrapper { + padding: 128px 16px; + text-align: center; +} + +header a { + text-decoration: none; +} + +header .site-name { + font-size: 1.5em; + color: #000; + font-weight: bold; +} + +nav { + margin-top: 20px; +} + +nav li { + display: inline-block; + padding: 5px; +} + +h1 { + margin-bottom: 20px; + font-size: 2em; +} + +h2 { + margin: 20px 0; +} + +p { + margin-bottom: 10px; +} + +a { + color: inherit; +} + +.description { + margin-bottom: 40px; +} + +.item-list > li { + display: block; + padding: 20px; + margin-top: 20px; + margin-bottom: 20px; + border-radius: 20px; + background-color: #eee; +} + +.item-list h1 { + margin-bottom: 15px; + font-size: 1.3em; +} + +.item-list p { + margin-bottom: 0; +} + +.tag-list { + margin-bottom: 15px; +} + +.tag-list li, +.tag { + display: inline-block; + background-color: #000; + color: #ddd; + padding: 4px 6px; + border-radius: 5px; + margin-right: 5px; +} + +.tag-list a, +.tag a { + text-decoration: none; +} + +.item-page .tag-list { + display: inline-block; +} + +.content { + margin-bottom: 40px; + line-height: 1.5em; +} + +.browse-all { + display: block; + margin-bottom: 30px; +} + +.all-tags li { + font-size: 1.4em; + margin-right: 10px; + padding: 6px 10px; +} + +footer { + color: #8a8a8a; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #222; + } + + body, + header .site-name { + color: #fff; + } + + .item-list > li { + background-color: #333; + } + + header { + background-color: #f44336; + } +} + +/** + * Example CSS file that can be used to style Splash HTML output + * Copyright (c) John Sundell 2018 + * MIT license - see LICENSE.md + */ + +pre { + margin-bottom: 1.5em; + background-color: #1a1a1a; + padding: 16px 0; + border-radius: 16px; +} + +pre code { + font-family: monospace; + display: block; + padding: 0 20px; + color: #a9bcbc; + line-height: 1.4em; + font-size: 0.95em; + overflow-x: auto; + white-space: pre; + -webkit-overflow-scrolling: touch; +} + +pre code .keyword { + color: #e73289; +} + +pre code .type { + color: #8281ca; +} + +pre code .call { + color: #348fe5; +} + +pre code .property { + color: #21ab9d; +} + +pre code .number { + color: #db6f57; +} + +pre code .string { + color: #fa641e; +} + +pre code .comment { + color: #6b8a94; +} + +pre code .dotAccess { + color: #92b300; +} + +pre code .preprocessing { + color: #b68a00; +} diff --git a/Sources/BrianPublishTheme/Theme+Brian.swift b/Sources/BrianPublishTheme/Theme+Brian.swift new file mode 100644 index 0000000..5aac0f4 --- /dev/null +++ b/Sources/BrianPublishTheme/Theme+Brian.swift @@ -0,0 +1,249 @@ +/** +* BrianPublishTheme +* Copyright (c) Brian Dinsen 2020 +* MIT license, see LICENSE file for details +* This file is a modified version of +* Foundation theme included in Publish +*/ + +import Plot +import Publish + +public extension Theme { + + static var brian: Self { + Theme( + htmlFactory: BrianHTMLFactory(), + resourcePaths: ["Resources/BrianTheme/styles.css"] + ) + } +} + +private struct BrianHTMLFactory: HTMLFactory { + func makeIndexHTML(for index: Index, + context: PublishingContext) throws -> HTML { + HTML( + .lang(context.site.language), + .head(for: index, on: context.site), + .body( + .header(for: context, selectedSection: nil), + .wrapper( + .h1(.text(index.title)), + .p( + .class("description"), + .text(context.site.description) + ), + .h2("Latest content"), + .itemList( + for: context.allItems( + sortedBy: \.date, + order: .descending + ), + on: context.site + ) + ), + .footer(for: context.site) + ) + ) + } + + func makeSectionHTML(for section: Section, + context: PublishingContext) throws -> HTML { + HTML( + .lang(context.site.language), + .head(for: section, on: context.site), + .body( + .header(for: context, selectedSection: section.id), + .wrapper( + .h1(.text(section.title)), + .itemList(for: section.items, on: context.site) + ), + .footer(for: context.site) + ) + ) + } + + func makeItemHTML(for item: Item, + context: PublishingContext) throws -> HTML { + HTML( + .lang(context.site.language), + .head(for: item, on: context.site), + .body( + .class("item-page"), + .header(for: context, selectedSection: item.sectionID), + .wrapper( + .article( + .div( + .class("content"), + .contentBody(item.body) + ), + .span("Tagged with: "), + .tagList(for: item, on: context.site) + ) + ), + .footer(for: context.site) + ) + ) + } + + func makePageHTML(for page: Page, + context: PublishingContext) throws -> HTML { + HTML( + .lang(context.site.language), + .head(for: page, on: context.site), + .body( + .header(for: context, selectedSection: nil), + .wrapper( + + ), + .footer(for: context.site) + ) + ) + } + + func makeTagListHTML(for page: TagListPage, + context: PublishingContext) throws -> HTML? { + HTML( + .lang(context.site.language), + .head(for: page, on: context.site), + .body( + .header(for: context, selectedSection: nil), + .wrapper( + .h1("Browse all tags"), + .ul( + .class("all-tags"), + .forEach(page.tags.sorted()) { tag in + .li( + .class("tag"), + .a( + .href(context.site.path(for: tag)), + .text(tag.string) + ) + ) + } + ) + ), + .footer(for: context.site) + ) + ) + } + + func makeTagDetailsHTML(for page: TagDetailsPage, + context: PublishingContext) throws -> HTML? { + HTML( + .lang(context.site.language), + .head(for: page, on: context.site), + .body( + .header(for: context, selectedSection: nil), + .wrapper( + .h1( + "Tagged with ", + .span(.class("tag"), .text(page.tag.string)) + ), + .a( + .class("browse-all"), + .text("Browse all tags"), + .href(context.site.tagListPath) + ), + .itemList( + for: context.items( + taggedWith: page.tag, + sortedBy: \.date, + order: .descending + ), + on: context.site + ) + ), + .footer(for: context.site) + ) + ) + } +} + +private extension Node where Context == HTML.BodyContext { + static func wrapper(_ nodes: Node...) -> Node { + .div(.class("wrapper"), .group(nodes)) + } + + static func header( + for context: PublishingContext, + selectedSection: T.SectionID? + ) -> Node { + let sectionIDs = T.SectionID.allCases + + return .header( + .wrapper( + .a(.class("site-name"), .href("/"), .text(context.site.name)), + .if(sectionIDs.count > 1, + .nav( + .ul(.forEach(sectionIDs) { section in + .li( + .a( + .class(section == selectedSection ? "selected" : ""), + .href(context.sections[section].path), + .text(context.sections[section].title) + ) + ) + }) + ) + ) + ) + ) + } + + static func itemList(for items: [Item], on site: T) -> Node { + return .ul( + .class("item-list"), + .forEach(items) { item in + .li(.article( + .h1(.a( + .href(item.path), + .text(item.title) + )), + .tagList(for: item, on: site), + .p(.text(item.description)) + )) + } + ) + } + + static func tagList(for item: Item, on site: T) -> Node { + return .ul(.class("tag-list"), .forEach(item.tags) { tag in + .li(.a( + .href(site.path(for: tag)), + .text(tag.string) + )) + }) + } + + static func footer(for site: T) -> Node { + return .footer( + .p( + .text("Generated with ❤️ using "), + .a( + .text("Publish"), + .href("https://github.com/johnsundell/publish") + ) + ), + .p( + .a( + .text("RSS feed"), + .href("/feed.rss") + ), + .text(" | "), + .a( + .text("Twitter"), + .href("https://twitter.com/briandinsen"), + .target(.blank) + ), + .text(" | "), + .a( + .text("GitHub"), + .href("https://github.com/dinsen"), + .target(.blank) + ) + ) + ) + } +} + diff --git a/Tests/BrianPublishThemeTests/BrianPublishThemeTests.swift b/Tests/BrianPublishThemeTests/BrianPublishThemeTests.swift new file mode 100644 index 0000000..aa58a41 --- /dev/null +++ b/Tests/BrianPublishThemeTests/BrianPublishThemeTests.swift @@ -0,0 +1,6 @@ +import XCTest +@testable import BrianPublishTheme + +final class BrianPublishThemeTests: XCTestCase { + +} diff --git a/Tests/BrianPublishThemeTests/XCTestManifests.swift b/Tests/BrianPublishThemeTests/XCTestManifests.swift new file mode 100644 index 0000000..14d3d57 --- /dev/null +++ b/Tests/BrianPublishThemeTests/XCTestManifests.swift @@ -0,0 +1,2 @@ +import XCTest + diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..ef772b7 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import BrianPublishThemeTests + +//var tests = [XCTestCaseEntry]() +//tests += BrianPublishThemeTests.allTests() +//XCTMain(tests)