From e87f46b2a60e1217f0f661c5ca724c0ad6413f14 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Mon, 29 Aug 2022 13:35:25 +0200 Subject: [PATCH] Markdown support: downloads, svg, code --- .../Generators/HTMLElementsGenerator.swift | 60 ++++++++++++ .../Generators/MarkdownProcessor.swift | 96 +++++++++++++------ .../Generators/PageGenerator.swift | 18 ++-- .../Generators/PageHeadGenerator.swift | 12 ++- 4 files changed, 152 insertions(+), 34 deletions(-) diff --git a/WebsiteGenerator/Generators/HTMLElementsGenerator.swift b/WebsiteGenerator/Generators/HTMLElementsGenerator.swift index 8e97604..4d63076 100644 --- a/WebsiteGenerator/Generators/HTMLElementsGenerator.swift +++ b/WebsiteGenerator/Generators/HTMLElementsGenerator.swift @@ -34,4 +34,64 @@ struct HTMLElementsGenerator { func makeNextText(_ text: String) -> String { "\(text)" } + + func svgImage(file: String) -> String { + """ + + + + """ + } + + func svgImage(file: String, x: Int, y: Int, width: Int, height: Int) -> String { + """ + + + + """ + } + + func downloadButtons(_ buttons: [(file: String, text: String, downloadName: String?)]) -> String { + let content = buttons.map { + if let download = $0.downloadName { + return button(file: $0.file, text: $0.text, downloadName: download) + } else { + return button(file: $0.file, text: $0.text) + } + }.joined(separator: "\n") + return flexParagraph(content) + } + + private func flexParagraph(_ content: String) -> String { + """ +

+ \(content) +

+ """ + } + + private func button(file: String, text: String) -> String { + """ + + \(text) + + """ + } + + private func button(file: String, text: String, downloadName: String) -> String { + """ + + \(text) + + + """ + } + + func scriptInclude(path: String) -> String { + "" + } + + func codeHighlightFooter() -> String { + "" + } } diff --git a/WebsiteGenerator/Generators/MarkdownProcessor.swift b/WebsiteGenerator/Generators/MarkdownProcessor.swift index 90a2921..ce586f0 100644 --- a/WebsiteGenerator/Generators/MarkdownProcessor.swift +++ b/WebsiteGenerator/Generators/MarkdownProcessor.swift @@ -15,8 +15,7 @@ struct PageContentGenerator { self.factory = factory } - func generate(page: Element, language: String, content: String) -> String { - + func generate(page: Element, language: String, content: String) -> (content: String, includesCode: Bool) { var hasCodeContent = false let imageModifier = Modifier(target: .images) { html, markdown in @@ -31,28 +30,42 @@ struct PageContentGenerator { return html } let linkModifier = Modifier(target: .links) { html, markdown in - #warning("Check links in markdown for (missing) files to copy") + let file = markdown.between("(", and: ")") + if let filePath = page.nonAbsolutePathRelativeToRootForContainedInputFile(file) { + // The target of the page link must be present after generation is complete + files.expect(file: filePath, source: page.path) + } + return html + } + let htmlModifier = Modifier(target: .html) { html, markdown in + //print("[HTML] Found in page \(page.path):") + //print(markdown) + // Thinks to check + // String { - // Split the markdown ![alt](file "title") - // For images: ![left_title](file "right_title") - // For videos: ![option...](file) + // Split the markdown ![alt](file title) + // For images: ![left_title](file right_title) + // For videos: ![option1,option2,...](file) + // For svg with custom area: ![x,y,width,height](file.svg) + // For downloads: ![download](file1,text1;file2,text2, ...) // For files: ? let fileAndTitle = markdown.between("(", and: ")") - let file = fileAndTitle.dropAfterFirst(" \"") - let title = fileAndTitle.contains(" \"") ? fileAndTitle.between("\"", and: "\"").nonEmpty : nil let alt = markdown.between("[", and: "]").nonEmpty + if alt == "download" { + return handleDownloadButtons(page: page, content: fileAndTitle) + } + + let file = fileAndTitle.dropAfterFirst(" ") + let title = fileAndTitle.contains(" ") ? fileAndTitle.dropBeforeFirst(" ").nonEmpty : nil let fileExtension = file.lastComponentAfter(".").lowercased() switch MediaType(fileExtension: fileExtension) { @@ -60,10 +73,9 @@ struct PageContentGenerator { return handleImage(page: page, file: file, rightTitle: title, leftTitle: alt) case .video: return handleVideo(page: page, file: file, optionString: alt) + case .svg: + return handleSvg(page: page, file: file, area: alt) case .file: - if fileExtension == "svg" { - return handleSvg(page: page, file: file) - } return handleFile(page: page, file: file, fileExtension: fileExtension) } } @@ -94,7 +106,7 @@ struct PageContentGenerator { return nil } guard let option = PageVideoTemplate.VideoOption(rawValue: optionText) else { - print("[WARN] Unknown video option \(optionText) in page \(page.path)") + log.add(warning: "Unknown video option \(optionText)", source: page.path) return nil } return option @@ -108,20 +120,50 @@ struct PageContentGenerator { return factory.video.generate(sources: sources, options: options) } - private func handleSvg(page: Element, file: String) -> String { + private func handleSvg(page: Element, file: String, area: String?) -> String { let imagePath = page.pathRelativeToRootForContainedInputFile(file) files.require(file: imagePath) - return """ - - - - """ + guard let area = area else { + return factory.html.svgImage(file: file) + } + let parts = area.components(separatedBy: ",") + guard parts.count == 4, + let x = Int(parts[0]), + let y = Int(parts[1]), + let width = Int(parts[2]), + let height = Int(parts[3]) else { + log.add(warning: "Invalid area string for svg image", source: page.path) + return factory.html.svgImage(file: file) + } + + return factory.html.svgImage(file: file, x: x, y: y, width: width, height: height) } private func handleFile(page: Element, file: String, fileExtension: String) -> String { - #warning("Handle other files in markdown") - print("[WARN] Unhandled file \(file) with extension \(fileExtension)") + log.add(warning: "Unhandled file \(file) with extension \(fileExtension)", source: page.path) return "" } + + private func handleDownloadButtons(page: Element, content: String) -> String { + let buttons = content + .components(separatedBy: ";") + .compactMap { button -> (file: String, text: String, downloadName: String?)? in + let parts = button.components(separatedBy: ",") + guard parts.count == 2 || parts.count == 3 else { + log.add(warning: "Invalid button definition", source: page.path) + return nil + } + let file = parts[0].trimmed + let title = parts[1].trimmed + let downloadName = parts.count == 3 ? parts[2].trimmed : nil + + // Ensure that file is available + let filePath = page.pathRelativeToRootForContainedInputFile(file) + files.require(file: filePath) + + return (file, title, downloadName) + } + return factory.html.downloadButtons(buttons) + } } diff --git a/WebsiteGenerator/Generators/PageGenerator.swift b/WebsiteGenerator/Generators/PageGenerator.swift index f213bb4..fdf7678 100644 --- a/WebsiteGenerator/Generators/PageGenerator.swift +++ b/WebsiteGenerator/Generators/PageGenerator.swift @@ -25,26 +25,32 @@ struct PageGenerator { } let path = page.fullPageUrl(for: language) let inputContentPath = page.path + "/\(language).md" - #warning("Make prev and next navigation relative") let metadata = page.localized(for: language) let nextLanguage = page.nextLanguage(for: language) - + let pageContent = makeContent(page: page, language: language, path: inputContentPath) + let pageIncludesCode = pageContent?.includesCode ?? false + var content = [PageTemplate.Key : String]() - content[.head] = factory.pageHead.generate(page: page, language: language) + content[.head] = factory.pageHead.generate(page: page, language: language, includesCode: pageIncludesCode) let sectionUrl = page.sectionUrl(for: language) content[.topBar] = factory.topBar.generate(sectionUrl: sectionUrl, languageButton: nextLanguage) content[.contentClass] = "content" + if !page.useCustomHeader { content[.header] = makeHeader(page: page, metadata: metadata, language: language) } - let pageContent = makeContent(page: page, language: language, path: inputContentPath) - content[.content] = pageContent ?? factory.placeholder + content[.content] = pageContent?.content ?? factory.placeholder content[.previousPageLinkText] = previousPage.unwrapped { factory.factory.html.makePrevText($0.text) } content[.previousPageUrl] = previousPage?.link content[.nextPageLinkText] = nextPage.unwrapped { factory.factory.html.makeNextText($0.text) } content[.nextPageUrl] = nextPage?.link content[.footer] = page.customFooterContent() + if pageIncludesCode { + let highlightCode = factory.factory.html.codeHighlightFooter() + content[.footer] = (content[.footer].unwrapped { $0 + "\n" } ?? "") + highlightCode + } + let url = files.urlInOutputFolder(path) guard factory.page.generate(content, to: url) else { return @@ -52,7 +58,7 @@ struct PageGenerator { log.add(info: "Generated \(pageContent == nil ? "empty page " : "")\(path)", source: page.path) } - private func makeContent(page: Element, language: String, path: String) -> String? { + private func makeContent(page: Element, language: String, path: String) -> (content: String, includesCode: Bool)? { guard let content = files.contentOfOptionalFile(atPath: path, source: page.path) else { return nil } diff --git a/WebsiteGenerator/Generators/PageHeadGenerator.swift b/WebsiteGenerator/Generators/PageHeadGenerator.swift index b1662a9..66a9458 100644 --- a/WebsiteGenerator/Generators/PageHeadGenerator.swift +++ b/WebsiteGenerator/Generators/PageHeadGenerator.swift @@ -10,7 +10,7 @@ struct PageHeadGenerator { self.factory = factory } - func generate(page: Element, language: String) -> String { + func generate(page: Element, language: String, includesCode: Bool = false) -> String { let metadata = page.localized(for: language) var content = [PageHeadTemplate.Key : String]() @@ -31,6 +31,16 @@ struct PageHeadGenerator { content[.image] = factory.html.linkPreviewImage(file: linkPreviewImageName) } content[.customPageContent] = page.customHeadContent() + if includesCode { + let scriptPath = "/assets/js/highlight.js" + #warning("Make highlight script path relative") + let includeText = factory.html.scriptInclude(path: scriptPath) + if let head = content[.customPageContent] { + content[.customPageContent] = head + "\n" + includeText + } else { + content[.customPageContent] = includeText + } + } return factory.pageHead.generate(content) }