Improve video and image handling in markdown
This commit is contained in:
parent
d4ed30ad80
commit
a444c51697
@ -51,6 +51,9 @@
|
|||||||
E2D55EDF28A2AD4F00B9453E /* LinkPreviewMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D55EDE28A2AD4F00B9453E /* LinkPreviewMetadata.swift */; };
|
E2D55EDF28A2AD4F00B9453E /* LinkPreviewMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D55EDE28A2AD4F00B9453E /* LinkPreviewMetadata.swift */; };
|
||||||
E2F8FA1E28A539C500632026 /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA1D28A539C500632026 /* MarkdownProcessor.swift */; };
|
E2F8FA1E28A539C500632026 /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA1D28A539C500632026 /* MarkdownProcessor.swift */; };
|
||||||
E2F8FA2028AB72D900632026 /* PlaceholderTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */; };
|
E2F8FA2028AB72D900632026 /* PlaceholderTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */; };
|
||||||
|
E2F8FA2428ACD0A800632026 /* PageImageTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA2328ACD0A800632026 /* PageImageTemplate.swift */; };
|
||||||
|
E2F8FA2628ACD64500632026 /* PageVideoTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA2528ACD64500632026 /* PageVideoTemplate.swift */; };
|
||||||
|
E2F8FA2828ACD84400632026 /* VideoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8FA2728ACD84400632026 /* VideoType.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@ -110,6 +113,9 @@
|
|||||||
E2D55EDE28A2AD4F00B9453E /* LinkPreviewMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewMetadata.swift; sourceTree = "<group>"; };
|
E2D55EDE28A2AD4F00B9453E /* LinkPreviewMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewMetadata.swift; sourceTree = "<group>"; };
|
||||||
E2F8FA1D28A539C500632026 /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.swift; sourceTree = "<group>"; };
|
E2F8FA1D28A539C500632026 /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.swift; sourceTree = "<group>"; };
|
||||||
E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderTemplate.swift; sourceTree = "<group>"; };
|
E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderTemplate.swift; sourceTree = "<group>"; };
|
||||||
|
E2F8FA2328ACD0A800632026 /* PageImageTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImageTemplate.swift; sourceTree = "<group>"; };
|
||||||
|
E2F8FA2528ACD64500632026 /* PageVideoTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageVideoTemplate.swift; sourceTree = "<group>"; };
|
||||||
|
E2F8FA2728ACD84400632026 /* VideoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoType.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -151,6 +157,7 @@
|
|||||||
E22E876B289D855D00E51191 /* ThumbnailStyle.swift */,
|
E22E876B289D855D00E51191 /* ThumbnailStyle.swift */,
|
||||||
E22E8797289EA42C00E51191 /* FileProcessor.swift */,
|
E22E8797289EA42C00E51191 /* FileProcessor.swift */,
|
||||||
E22E8777289DA0E100E51191 /* GenerationError.swift */,
|
E22E8777289DA0E100E51191 /* GenerationError.swift */,
|
||||||
|
E2F8FA2728ACD84400632026 /* VideoType.swift */,
|
||||||
E22E8794289E81D700E51191 /* FileSystem.swift */,
|
E22E8794289E81D700E51191 /* FileSystem.swift */,
|
||||||
);
|
);
|
||||||
path = WebsiteGenerator;
|
path = WebsiteGenerator;
|
||||||
@ -224,6 +231,8 @@
|
|||||||
E2C5A5E028A0373300102A25 /* ThumbnailTemplate.swift */,
|
E2C5A5E028A0373300102A25 /* ThumbnailTemplate.swift */,
|
||||||
E2C5A5DA28A02F9000102A25 /* TopBarTemplate.swift */,
|
E2C5A5DA28A02F9000102A25 /* TopBarTemplate.swift */,
|
||||||
E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */,
|
E2F8FA1F28AB72D900632026 /* PlaceholderTemplate.swift */,
|
||||||
|
E2F8FA2328ACD0A800632026 /* PageImageTemplate.swift */,
|
||||||
|
E2F8FA2528ACD64500632026 /* PageVideoTemplate.swift */,
|
||||||
);
|
);
|
||||||
path = Elements;
|
path = Elements;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -319,6 +328,7 @@
|
|||||||
E26555E428A2C4FA00BAF496 /* LinkPreviewMetadataProvider.swift in Sources */,
|
E26555E428A2C4FA00BAF496 /* LinkPreviewMetadataProvider.swift in Sources */,
|
||||||
E22E87AA289F1AEE00E51191 /* PageHeadGenerator.swift in Sources */,
|
E22E87AA289F1AEE00E51191 /* PageHeadGenerator.swift in Sources */,
|
||||||
E2D55EDB28A2511D00B9453E /* OverviewSectionCleanTemplate.swift in Sources */,
|
E2D55EDB28A2511D00B9453E /* OverviewSectionCleanTemplate.swift in Sources */,
|
||||||
|
E2F8FA2828ACD84400632026 /* VideoType.swift in Sources */,
|
||||||
E2D55EDF28A2AD4F00B9453E /* LinkPreviewMetadata.swift in Sources */,
|
E2D55EDF28A2AD4F00B9453E /* LinkPreviewMetadata.swift in Sources */,
|
||||||
E22E876A289D84FD00E51191 /* Section+Metadata.swift in Sources */,
|
E22E876A289D84FD00E51191 /* Section+Metadata.swift in Sources */,
|
||||||
E2F8FA2028AB72D900632026 /* PlaceholderTemplate.swift in Sources */,
|
E2F8FA2028AB72D900632026 /* PlaceholderTemplate.swift in Sources */,
|
||||||
@ -340,6 +350,7 @@
|
|||||||
E2C5A5E328A037F900102A25 /* PageTemplate.swift in Sources */,
|
E2C5A5E328A037F900102A25 /* PageTemplate.swift in Sources */,
|
||||||
E2C5A5DD28A036BE00102A25 /* OverviewSectionTemplate.swift in Sources */,
|
E2C5A5DD28A036BE00102A25 /* OverviewSectionTemplate.swift in Sources */,
|
||||||
E2C5A5E528A03A6500102A25 /* BackNavigationTemplate.swift in Sources */,
|
E2C5A5E528A03A6500102A25 /* BackNavigationTemplate.swift in Sources */,
|
||||||
|
E2F8FA2628ACD64500632026 /* PageVideoTemplate.swift in Sources */,
|
||||||
E2C5A5DB28A02F9000102A25 /* TopBarTemplate.swift in Sources */,
|
E2C5A5DB28A02F9000102A25 /* TopBarTemplate.swift in Sources */,
|
||||||
E22E87B6289FF67B00E51191 /* Metadata.swift in Sources */,
|
E22E87B6289FF67B00E51191 /* Metadata.swift in Sources */,
|
||||||
E22E8778289DA0E100E51191 /* GenerationError.swift in Sources */,
|
E22E8778289DA0E100E51191 /* GenerationError.swift in Sources */,
|
||||||
@ -352,6 +363,7 @@
|
|||||||
E22E879B289EE02F00E51191 /* Optional+Extensions.swift in Sources */,
|
E22E879B289EE02F00E51191 /* Optional+Extensions.swift in Sources */,
|
||||||
E22E877A289DA9F900E51191 /* Site.swift in Sources */,
|
E22E877A289DA9F900E51191 /* Site.swift in Sources */,
|
||||||
E22E87B2289F296700E51191 /* ThumbnailInfo.swift in Sources */,
|
E22E87B2289F296700E51191 /* ThumbnailInfo.swift in Sources */,
|
||||||
|
E2F8FA2428ACD0A800632026 /* PageImageTemplate.swift in Sources */,
|
||||||
E22E8787289DDF4C00E51191 /* Page.swift in Sources */,
|
E22E8787289DDF4C00E51191 /* Page.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -5,6 +5,32 @@ import AppKit
|
|||||||
|
|
||||||
final class FileProcessor {
|
final class FileProcessor {
|
||||||
|
|
||||||
|
enum MediaType {
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
case file
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaType(forExtension fileExtension: String) -> MediaType {
|
||||||
|
if supportedImageExtensions[fileExtension] != nil {
|
||||||
|
return .image
|
||||||
|
}
|
||||||
|
if supportedVideoExtensions.contains(fileExtension) {
|
||||||
|
return .video
|
||||||
|
}
|
||||||
|
return .file
|
||||||
|
}
|
||||||
|
|
||||||
|
private let supportedImageExtensions: [String : NSBitmapImageRep.FileType] = [
|
||||||
|
"jpg" : .jpeg,
|
||||||
|
"jpeg" : .jpeg,
|
||||||
|
"png" : .png,
|
||||||
|
]
|
||||||
|
|
||||||
|
private let supportedVideoExtensions: Set<String> = [
|
||||||
|
"mp4", "mov"
|
||||||
|
]
|
||||||
|
|
||||||
struct ImageOutput: Hashable {
|
struct ImageOutput: Hashable {
|
||||||
|
|
||||||
let source: String
|
let source: String
|
||||||
@ -89,7 +115,10 @@ final class FileProcessor {
|
|||||||
throw GenerationError.missingImage(sourceUrl.path)
|
throw GenerationError.missingImage(sourceUrl.path)
|
||||||
}
|
}
|
||||||
guard let imageSize = NSImage(contentsOfFile: sourceUrl.path)?.size else {
|
guard let imageSize = NSImage(contentsOfFile: sourceUrl.path)?.size else {
|
||||||
throw GenerationError.failedToGenerateImage(sourceUrl.path)
|
let height = image.desiredHeight.unwrapped(CGFloat.init)
|
||||||
|
let width = CGFloat(image.width)
|
||||||
|
return .init(width: width, height: height ?? width / 16 * 9)
|
||||||
|
//throw GenerationError.failedToGenerateImage(sourceUrl.path)
|
||||||
}
|
}
|
||||||
let scaledSize = getScaledSize(of: imageSize, to: CGFloat(image.width))
|
let scaledSize = getScaledSize(of: imageSize, to: CGFloat(image.width))
|
||||||
|
|
||||||
@ -146,8 +175,10 @@ final class FileProcessor {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just copy SVG files
|
// Ensure that image file is supported
|
||||||
guard destination.pathExtension.lowercased() != "svg" else {
|
let ext = destination.pathExtension.lowercased()
|
||||||
|
guard supportedImageExtensions[ext] != nil else {
|
||||||
|
print("Copying file \(source.path)")
|
||||||
try FileSystem.copy(source, to: destination)
|
try FileSystem.copy(source, to: destination)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -166,6 +197,7 @@ final class FileProcessor {
|
|||||||
private func createImage(_ destination: URL, from source: URL, with desiredWidth: CGFloat, and desiredHeight: CGFloat?) throws {
|
private func createImage(_ destination: URL, from source: URL, with desiredWidth: CGFloat, and desiredHeight: CGFloat?) throws {
|
||||||
|
|
||||||
guard let sourceImage = NSImage(contentsOfFile: source.path) else {
|
guard let sourceImage = NSImage(contentsOfFile: source.path) else {
|
||||||
|
print("Failed to load image \(source.path)")
|
||||||
throw GenerationError.failedToGenerateImage(source.path)
|
throw GenerationError.failedToGenerateImage(source.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,16 +245,21 @@ final class FileProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func saveImage(_ image: NSImage, atUrl url: URL) throws {
|
private func saveImage(_ image: NSImage, atUrl url: URL) throws {
|
||||||
|
let ext = url.pathExtension.lowercased()
|
||||||
|
guard let type = supportedImageExtensions[ext] else {
|
||||||
|
print("No image type for \(url.path)")
|
||||||
|
throw GenerationError.failedToGenerateImage(url.path)
|
||||||
|
}
|
||||||
guard let tiff = image.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) else {
|
guard let tiff = image.tiffRepresentation, let tiffData = NSBitmapImageRep(data: tiff) else {
|
||||||
print("Failed to get jpg data for image \(url.path)")
|
print("Failed to get data for image \(url.path)")
|
||||||
throw GenerationError.failedToGenerateImage(url.path)
|
throw GenerationError.failedToGenerateImage(url.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let jpgData = tiffData.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(0.7)]) else {
|
guard let data = tiffData.representation(using: type, properties: [.compressionFactor: NSNumber(0.7)]) else {
|
||||||
print("Failed to get jpg data for image \(url.path)")
|
print("Failed to get data for image \(url.path)")
|
||||||
throw GenerationError.failedToGenerateImage(url.path)
|
throw GenerationError.failedToGenerateImage(url.path)
|
||||||
}
|
}
|
||||||
try jpgData.createFolderAndWrite(to: url)
|
try data.createFolderAndWrite(to: url)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,12 @@ import Ink
|
|||||||
|
|
||||||
struct PageContentGenerator {
|
struct PageContentGenerator {
|
||||||
|
|
||||||
|
private let factory: TemplateFactory
|
||||||
|
|
||||||
private let files: FileProcessor
|
private let files: FileProcessor
|
||||||
|
|
||||||
init(files: FileProcessor) {
|
init(factory: TemplateFactory, files: FileProcessor) {
|
||||||
|
self.factory = factory
|
||||||
self.files = files
|
self.files = files
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,11 +22,9 @@ struct PageContentGenerator {
|
|||||||
var hasCodeContent = false
|
var hasCodeContent = false
|
||||||
|
|
||||||
let imageModifier = Modifier(target: .images) { html, markdown in
|
let imageModifier = Modifier(target: .images) { html, markdown in
|
||||||
let result = processMarkdownImage(markdown: markdown, html: html, page: page)
|
do {
|
||||||
switch result {
|
return try processMarkdownImage(markdown: markdown, html: html, page: page)
|
||||||
case .success(let content):
|
} catch {
|
||||||
return content
|
|
||||||
case .failure(let error):
|
|
||||||
errorToThrow = error
|
errorToThrow = error
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -36,9 +37,13 @@ struct PageContentGenerator {
|
|||||||
hasCodeContent = true
|
hasCodeContent = true
|
||||||
return html
|
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")
|
#warning("Check links in markdown for (missing) files to copy")
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier])
|
||||||
|
|
||||||
if hasCodeContent {
|
if hasCodeContent {
|
||||||
#warning("Automatically add hljs hightlighting if code samples are found")
|
#warning("Automatically add hljs hightlighting if code samples are found")
|
||||||
}
|
}
|
||||||
@ -50,81 +55,68 @@ struct PageContentGenerator {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processMarkdownImage(markdown: Substring, html: String, page: Page) -> Result<String, Error> {
|
private func processMarkdownImage(markdown: Substring, html: String, page: Page) throws -> String {
|
||||||
let fileAndTitle = markdown
|
// Split the markdown ![alt](file "title")
|
||||||
.components(separatedBy: "(").last!
|
// For images: ![left_title](file "right_title")
|
||||||
.components(separatedBy: ")").first!
|
// 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 fileExtension = file.lastComponentAfter(".").lowercased()
|
||||||
let rightSubtitle: String?
|
switch files.mediaType(forExtension: fileExtension) {
|
||||||
if fileAndTitle.contains(" \"") {
|
case .image:
|
||||||
rightSubtitle = fileAndTitle.dropBeforeFirst("\"").dropAfterLast("\"")
|
return try handleImage(page: page, file: file, rightTitle: title, leftTitle: alt)
|
||||||
} else {
|
case .video:
|
||||||
rightSubtitle = nil
|
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")
|
#warning("Specify page image width in configuration")
|
||||||
let pageImageWidth = 748
|
let pageImageWidth = 748
|
||||||
let size: NSSize
|
let size = try files.requireImage(source: imagePath, destination: imagePath, width: pageImageWidth)
|
||||||
let imagePath = page.pathRelativeToRootForContainedInputFile(file)
|
|
||||||
do {
|
let imagePath2x = imagePath.insert("@2x", beforeLast: ".")
|
||||||
size = try files.requireImage(
|
|
||||||
source: imagePath,
|
|
||||||
destination: imagePath,
|
|
||||||
width: pageImageWidth,
|
|
||||||
desiredHeight: nil,
|
|
||||||
createDoubleVersion: true)
|
|
||||||
} catch {
|
|
||||||
return .failure(error)
|
|
||||||
}
|
|
||||||
let file2x = file.insert("@2x", beforeLast: ".")
|
let file2x = file.insert("@2x", beforeLast: ".")
|
||||||
#warning("Move HTML code to single location")
|
try files.requireImage(source: imagePath, destination: imagePath2x, width: 2 * pageImageWidth)
|
||||||
let result = articelImage(
|
|
||||||
image: file,
|
let content: [PageImageTemplate.Key : String] = [
|
||||||
image2x: file2x,
|
.image: file,
|
||||||
width: size.width,
|
.image2x: file2x,
|
||||||
height: size.height,
|
.width: "\(Int(size.width))",
|
||||||
rightSubtitle: rightSubtitle,
|
.height: "\(Int(size.height))",
|
||||||
leftSubtitle: leftSubtitle)
|
.leftText: leftTitle ?? "",
|
||||||
return .success(result)
|
.rightText: rightTitle ?? ""]
|
||||||
|
return factory.image.generate(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func articelImage(image: String, image2x: String, width: CGFloat, height: CGFloat, rightSubtitle: String?, leftSubtitle: String?) -> String {
|
private func handleVideo(page: Page, file: String, optionString: String?) throws -> String {
|
||||||
let subtitleCode = subtitle(left: leftSubtitle, right: rightSubtitle)
|
let options: [PageVideoTemplate.VideoOption] = optionString.unwrapped { string in
|
||||||
return fullImageCode(image: image, image2x: image2x, width: width, height: height, subtitle: subtitleCode)
|
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 {
|
let filePath = page.pathRelativeToRootForContainedInputFile(file)
|
||||||
"""
|
files.require(file: filePath)
|
||||||
<span class="image">
|
return factory.video.generate(sources: sources, options: options)
|
||||||
<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>
|
|
||||||
"""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,8 @@ struct PageGenerator {
|
|||||||
return factory.placeholder
|
return factory.placeholder
|
||||||
}
|
}
|
||||||
print("Generated page \(page.path)")
|
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 {
|
private func makeHead(page: Page, language: String) throws -> String {
|
||||||
|
18
WebsiteGenerator/Templates/Elements/PageImageTemplate.swift
Normal file
18
WebsiteGenerator/Templates/Elements/PageImageTemplate.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PageImageTemplate: Template {
|
||||||
|
|
||||||
|
enum Key: String, CaseIterable {
|
||||||
|
case image = "IMAGE"
|
||||||
|
case image2x = "IMAGE_2X"
|
||||||
|
case width = "WIDTH"
|
||||||
|
case height = "HEIGHT"
|
||||||
|
case leftText = "LEFT_TEXT"
|
||||||
|
case rightText = "RIGHT_TEXT"
|
||||||
|
}
|
||||||
|
|
||||||
|
static let templateName = "image.html"
|
||||||
|
|
||||||
|
let raw: String
|
||||||
|
|
||||||
|
}
|
37
WebsiteGenerator/Templates/Elements/PageVideoTemplate.swift
Normal file
37
WebsiteGenerator/Templates/Elements/PageVideoTemplate.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PageVideoTemplate: Template {
|
||||||
|
|
||||||
|
typealias VideoSource = (url: String, type: VideoType)
|
||||||
|
|
||||||
|
enum Key: String, CaseIterable {
|
||||||
|
case options = "OPTIONS"
|
||||||
|
case sources = "SOURCES"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VideoOption: String {
|
||||||
|
case controls
|
||||||
|
case autoplay
|
||||||
|
case muted
|
||||||
|
case loop
|
||||||
|
case playsinline
|
||||||
|
case poster
|
||||||
|
case preload
|
||||||
|
}
|
||||||
|
|
||||||
|
static let templateName = "video.html"
|
||||||
|
|
||||||
|
let raw: String
|
||||||
|
|
||||||
|
func generate<T>(sources: [VideoSource], options: T) -> String where T: Sequence, T.Element == VideoOption {
|
||||||
|
let sourcesCode = sources.map(makeSource).joined(separator: "\n")
|
||||||
|
let optionCode = options.map { $0.rawValue }.joined(separator: " ")
|
||||||
|
return generate([.sources: sourcesCode, .options: optionCode])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSource(_ source: VideoSource) -> String {
|
||||||
|
"""
|
||||||
|
<source src="\(source.url)" type="\(source.type.htmlType)">
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
@ -47,6 +47,10 @@ final class TemplateFactory {
|
|||||||
|
|
||||||
let page: PageTemplate
|
let page: PageTemplate
|
||||||
|
|
||||||
|
let image: PageImageTemplate
|
||||||
|
|
||||||
|
let video: PageVideoTemplate
|
||||||
|
|
||||||
// MARK: Init
|
// MARK: Init
|
||||||
|
|
||||||
init(templateFolder: URL) throws {
|
init(templateFolder: URL) throws {
|
||||||
@ -63,5 +67,7 @@ final class TemplateFactory {
|
|||||||
self.leftHeader = try .init(in: templateFolder)
|
self.leftHeader = try .init(in: templateFolder)
|
||||||
self.centeredHeader = try .init(in: templateFolder)
|
self.centeredHeader = try .init(in: templateFolder)
|
||||||
self.page = try .init(in: templateFolder)
|
self.page = try .init(in: templateFolder)
|
||||||
|
self.image = try .init(in: templateFolder)
|
||||||
|
self.video = try .init(in: templateFolder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
WebsiteGenerator/VideoType.swift
Normal file
12
WebsiteGenerator/VideoType.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum VideoType: String {
|
||||||
|
case mp4
|
||||||
|
|
||||||
|
var htmlType: String {
|
||||||
|
switch self {
|
||||||
|
case .mp4:
|
||||||
|
return "video/mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user