Improve video and image handling in markdown

This commit is contained in:
Christoph Hagen
2022-08-17 10:36:21 +02:00
parent d4ed30ad80
commit a444c51697
8 changed files with 198 additions and 83 deletions

View File

@@ -3,9 +3,12 @@ import Ink
struct PageContentGenerator {
private let factory: TemplateFactory
private let files: FileProcessor
init(files: FileProcessor) {
init(factory: TemplateFactory, files: FileProcessor) {
self.factory = factory
self.files = files
}
@@ -19,11 +22,9 @@ struct PageContentGenerator {
var hasCodeContent = false
let imageModifier = Modifier(target: .images) { html, markdown in
let result = processMarkdownImage(markdown: markdown, html: html, page: page)
switch result {
case .success(let content):
return content
case .failure(let error):
do {
return try processMarkdownImage(markdown: markdown, html: html, page: page)
} catch {
errorToThrow = error
return ""
}
@@ -36,9 +37,13 @@ struct PageContentGenerator {
hasCodeContent = true
return html
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier])
let linkModifier = Modifier(target: .links) { html, markdown in
#warning("Check links in markdown for (missing) files to copy")
return html
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier])
#warning("Check links in markdown for (missing) files to copy")
if hasCodeContent {
#warning("Automatically add hljs hightlighting if code samples are found")
}
@@ -50,81 +55,68 @@ struct PageContentGenerator {
return result
}
private func processMarkdownImage(markdown: Substring, html: String, page: Page) -> Result<String, Error> {
let fileAndTitle = markdown
.components(separatedBy: "(").last!
.components(separatedBy: ")").first!
private func processMarkdownImage(markdown: Substring, html: String, page: Page) throws -> String {
// Split the markdown ![alt](file "title")
// For images: ![left_title](file "right_title")
// For videos: ![option...](file)
// 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
let file = fileAndTitle.components(separatedBy: " \"").first! // Remove title
let rightSubtitle: String?
if fileAndTitle.contains(" \"") {
rightSubtitle = fileAndTitle.dropBeforeFirst("\"").dropAfterLast("\"")
} else {
rightSubtitle = nil
let fileExtension = file.lastComponentAfter(".").lowercased()
switch files.mediaType(forExtension: fileExtension) {
case .image:
return try handleImage(page: page, file: file, rightTitle: title, leftTitle: alt)
case .video:
return try handleVideo(page: page, file: file, optionString: alt)
case .file:
#warning("Handle other files in markdown")
print("[WARN] Unhandled file \(file) with extension \(fileExtension)")
return ""
}
let leftSubtitle = markdown
.components(separatedBy: "]").first!
.components(separatedBy: "[").last!.nonEmpty
}
private func handleImage(page: Page, file: String, rightTitle: String?, leftTitle: String?) throws -> String {
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
#warning("Specify page image width in configuration")
let pageImageWidth = 748
let size: NSSize
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
do {
size = try files.requireImage(
source: imagePath,
destination: imagePath,
width: pageImageWidth,
desiredHeight: nil,
createDoubleVersion: true)
} catch {
return .failure(error)
}
let size = try files.requireImage(source: imagePath, destination: imagePath, width: pageImageWidth)
let imagePath2x = imagePath.insert("@2x", beforeLast: ".")
let file2x = file.insert("@2x", beforeLast: ".")
#warning("Move HTML code to single location")
let result = articelImage(
image: file,
image2x: file2x,
width: size.width,
height: size.height,
rightSubtitle: rightSubtitle,
leftSubtitle: leftSubtitle)
return .success(result)
try files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * pageImageWidth)
let content: [PageImageTemplate.Key : String] = [
.image: file,
.image2x: file2x,
.width: "\(Int(size.width))",
.height: "\(Int(size.height))",
.leftText: leftTitle ?? "",
.rightText: rightTitle ?? ""]
return factory.image.generate(content)
}
private func articelImage(image: String, image2x: String, width: CGFloat, height: CGFloat, rightSubtitle: String?, leftSubtitle: String?) -> String {
let subtitleCode = subtitle(left: leftSubtitle, right: rightSubtitle)
return fullImageCode(image: image, image2x: image2x, width: width, height: height, subtitle: subtitleCode)
}
private func handleVideo(page: Page, file: String, optionString: String?) throws -> String {
let options: [PageVideoTemplate.VideoOption] = optionString.unwrapped { string in
string.components(separatedBy: " ").compactMap { optionText in
guard let optionText = optionText.trimmed.nonEmpty else {
return nil
}
guard let option = PageVideoTemplate.VideoOption(rawValue: optionText) else {
print("[WARN] Unknown video option \(optionText) in page \(page.path)")
return nil
}
return option
}
} ?? []
#warning("Check page folder for alternative video versions")
let sources: [PageVideoTemplate.VideoSource] = [(url: file, type: .mp4)]
private func articleImageWithoutSubtitle(image: String, image2x: String, width: CGFloat, height: CGFloat) -> String {
"""
<span class="image">
<img src="\(image)" srcset="\(image2x) 2x" width="\(Int(width))" height="\(Int(height))" loading="lazy"/>
</span>
"""
}
private func subtitle(left: String?, right: String?) -> String {
guard left != nil || right != nil else {
return ""
}
let leftCode = left.unwrapped { "<span class=\"left\">\($0)</span>" } ?? ""
let rightCode = right.unwrapped { "<span class=\"right\">\($0)</span>" } ?? ""
return """
<div class="subtitle">
\(leftCode)
\(rightCode)
</div>
"""
}
private func fullImageCode(image: String, image2x: String, width: CGFloat, height: CGFloat, subtitle: String) -> String {
"""
<span class="image">
<img src="\(image)" srcset="\(image2x) 2x" width="\(Int(width))" height="\(Int(height))" loading="lazy"/>
\(subtitle)
</span>
"""
let filePath = page.pathRelativeToRootForContainedInputFile(file)
files.require(file: filePath)
return factory.video.generate(sources: sources, options: options)
}
}

View File

@@ -55,7 +55,8 @@ struct PageGenerator {
return factory.placeholder
}
print("Generated page \(page.path)")
return try PageContentGenerator(files: files).generate(page: page, language: language, at: url)
return try PageContentGenerator(factory: factory.factory, files: files)
.generate(page: page, language: language, at: url)
}
private func makeHead(page: Page, language: String) throws -> String {