diff --git a/Sources/Generator/Generators/PageContentGenerator.swift b/Sources/Generator/Generators/PageContentGenerator.swift index 82dc891..628ee67 100644 --- a/Sources/Generator/Generators/PageContentGenerator.swift +++ b/Sources/Generator/Generators/PageContentGenerator.swift @@ -4,6 +4,32 @@ import Splash struct PageContentGenerator { + private let factory: TemplateFactory + + private let siteRoot: Element + + private let results: GenerationResultsHandler + + init(factory: TemplateFactory, siteRoot: Element, results: GenerationResultsHandler) { + self.factory = factory + self.siteRoot = siteRoot + self.results = results + } + + func generate(page: Element, language: String, content: String) -> (content: String, headers: RequiredHeaders) { + let parser = PageContentParser( + factory: factory, + siteRoot: siteRoot, + results: results, + page: page, + language: language) + return parser.generatePage(from: content) + } + +} + +final class PageContentParser { + private let pageLinkMarker = "page:" private let largeImageIndicator = "*large*" @@ -16,42 +42,52 @@ struct PageContentGenerator { private let results: GenerationResultsHandler - init(factory: TemplateFactory, siteRoot: Element, results: GenerationResultsHandler) { + private let page: Element + + private let language: String + + private var headers = RequiredHeaders() + + private var largeImageCount: Int = 0 + + init(factory: TemplateFactory, siteRoot: Element, results: GenerationResultsHandler, page: Element, language: String) { self.factory = factory self.siteRoot = siteRoot self.results = results + self.page = page + self.language = language } - func generate(page: Element, language: String, content: String) -> (content: String, includesCode: Bool) { - var hasCodeContent = false - var largeImageCount = 0 + func generatePage(from content: String) -> (content: String, headers: RequiredHeaders) { + headers = .init() let imageModifier = Modifier(target: .images) { html, markdown in - processMarkdownImage(markdown: markdown, html: html, page: page, language: language, largeImageCount: &largeImageCount) + self.processMarkdownImage(markdown: markdown, html: html) } let codeModifier = Modifier(target: .codeBlocks) { html, markdown in if markdown.starts(with: "```swift") { let code = markdown.between("```swift", and: "```").trimmed - return "
" + swift.highlight(code) + "
" + return "
" + self.swift.highlight(code) + "
" } - hasCodeContent = true + self.headers.insert(.codeHightlighting) return html } let linkModifier = Modifier(target: .links) { html, markdown in - handleLink(page: page, language: language, html: html, markdown: markdown) + self.handleLink(html: html, markdown: markdown) } let htmlModifier = Modifier(target: .html) { html, markdown in - handleHTML(page: page, language: language, html: html, markdown: markdown) + self.handleHTML(html: html, markdown: markdown) } let headlinesModifier = Modifier(target: .headings) { html, markdown in - handleHeadlines(page: page, language: language, html: html, markdown: markdown) + self.handleHeadlines(html: html, markdown: markdown) } let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier, htmlModifier, headlinesModifier]) - return (parser.html(from: content), hasCodeContent) + return (parser.html(from: content), headers) + } - private func handleLink(page: Element, language: String, html: String, markdown: Substring) -> String { + private func handleLink(html: String, markdown: Substring) -> String { let file = markdown.between("(", and: ")") if file.hasPrefix(pageLinkMarker) { let textToChange = file.dropAfterFirst("#") @@ -72,7 +108,7 @@ struct PageContentGenerator { return html } - private func handleHTML(page: Element, language: String, html: String, markdown: Substring) -> String { + private func handleHTML(html: String, markdown: Substring) -> String { // TODO: Check HTML code in markdown for required resources //print("[HTML] Found in page \(page.path):") //print(markdown) @@ -83,7 +119,7 @@ struct PageContentGenerator { return html } - private func handleHeadlines(page: Element, language: String, html: String, markdown: Substring) -> String { + private func handleHeadlines(html: String, markdown: Substring) -> String { let id = markdown .last(after: "#") .trimmed @@ -96,7 +132,7 @@ struct PageContentGenerator { return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">") } - private func processMarkdownImage(markdown: Substring, html: String, page: Element, language: String, largeImageCount: inout Int) -> String { + private func processMarkdownImage(markdown: Substring, html: String) -> String { // Split the markdown ![alt](file title) // There are several known shorthand commands // For images: ![*large* left_title](file right_title) @@ -113,7 +149,7 @@ struct PageContentGenerator { } let alt = markdown.between("[", and: "]").nonEmpty?.removingPercentEncoding if let alt = alt, let command = ShorthandMarkdownKey(rawValue: alt) { - return handleShortHandCommand(command, page: page, language: language, content: fileAndTitle) + return handleShortHandCommand(command, content: fileAndTitle) } let file = fileAndTitle.dropAfterFirst(" ") @@ -121,39 +157,39 @@ struct PageContentGenerator { let fileExtension = file.lastComponentAfter(".").lowercased() if let _ = ImageType(fileExtension: fileExtension) { - return handleImage(page: page, file: file, rightTitle: title, leftTitle: alt, largeImageCount: &largeImageCount) + return handleImage(file: file, rightTitle: title, leftTitle: alt, largeImageCount: &largeImageCount) } if let _ = VideoType(rawValue: fileExtension) { - return handleVideo(page: page, file: file, optionString: alt) + return handleVideo(file: file, optionString: alt) } switch fileExtension { case "svg": - return handleSvg(page: page, file: file, area: alt) + return handleSvg(file: file, area: alt) case "gif": - return handleGif(page: page, file: file, altText: alt ?? "gif image") + return handleGif(file: file, altText: alt ?? "gif image") default: - return handleFile(page: page, file: file, fileExtension: fileExtension) + return handleFile(file: file, fileExtension: fileExtension) } } - private func handleShortHandCommand(_ command: ShorthandMarkdownKey, page: Element, language: String, content: String) -> String { + private func handleShortHandCommand(_ command: ShorthandMarkdownKey, content: String) -> String { switch command { case .downloadButtons: - return handleDownloadButtons(page: page, content: content) + return handleDownloadButtons(content: content) case .externalLink: - return handleExternalButtons(page: page, content: content) + return handleExternalButtons(content: content) case .includedHtml: - return handleExternalHTML(page: page, file: content) + return handleExternalHTML(file: content) case .box: - return handleSimpleBox(page: page, content: content) + return handleSimpleBox(content: content) case .pageLink: - return handlePageLink(page: page, language: language, pageId: content) + return handlePageLink(pageId: content) case .model3d: - return handle3dModel(page: page, content: content) + return handle3dModel(content: content) } } - private func handleImage(page: Element, file: String, rightTitle: String?, leftTitle: String?, largeImageCount: inout Int) -> String { + private func handleImage(file: String, rightTitle: String?, leftTitle: String?, largeImageCount: inout Int) -> String { let imagePath = page.pathRelativeToRootForContainedInputFile(file) let left: String let createFullScreenVersion: Bool @@ -194,7 +230,7 @@ struct PageContentGenerator { return factory.largeImage.generate(content) } - private func handleVideo(page: Element, file: String, optionString: String?) -> String { + private func handleVideo(file: String, optionString: String?) -> String { let options: [PageVideoTemplate.VideoOption] = optionString.unwrapped { string in string.components(separatedBy: " ").compactMap { optionText -> PageVideoTemplate.VideoOption? in guard let optionText = optionText.trimmed.nonEmpty else { @@ -245,7 +281,7 @@ struct PageContentGenerator { } } - private func handleGif(page: Element, file: String, altText: String) -> String { + private func handleGif(file: String, altText: String) -> String { let imagePath = page.pathRelativeToRootForContainedInputFile(file) results.require(file: imagePath, source: page.path) @@ -257,7 +293,7 @@ struct PageContentGenerator { return factory.html.image(file: file, width: width, height: height, altText: altText) } - private func handleSvg(page: Element, file: String, area: String?) -> String { + private func handleSvg(file: String, area: String?) -> String { let imagePath = page.pathRelativeToRootForContainedInputFile(file) results.require(file: imagePath, source: page.path) @@ -294,12 +330,12 @@ struct PageContentGenerator { return factory.html.svgImage(file: file, part: part, altText: altText) } - private func handleFile(page: Element, file: String, fileExtension: String) -> String { + private func handleFile(file: String, fileExtension: String) -> String { results.warning("Unhandled file \(file) with extension \(fileExtension)", source: page.path) return "" } - private func handleDownloadButtons(page: Element, content: String) -> String { + private func handleDownloadButtons(content: String) -> String { let buttons = content .components(separatedBy: ";") .compactMap { button -> (file: String, text: String, downloadName: String?)? in @@ -321,7 +357,7 @@ struct PageContentGenerator { return factory.html.downloadButtons(buttons) } - private func handleExternalButtons(page: Element, content: String) -> String { + private func handleExternalButtons(content: String) -> String { let buttons = content .components(separatedBy: ";") .compactMap { button -> (url: String, text: String)? in @@ -341,12 +377,12 @@ struct PageContentGenerator { return factory.html.externalButtons(buttons) } - private func handleExternalHTML(page: Element, file: String) -> String { + private func handleExternalHTML(file: String) -> String { let path = page.pathRelativeToRootForContainedInputFile(file) return results.getContentOfRequiredFile(at: path, source: page.path) ?? "" } - private func handleSimpleBox(page: Element, content: String) -> String { + private func handleSimpleBox(content: String) -> String { let parts = content.components(separatedBy: ";") guard parts.count > 1 else { results.warning("Invalid box specification", source: page.path) @@ -357,7 +393,7 @@ struct PageContentGenerator { return factory.makePlaceholder(title: title, text: text) } - private func handlePageLink(page: Element, language: String, pageId: String) -> String { + private func handlePageLink(pageId: String) -> String { guard let linkedPage = siteRoot.find(pageId) else { // Checking the page path will add it to the missing pages _ = results.getPagePath(for: pageId, source: page.path, language: language) @@ -403,7 +439,7 @@ struct PageContentGenerator { return factory.pageLink.generate(content) } - private func handle3dModel(page: Element, content: String) -> String { + private func handle3dModel(content: String) -> String { let parts = content.components(separatedBy: ";") guard parts.count > 1 else { results.warning("Invalid 3d model specification", source: page.path) @@ -419,6 +455,9 @@ struct PageContentGenerator { let filePath = page.pathRelativeToRootForContainedInputFile(file) results.require(file: filePath, source: page.path) + // Add required file to head + headers.insert(.modelViewer) + let description = parts.dropFirst().joined(separator: ";") return """ diff --git a/Sources/Generator/Generators/PageGenerator.swift b/Sources/Generator/Generators/PageGenerator.swift index 56347ad..51947b5 100644 --- a/Sources/Generator/Generators/PageGenerator.swift +++ b/Sources/Generator/Generators/PageGenerator.swift @@ -24,12 +24,12 @@ struct PageGenerator { let inputContentPath = page.path + "/\(language).md" let metadata = page.localized(for: language) let nextLanguage = page.nextLanguage(for: language) - let (pageContent, pageIncludesCode) = makeContent( + let (pageContent, additionalHeaders) = makeContent( page: page, metadata: metadata, language: language, path: inputContentPath) var content = [PageTemplate.Key : String]() content[.language] = language - content[.head] = factory.pageHead.generate(page: page, language: language, includesCode: pageIncludesCode) + content[.head] = factory.pageHead.generate(page: page, language: language, headers: additionalHeaders) let sectionUrl = page.sectionUrl(for: language) content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage, page: page) content[.contentClass] = "content" @@ -42,7 +42,7 @@ struct PageGenerator { content[.nextPageUrl] = navLink(from: page, to: nextPage, language: language) content[.footer] = results.getContentOfOptionalFile(at: page.additionalFooterContentPath, source: page.path) - if pageIncludesCode { + if additionalHeaders.contains(.codeHightlighting) { let highlightCode = factory.factory.html.codeHighlightFooter() content[.footer] = (content[.footer].unwrapped { $0 + "\n" } ?? "") + highlightCode } @@ -74,7 +74,7 @@ struct PageGenerator { return factory.factory.html.makeNextText(text) } - private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, includesCode: Bool) { + private func makeContent(page: Element, metadata: Element.LocalizedMetadata, language: String, path: String) -> (content: String, headers: RequiredHeaders) { if let raw = results.getContentOfMdFile(at: path, source: page.path)?.trimmed.nonEmpty { let (content, includesCode) = contentGenerator.generate(page: page, language: language, content: raw) return (content, includesCode) diff --git a/Sources/Generator/Generators/PageHeadGenerator.swift b/Sources/Generator/Generators/PageHeadGenerator.swift index 22b2901..a8a0ac1 100644 --- a/Sources/Generator/Generators/PageHeadGenerator.swift +++ b/Sources/Generator/Generators/PageHeadGenerator.swift @@ -14,7 +14,7 @@ struct PageHeadGenerator { self.results = results } - func generate(page: Element, language: String, includesCode: Bool = false) -> String { + func generate(page: Element, language: String, headers: RequiredHeaders = .init()) -> String { let metadata = page.localized(for: language) var content = [PageHeadTemplate.Key : String]() @@ -36,17 +36,17 @@ struct PageHeadGenerator { content[.image] = factory.html.linkPreviewImage(file: linkPreviewImageName) } content[.customPageContent] = results.getContentOfOptionalFile(at: page.additionalHeadContentPath, source: page.path) - if includesCode { - let scriptPath = "assets/js/highlight.js" - let relative = page.relativePathToOtherSiteElement(file: scriptPath) - let includeText = factory.html.scriptInclude(path: relative) - if let head = content[.customPageContent] { - content[.customPageContent] = head + "\n" + includeText - } else { - content[.customPageContent] = includeText - } - } + let head = (content[.customPageContent].unwrapped { [$0] } ?? []) + headers + .map { $0.rawValue } + .sorted() + .map { option in + let scriptPath = "assets/js/\(option)" + let relative = page.relativePathToOtherSiteElement(file: scriptPath) + return factory.html.scriptInclude(path: relative) + } + + content[.customPageContent] = head.joined(separator: "\n") return factory.pageHead.generate(content) } } diff --git a/Sources/Generator/Processing/RequiredHeaders.swift b/Sources/Generator/Processing/RequiredHeaders.swift new file mode 100644 index 0000000..473da3b --- /dev/null +++ b/Sources/Generator/Processing/RequiredHeaders.swift @@ -0,0 +1,8 @@ +import Foundation + +enum HeaderFile: String { + case codeHightlighting = "highlight.js" + case modelViewer = "model-viewer.js" +} + +typealias RequiredHeaders = Set