├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── DarkImagePublishPlugin │ └── DarkImagePublishPlugin.swift ├── Tests ├── DarkImagePublishPluginTests │ ├── DarkImagePublishPluginTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Guilherme Rambo 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "codextended", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/johnsundell/codextended.git", 7 | "state" : { 8 | "revision" : "8d7c46dfc9c55240870cf5561d6cefa41e3d7105", 9 | "version" : "0.3.0" 10 | } 11 | }, 12 | { 13 | "identity" : "collectionconcurrencykit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/johnsundell/collectionConcurrencyKit.git", 16 | "state" : { 17 | "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", 18 | "version" : "0.2.0" 19 | } 20 | }, 21 | { 22 | "identity" : "files", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/johnsundell/files.git", 25 | "state" : { 26 | "revision" : "d273b5b7025d386feef79ef6bad7de762e106eaf", 27 | "version" : "4.2.0" 28 | } 29 | }, 30 | { 31 | "identity" : "ink", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/johnsundell/ink.git", 34 | "state" : { 35 | "revision" : "77c3d8953374a9cf5418ef0bd7108524999de85a", 36 | "version" : "0.5.1" 37 | } 38 | }, 39 | { 40 | "identity" : "plot", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/johnsundell/plot.git", 43 | "state" : { 44 | "revision" : "b358860fe565eb53e98b1f5807eb5939c8124547", 45 | "version" : "0.11.0" 46 | } 47 | }, 48 | { 49 | "identity" : "publish", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/johnsundell/Publish.git", 52 | "state" : { 53 | "revision" : "1c8ad00d39c985cb5d497153241a2f1b654e0d40", 54 | "version" : "0.9.0" 55 | } 56 | }, 57 | { 58 | "identity" : "shellout", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/johnsundell/shellout.git", 61 | "state" : { 62 | "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", 63 | "version" : "2.3.0" 64 | } 65 | }, 66 | { 67 | "identity" : "sweep", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/johnsundell/sweep.git", 70 | "state" : { 71 | "revision" : "801c2878e4c6c5baf32fe132e1f3f3af6f9fd1b0", 72 | "version" : "0.4.0" 73 | } 74 | } 75 | ], 76 | "version" : 2 77 | } 78 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DarkImagePublishPlugin", 7 | platforms: [.macOS(.v12)], 8 | products: [ 9 | .library( 10 | name: "DarkImagePublishPlugin", 11 | targets: ["DarkImagePublishPlugin"] 12 | ) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/johnsundell/Publish.git", from: "0.9.0") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "DarkImagePublishPlugin", 20 | dependencies: ["Publish"] 21 | ), 22 | .testTarget( 23 | name: "DarkImagePublishPluginTests", 24 | dependencies: ["DarkImagePublishPlugin"] 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DarkImagePublishPlugin 😎 2 | 3 | A plugin for [Publish](https://github.com/JohnSundell/Publish) that lets you have two variants for images on your site: one for light mode, and one for dark mode. This is currently used in [rambo.codes](https://rambo.codes). 4 | 5 | ![demo](./demo.gif) 6 | 7 | ## How to use it 8 | 9 | Just use the regular markdown syntax for images and the plugin takes care of the rest, so that the following markdown: 10 | 11 | ```markdown 12 | ![some image](/assets/img/2/1.png) 13 | ``` 14 | 15 | Becomes this in HTML: 16 | 17 | ```html 18 |
19 | 20 | 21 | some image 22 | 23 |
24 | ``` 25 | 26 | ## Installing the plugin 27 | 28 | To install the plugin, add it to your site's publishing steps: 29 | 30 | ```swift 31 | try mysite().publish(using: [ 32 | .installPlugin(.darkImage()), 33 | // ... 34 | ]) 35 | ``` 36 | 37 | You can customize the suffix that's used for the dark variant by passing the `suffix` parameter: 38 | 39 | ```swift 40 | try mysite().publish(using: [ 41 | .installPlugin(.darkImage(suffix: "bestmode")), 42 | // ... 43 | ]) 44 | ``` 45 | 46 | ## Light-only images 47 | 48 | In some cases, you might have just a single variant of an image, so trying to load the dark variant would fail. If you have an image with only a single variant, add the `?nodark` suffix to your image's path/URL: 49 | 50 | ```markdown 51 | ![some image](/assets/img/2/1.png?nodark) 52 | ``` 53 | 54 | In that case, the generated HTML will look like this: 55 | 56 | ```html 57 |
58 | 59 | some image 60 | 61 |
62 | ``` -------------------------------------------------------------------------------- /Sources/DarkImagePublishPlugin/DarkImagePublishPlugin.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * Dark image plugin for Publish 3 | * © 2020 Guilherme Rambo 4 | * BSD-2 license, see LICENSE file for details 5 | */ 6 | 7 | import Publish 8 | import Ink 9 | 10 | public extension Plugin { 11 | static func darkImage(suffix: String = "-dark") -> Self { 12 | Plugin(name: "DarkImage") { context in 13 | context.markdownParser.addModifier( 14 | .darkImage(suffix: suffix) 15 | ) 16 | } 17 | } 18 | } 19 | 20 | public extension Modifier { 21 | private static let noDarkMarker = "?nodark" 22 | 23 | static func darkImage(suffix: String) -> Self { 24 | return Modifier(target: .images) { html, markdown in 25 | let lightOnly = markdown.contains(Self.noDarkMarker) 26 | let input = markdown.replacingOccurrences(of: Self.noDarkMarker, with: "") 27 | 28 | let path = input.dropFirst("![".count).dropLast(")".count).drop(while: { $0 != "(" }).dropFirst() 29 | 30 | guard let dotIndex = path.lastIndex(of: ".") else { return html } 31 | 32 | var darkPath = path 33 | darkPath.insert(contentsOf: suffix, at: dotIndex) 34 | 35 | var altSuffix = "" 36 | if let alt = input.firstSubstring(between: "[", and: "]") { 37 | altSuffix = " alt=\"\(alt)\"" 38 | } 39 | 40 | if lightOnly { 41 | return """ 42 |
43 | 44 | 45 | 46 |
47 | """ 48 | } else { 49 | return """ 50 |
51 | 52 | 53 | 54 | 55 |
56 | """ 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/DarkImagePublishPluginTests/DarkImagePublishPluginTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import DarkImagePublishPlugin 3 | import Ink 4 | 5 | final class DarkImagePublishPluginTests: XCTestCase { 6 | func testGeneratingFigureTagsForImage() { 7 | let parser = MarkdownParser(modifiers: [.darkImage(suffix: "-dark")]) 8 | let html = parser.html(from: "![some image](/assets/img/2/1.png)") 9 | 10 | XCTAssertEqual(html, """ 11 |
12 | 13 | 14 | some image 15 | 16 |
17 | """) 18 | } 19 | 20 | func testNoDarkMarker() { 21 | let parser = MarkdownParser(modifiers: [.darkImage(suffix: "-dark")]) 22 | let html = parser.html(from: "![some image](/assets/img/2/1.png?nodark)") 23 | 24 | XCTAssertEqual(html, """ 25 |
26 | 27 | some image 28 | 29 |
30 | """) 31 | } 32 | 33 | static var allTests = [ 34 | ("testGeneratingFigureTagsForImage", testGeneratingFigureTagsForImage), 35 | ("testNoDarkMarker", testNoDarkMarker), 36 | ] 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Tests/DarkImagePublishPluginTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(DarkImagePublishPluginTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DarkImagePublishPluginTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DarkImagePublishPluginTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/insidegui/DarkImagePublishPlugin/c5d6b3bbe4e434cad20cd67c3ff2c12b99e98f0b/demo.gif --------------------------------------------------------------------------------