Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/Publish/API/PublishingContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ internal extension PublishingContext {

func copyFileToOutput(_ file: File,
targetFolderPath: Path?) throws {
guard !site.shouldIgnore(file) else { return }
try copyLocationToOutput(
file,
targetFolderPath: targetFolderPath,
Expand All @@ -302,6 +303,7 @@ internal extension PublishingContext {

func copyFolderToOutput(_ folder: Folder,
targetFolderPath: Path?) throws {
guard !site.shouldIgnore(folder) else { return }
try copyLocationToOutput(
folder,
targetFolderPath: targetFolderPath,
Expand Down Expand Up @@ -349,6 +351,7 @@ private extension PublishingContext {
targetFolderPath: Path?,
errorReason: FileIOError.Reason
) throws {
guard !site.shouldIgnore(location) else { return }
let targetFolder = try targetFolderPath.map {
try createOutputFolder(at: $0)
}
Expand Down
42 changes: 42 additions & 0 deletions Sources/Publish/API/Website.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import Foundation
import Plot
import Files

/// Protocol that all `Website.SectionID` implementations must conform to.
public protocol WebsiteSectionID: Decodable, Hashable, CaseIterable, RawRepresentable where RawValue == String {}
Expand Down Expand Up @@ -40,6 +41,12 @@ public protocol Website {
/// The configuration to use when generating tag HTML for the website.
/// If this is `nil`, then no tag HTML will be generated.
var tagHTMLConfig: TagHTMLConfiguration? { get }
/// File or folder names to exclude when publishing the site. Regular expressions may be used.
/// - Bare strings with no regexp-related characters will be matched exactly. For example `"templates"` will ignore anything named `templates` but not something named `templates1` or `my templates`, etc.
/// - Wildcards are allowed and follow usual regular expression meanings. For example, `templates.*` means to ignore anything that starts with `templates` and includes zero or more characters after `templates`.
/// - It's not necessary to use `^` or `$` to indicate the start or end of a regular expression, but it's not harmful either.
/// If a folder is ignored, everything in that folder will be ignored, regardless of whether the folder contents match anything in `ignoredPaths`.
var ignoredPaths: [String]? { get }
}

// MARK: - Defaults
Expand Down Expand Up @@ -154,4 +161,39 @@ public extension Website {
func url(for location: Location) -> URL {
url(for: location.path)
}

/// Make `ignoredPaths` optional.
var ignoredPaths: [String]? { nil }

/// Should the site ignore a file or folder with a specific name?
/// - Parameter name: Name of a file or folder, commonly `file.name` or `folder.name`.
/// - Returns: True if the web site should ignore the file/folder name
///
/// Sites can indicate file/folder names to ignore using the `ignoredPaths` property. See that property for a discussion of how to use it.
///
/// This function checks only strings and should be called with a file or folder name.
func shouldIgnore(name: String) -> Bool {
guard let ignoredPaths = ignoredPaths else { return false }
return !ignoredPaths.filter({
// Add `^` and `$` to the ends of the pattern to avoid unexpected matches. Matching something like "foo" anywhere in a filename requires wildcards, e.g. ".*foo.*". Extra line start/end markers don't affect matching, so there's no need to check for them before trying the pattern.
name.range(of: "^\($0)$", options: .regularExpression) != nil
}).isEmpty
}

/// Should the site ignore a `File` or `Folder`?
/// - Returns: True if the site should ignore the `File`/`Folder`.
///
/// Sites can indicate file/folder names to ignore using the `ignoredPaths` property. See that property for a discussion of how to use it.
///
/// *This function checks all path components for the location.* A file or folder that doesn't match the ignore list could still be contained in a folder that does match. This function deals with this by checking all path components against the `ignoredPaths` list and returning true if any matches are found. If you want to check a file or folder's name without checking the entire path, use `shouldIgnore(name:)`
func shouldIgnore<T: Files.Location>(_ location: T) -> Bool {
!location.pathComponents.filter({ shouldIgnore(name:$0) }).isEmpty
}
}

extension Files.Location {
// Used in `shouldIgnore<T: Files.Location>(_ location)`
var pathComponents: [String] {
path.split(separator: "/").map { String($0) }
}
}
3 changes: 3 additions & 0 deletions Sources/Publish/Internal/MarkdownFileHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal struct MarkdownFileHandler<Site: Website> {
}

for subfolder in folder.subfolders {
guard !context.site.shouldIgnore(subfolder) else { continue }
guard let sectionID = Site.SectionID(rawValue: subfolder.name.lowercased()) else {
try addPagesForMarkdownFiles(
inFolder: subfolder,
Expand All @@ -35,6 +36,7 @@ internal struct MarkdownFileHandler<Site: Website> {
}

for file in subfolder.files.recursive {
guard !context.site.shouldIgnore(file) else { continue }
guard file.isMarkdown else { continue }

if file.nameExcludingExtension == "index", file.parent == subfolder {
Expand Down Expand Up @@ -86,6 +88,7 @@ private extension MarkdownFileHandler {
factory: MarkdownContentFactory<Site>
) throws {
for file in folder.files {
guard !context.site.shouldIgnore(file) else { continue }
guard file.isMarkdown else { continue }

if file.nameExcludingExtension == "index", !recursively {
Expand Down
50 changes: 50 additions & 0 deletions Tests/PublishTests/Tests/WebsiteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import XCTest
import Publish
import Files

final class WebsiteTests: PublishTestCase {
private var website: WebsiteStub.WithoutItemMetadata!
Expand Down Expand Up @@ -81,4 +82,53 @@ final class WebsiteTests: PublishTestCase {
URL(string: "https://swiftbysundell.com/mypage")
)
}

func testIgnoreStrings() {
XCTAssertTrue(website.shouldIgnore(name: "templates"))
XCTAssertFalse(website.shouldIgnore(name: "templates1"))
XCTAssertFalse(website.shouldIgnore(name: "@templates"))

XCTAssertTrue(website.shouldIgnore(name: "skip-this-file.md"))
XCTAssertTrue(website.shouldIgnore(name: "skip-this-file-too.png"))
XCTAssertTrue(website.shouldIgnore(name: "skip-this-file"))
XCTAssertFalse(website.shouldIgnore(name: "dont-skip-this-file"))

XCTAssertTrue(website.shouldIgnore(name: "notes"))
XCTAssertFalse(website.shouldIgnore(name: "notes1"))

XCTAssertTrue(website.shouldIgnore(name: "batter"))
XCTAssertTrue(website.shouldIgnore(name: "butter"))
XCTAssertFalse(website.shouldIgnore(name: "butter1"))

XCTAssertTrue(website.shouldIgnore(name: "lisp"))
XCTAssertTrue(website.shouldIgnore(name: "lip"))
XCTAssertTrue(website.shouldIgnore(name: "lp"))
XCTAssertTrue(website.shouldIgnore(name: "l1234567890p"))
XCTAssertFalse(website.shouldIgnore(name: "lisp-too"))
}

func testIgnoreFiles() {
// Set up a couple of real files to test ignore-matching on a File, since they require existing files.
let folder: Folder! = try! Folder.temporary.createSubfolderIfNeeded(withName: ".publishTest")
let includeFile = try! folder.createFile(named: "include.txt")
XCTAssertFalse(website.shouldIgnore(includeFile))

let ignoreFile = try! folder.createFile(named: "ignore.txt")
XCTAssertTrue(website.shouldIgnore(ignoreFile))

try? folder.delete()
}
}

extension Website {
var ignoredPaths: [String]? { [
"templates", // Should only match the exact string
"skip-this-file.*", // Should match anyhing starting with "skip-this-file" and 0 or more chars after that
"^notes$", // Should match "notes" exactly and nothing else. Included because `shouldIgnore` adds a "^" and "$", which should not be affected by including those in the pattern.
"b.tter",
"l.*p",
// (#file as NSString).lastPathComponent
"ignore.txt"
] }
}