Markdown support: downloads, svg, code

This commit is contained in:
Christoph Hagen 2022-08-29 13:35:25 +02:00
parent 534cdf989f
commit e87f46b2a6
4 changed files with 152 additions and 34 deletions

View File

@ -34,4 +34,64 @@ struct HTMLElementsGenerator {
func makeNextText(_ text: String) -> String {
"\(text)<span class=\"icon-next\"></span>"
}
func svgImage(file: String) -> String {
"""
<span class="image">
<img src="\(file)"/>
</span>
"""
}
func svgImage(file: String, x: Int, y: Int, width: Int, height: Int) -> String {
"""
<span class="image">
<img src="\(file)#svgView(viewBox(\(x), \(y), \(width), \(height))" style="aspect-ratio:\(Float(width)/Float(height))"/>
</span>
"""
}
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 {
"""
<p style="display: flex">
\(content)
</p>
"""
}
private func button(file: String, text: String) -> String {
"""
<a class="download-button" href="\(file)">
\(text)<span class="icon-download"></span>
</a>
"""
}
private func button(file: String, text: String, downloadName: String) -> String {
"""
<a class="download-button" href="\(file)" download="\(downloadName)">
\(text)<span class="icon-download"></span>
</a>
"""
}
func scriptInclude(path: String) -> String {
"<script src=\"\(path)\"></script>"
}
func codeHighlightFooter() -> String {
"<script>hljs.highlightAll();</script>"
}
}

View File

@ -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
// <img src=
// <a href=
//
return html
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier])
if hasCodeContent {
#warning("Automatically add hljs highlighting if code samples are found")
}
return parser.html(from: content)
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier, htmlModifier])
return (parser.html(from: content), hasCodeContent)
}
private func processMarkdownImage(markdown: Substring, html: String, page: Element) -> 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 """
<span class="image">
<img src="\(file)"/>
</span>
"""
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)
}
}

View File

@ -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
}

View File

@ -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)
}