Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate HTML generator produces unique files #142

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
84 changes: 57 additions & 27 deletions Sources/Publish/Internal/HTMLGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,32 @@ internal struct HTMLGenerator<Site: Website> {
let context: PublishingContext<Site>

func generate() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { try await copyThemeResources() }
group.addTask { try generateIndexHTML() }
group.addTask { try await generateSectionHTML() }
group.addTask { try await generatePageHTML() }
group.addTask { try await generateTagHTMLIfNeeded() }

// Throw any errors generated by the above set of operations:
for try await _ in group {}
try await withThrowingTaskGroup(of: (substep: String, paths: [Path]).self) { group in
group.addTask { (substep: "Copy theme resources", paths: try await copyThemeResources()) }
group.addTask { (substep: "Generate index", paths: try generateIndexHTML()) }
group.addTask { (substep: "Generate sections", paths: try await generateSectionHTML()) }
group.addTask { (substep: "Generate pages", paths: try await generatePageHTML()) }
group.addTask { (substep: "Generate tags", paths: try await generateTagHTMLIfNeeded()) }

try await validate(group)
}
}
}

private extension HTMLGenerator {
func copyThemeResources() async throws {
func copyThemeResources() async throws -> [Path] {
guard !theme.resourcePaths.isEmpty else {
return
return []
}

let creationFile = try File(path: theme.creationPath.string)
let packageFolder = try creationFile.resolveSwiftPackageFolder()

try await theme.resourcePaths.concurrentForEach { path in
return try await theme.resourcePaths.concurrentMap { path -> Path in
do {
let file = try packageFolder.file(at: path.string)
try context.copyFileToOutput(file, targetFolderPath: nil)
return path
} catch {
throw PublishingError(
path: path,
Expand All @@ -51,34 +51,42 @@ private extension HTMLGenerator {
}
}

func generateIndexHTML() throws {
func generateIndexHTML() throws -> [Path] {
let html = try theme.makeIndexHTML(context.index, context)
let indexFile = try context.createOutputFile(at: "index.html")
let path = Path("index.html")
let indexFile = try context.createOutputFile(at: path)
try indexFile.write(html.render(indentedBy: indentation))
return [path]
}

func generateSectionHTML() async throws {
try await context.sections.concurrentForEach { section in
try outputHTML(
func generateSectionHTML() async throws -> [Path] {
try await context.sections.concurrentFlatMap { section -> [Path] in
var allPaths = [Path]()

let sectionPath = try outputHTML(
for: section,
indentedBy: indentation,
using: theme.makeSectionHTML,
fileMode: .foldersAndIndexFiles
)

try await section.items.concurrentForEach { item in
allPaths.append(sectionPath)

let sectionItemPaths = try await section.items.concurrentMap { item -> Path in
try outputHTML(
for: item,
indentedBy: indentation,
using: theme.makeItemHTML,
fileMode: fileMode
)
}

allPaths.append(contentsOf: sectionItemPaths)
return allPaths
}
}

func generatePageHTML() async throws {
try await context.pages.values.concurrentForEach { page in
func generatePageHTML() async throws -> [Path] {
try await context.pages.values.concurrentMap { page -> Path in
try outputHTML(
for: page,
indentedBy: indentation,
Expand All @@ -88,9 +96,9 @@ private extension HTMLGenerator {
}
}

func generateTagHTMLIfNeeded() async throws {
func generateTagHTMLIfNeeded() async throws -> [Path] {
guard let config = context.site.tagHTMLConfig else {
return
return []
}

let listPage = TagListPage(
Expand All @@ -99,13 +107,15 @@ private extension HTMLGenerator {
content: config.listContent ?? .init()
)

var allPaths = [Path]()
if let listHTML = try theme.makeTagListHTML(listPage, context) {
let listPath = Path("\(config.basePath)/index.html")
let listFile = try context.createOutputFile(at: listPath)
try listFile.write(listHTML.render(indentedBy: indentation))
allPaths.append(listPath)
}

try await context.allTags.concurrentForEach { tag in
let tagPaths: [Path] = try await context.allTags.concurrentCompactMap { tag -> Path? in
let detailsPath = context.site.path(for: tag)
let detailsContent = config.detailsContentResolver(tag)

Expand All @@ -116,28 +126,48 @@ private extension HTMLGenerator {
)

guard let detailsHTML = try theme.makeTagDetailsHTML(detailsPage, context) else {
return
return nil
}

try outputHTML(
return try outputHTML(
for: detailsPage,
indentedBy: indentation,
using: { _, _ in detailsHTML },
fileMode: fileMode
)
}

allPaths.append(contentsOf: tagPaths)
return allPaths
}

func validate(_ group: ThrowingTaskGroup<(substep: String, paths: [Path]), Error>) async throws {
var pathSubsteps = [Path: [String]]()
for try await substepAndPaths in group {
for path in substepAndPaths.paths {
if let previousSubsteps = pathSubsteps[path] {
let substeps = previousSubsteps.appending(substepAndPaths.substep)
throw PublishingError(
path: path,
infoMessage: "Path conflict in substeps: \(substeps)"
)
}
pathSubsteps[path, default: []].append(substepAndPaths.substep)
}
}
}

func outputHTML<T: Location>(
for location: T,
indentedBy indentation: Indentation.Kind?,
using generator: (T, PublishingContext<Site>) throws -> HTML,
fileMode: HTMLFileMode
) throws {
) throws -> Path {
let html = try generator(location, context)
let path = filePath(for: location, fileMode: fileMode)
let file = try context.createOutputFile(at: path)
try file.write(html.render(indentedBy: indentation))
return path
}

func filePath(for location: Location, fileMode: HTMLFileMode) -> Path {
Expand Down
25 changes: 25 additions & 0 deletions Tests/PublishTests/Tests/HTMLGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,31 @@ final class HTMLGenerationTests: PublishTestCase {
)
}

func testGeneratingConflictingFilesThrowsError() throws {
let folder = try Folder.createTemporary()

var thrownError: PublishingError?
do {
try publishWebsite(
in: folder,
using: [
.addMarkdownFiles(),
.generateHTML(withTheme: .foundation)
],
content: [
// This file has the same name as the `WebsiteStub.SectionID.one` case, which
// causes multiple outputs at the same location.
"one.md": "# One content",
]
)
} catch {
thrownError = error as? PublishingError
}

let path = try require(thrownError?.path)
XCTAssertEqual(path, "one/index.html")
}

func testFoundationTheme() throws {
let folder = try Folder.createTemporary()

Expand Down