Markdown support: downloads, svg, code
This commit is contained in:
parent
534cdf989f
commit
e87f46b2a6
@ -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>"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user