Refactor page content generators
This commit is contained in:
@ -1,86 +0,0 @@
|
||||
|
||||
enum ContentBlock: String, CaseIterable {
|
||||
|
||||
case audio
|
||||
|
||||
case swift
|
||||
|
||||
var processor: BlockProcessor.Type {
|
||||
switch self {
|
||||
case .audio: return AudioBlockProcessor.self
|
||||
case .swift: return SwiftBlockProcessor.self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol BlockProcessor {
|
||||
|
||||
static var blockId: ContentBlock { get }
|
||||
|
||||
var results: PageGenerationResults { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
|
||||
|
||||
func process(_ markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension BlockProcessor {
|
||||
|
||||
func invalid(_ markdown: Substring) {
|
||||
results.invalid(block: Self.blockId, markdown)
|
||||
}
|
||||
}
|
||||
|
||||
protocol BlockLineProcessor: BlockProcessor {
|
||||
|
||||
func process(_ lines: [String], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension BlockLineProcessor {
|
||||
|
||||
func process(_ markdown: Substring) -> String {
|
||||
let lines = markdown
|
||||
.between("```\(Self.blockId.self)", and: "```")
|
||||
.components(separatedBy: "\n")
|
||||
return process(lines, markdown: markdown)
|
||||
}
|
||||
}
|
||||
|
||||
protocol OrderedKeyBlockProcessor: BlockLineProcessor {
|
||||
|
||||
associatedtype Key: Hashable, RawRepresentable where Key.RawValue == String
|
||||
|
||||
func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension OrderedKeyBlockProcessor {
|
||||
|
||||
func process(_ lines: [String], markdown: Substring) -> String {
|
||||
let result: [(key: Key, value: String)] = lines.compactMap { line in
|
||||
guard line.trimmed != "" else {
|
||||
return nil
|
||||
}
|
||||
let (rawKey, rawValue) = line.splitAtFirst(":")
|
||||
guard let key = Key(rawValue: rawKey.trimmed) else {
|
||||
print("Invalid key \(rawKey)")
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
return (key, rawValue.trimmed)
|
||||
}
|
||||
return process(result, markdown: markdown)
|
||||
}
|
||||
}
|
||||
|
||||
protocol KeyedBlockProcessor: OrderedKeyBlockProcessor {
|
||||
|
||||
func process(_ arguments: [Key : String], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension KeyedBlockProcessor {
|
||||
|
||||
func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String {
|
||||
let result = arguments.reduce(into: [:]) { $0[$1.key] = $1.value }
|
||||
return process(result, markdown: markdown)
|
||||
}
|
||||
}
|
14
CHDataManagement/Generator/Blocks/ContentBlock.swift
Normal file
14
CHDataManagement/Generator/Blocks/ContentBlock.swift
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
enum ContentBlock: String, CaseIterable {
|
||||
|
||||
case audio
|
||||
|
||||
case swift
|
||||
|
||||
var processor: BlockProcessor.Type {
|
||||
switch self {
|
||||
case .audio: return AudioBlockProcessor.self
|
||||
case .swift: return SwiftBlockProcessor.self
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
|
||||
protocol BlockLineProcessor: BlockProcessor {
|
||||
|
||||
func process(_ lines: [String], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension BlockLineProcessor {
|
||||
|
||||
func process(_ markdown: Substring) -> String {
|
||||
let lines = markdown
|
||||
.between("```\(Self.blockId.self)", and: "```")
|
||||
.components(separatedBy: "\n")
|
||||
return process(lines, markdown: markdown)
|
||||
}
|
||||
}
|
18
CHDataManagement/Generator/Blocks/Types/BlockProcessor.swift
Normal file
18
CHDataManagement/Generator/Blocks/Types/BlockProcessor.swift
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
protocol BlockProcessor {
|
||||
|
||||
static var blockId: ContentBlock { get }
|
||||
|
||||
var results: PageGenerationResults { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
|
||||
|
||||
func process(_ markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension BlockProcessor {
|
||||
|
||||
func invalid(_ markdown: Substring) {
|
||||
results.invalid(block: Self.blockId, markdown)
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
|
||||
protocol KeyedBlockProcessor: OrderedKeyBlockProcessor {
|
||||
|
||||
func process(_ arguments: [Key : String], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension KeyedBlockProcessor {
|
||||
|
||||
func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String {
|
||||
let result = arguments.reduce(into: [:]) { $0[$1.key] = $1.value }
|
||||
return process(result, markdown: markdown)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
|
||||
protocol OrderedKeyBlockProcessor: BlockLineProcessor {
|
||||
|
||||
associatedtype Key: Hashable, RawRepresentable where Key.RawValue == String
|
||||
|
||||
func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension OrderedKeyBlockProcessor {
|
||||
|
||||
func process(_ lines: [String], markdown: Substring) -> String {
|
||||
let result: [(key: Key, value: String)] = lines.compactMap { line in
|
||||
guard line.trimmed != "" else {
|
||||
return nil
|
||||
}
|
||||
let (rawKey, rawValue) = line.splitAtFirst(":")
|
||||
guard let key = Key(rawValue: rawKey.trimmed) else {
|
||||
print("Invalid key \(rawKey)")
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
return (key, rawValue.trimmed)
|
||||
}
|
||||
return process(result, markdown: markdown)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
struct AudioPlayerCommandProcessor: CommandProcessor {
|
||||
struct AudioPlayerCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .audioPlayer
|
||||
static let commandType: CommandType = .audioPlayer
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -15,14 +15,14 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 2 else {
|
||||
results.invalid(command: .audioPlayer, "Invalid audio player arguments")
|
||||
invalid("Invalid audio player arguments")
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
let titleText = arguments[1]
|
||||
|
||||
guard content.isValidIdForFile(fileId) else {
|
||||
results.invalid(command: .audioPlayer, "Invalid file id \(fileId) for audio player")
|
||||
invalid("Invalid file id \(fileId) for audio player")
|
||||
return ""
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct BoxCommandProcessor: CommandProcessor {
|
||||
struct BoxCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .box
|
||||
static let commandType: CommandType = .box
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
@ -14,7 +14,7 @@ struct BoxCommandProcessor: CommandProcessor {
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count > 1 else {
|
||||
results.invalid(command: .box, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let title = arguments[0]
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct ButtonCommandProcessor: CommandProcessor {
|
||||
struct ButtonCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .buttons
|
||||
static let commandType: CommandType = .buttons
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -27,7 +27,7 @@ struct ButtonCommandProcessor: CommandProcessor {
|
||||
|
||||
private func convert(button: String, markdown: Substring) -> ContentButtons.Item? {
|
||||
guard let type = PageIcon(rawValue: button.dropAfterFirst("=").trimmed) else {
|
||||
results.invalid(command: commandType, markdown)
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
let parts = button.dropBeforeFirst("=").components(separatedBy: ",").map { $0.trimmed }
|
||||
@ -41,14 +41,14 @@ struct ButtonCommandProcessor: CommandProcessor {
|
||||
case .buttonPlay:
|
||||
return play(arguments: parts, markdown: markdown)
|
||||
default:
|
||||
results.invalid(command: commandType, markdown)
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func download(arguments: [String], markdown: Substring) -> ContentButtons.Item? {
|
||||
guard (2...3).contains(arguments.count) else {
|
||||
results.invalid(command: commandType, markdown)
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
let fileId = arguments[0].trimmed
|
18
CHDataManagement/Generator/Commands/CommandProcessor.swift
Normal file
18
CHDataManagement/Generator/Commands/CommandProcessor.swift
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
protocol CommandProcessor {
|
||||
|
||||
static var commandType: CommandType { get }
|
||||
|
||||
var results: PageGenerationResults { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String
|
||||
}
|
||||
|
||||
extension CommandProcessor {
|
||||
|
||||
func invalid(_ markdown: Substring) {
|
||||
results.invalid(command: Self.commandType, markdown)
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import Foundation
|
||||
/**
|
||||
A string key used in markdown to indicate special elements
|
||||
*/
|
||||
enum ShorthandMarkdownKey: String {
|
||||
enum CommandType: String {
|
||||
|
||||
/// An image
|
||||
/// Format: ` -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .includedHtml, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct IconCommandProcessor: CommandProcessor {
|
||||
struct IconCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .icons
|
||||
static let commandType: CommandType = .icons
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
@ -13,7 +13,7 @@ struct IconCommandProcessor: CommandProcessor {
|
||||
var icons = [PageIcon]()
|
||||
for argument in arguments {
|
||||
guard let icon = PageIcon(rawValue: argument) else {
|
||||
results.invalid(command: .icons, markdown)
|
||||
invalid(markdown)
|
||||
continue
|
||||
}
|
||||
icons.append(icon)
|
@ -1,6 +1,7 @@
|
||||
struct ImageCommandProcessor: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .image
|
||||
struct ImageCommand: CommandProcessor {
|
||||
|
||||
static let commandType: CommandType = .image
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -27,7 +28,7 @@ struct ImageCommandProcessor: CommandProcessor {
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard (1...2).contains(arguments.count) else {
|
||||
results.invalid(command: .image, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let imageId = arguments[0]
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct ImageCompareCommandProcessor: CommandProcessor {
|
||||
struct ImageCompareCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .imageCompare
|
||||
static let commandType: CommandType = .imageCompare
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -17,7 +17,7 @@ struct ImageCompareCommandProcessor: CommandProcessor {
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 2 else {
|
||||
results.invalid(command: .imageCompare, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let leftImageId = arguments[0]
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct LabelsCommandProcessor: CommandProcessor {
|
||||
struct LabelsCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .labels
|
||||
static let commandType: CommandType = .labels
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -16,11 +16,11 @@ struct LabelsCommandProcessor: CommandProcessor {
|
||||
let labels: [ContentLabel] = arguments.compactMap { arg in
|
||||
let parts = arg.components(separatedBy: "=")
|
||||
guard parts.count == 2 else {
|
||||
results.invalid(command: .labels, markdown)
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
guard let icon = PageIcon(rawValue: parts[0].trimmed) else {
|
||||
results.invalid(command: .labels, markdown)
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
results.require(icon: icon)
|
44
CHDataManagement/Generator/Commands/ModelCommand.swift
Normal file
44
CHDataManagement/Generator/Commands/ModelCommand.swift
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
struct ModelCommand: CommandProcessor {
|
||||
|
||||
static let commandType: CommandType = .model
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard fileId.hasSuffix(".glb") else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "Model command")
|
||||
return ""
|
||||
}
|
||||
results.require(file: file)
|
||||
results.require(header: .modelViewer)
|
||||
|
||||
let description = file.localized(in: language)
|
||||
return ModelViewer(file: file.absoluteUrl, description: description).content
|
||||
}
|
||||
|
||||
|
||||
}
|
61
CHDataManagement/Generator/Commands/PageLinkCommand.swift
Normal file
61
CHDataManagement/Generator/Commands/PageLinkCommand.swift
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
struct PageLinkCommand: CommandProcessor {
|
||||
|
||||
static let commandType: CommandType = .pageLink
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let pageId = arguments[0]
|
||||
|
||||
guard let page = content.page(pageId) else {
|
||||
results.missing(page: pageId, source: "Page link command")
|
||||
return ""
|
||||
}
|
||||
guard !page.isDraft else {
|
||||
// Prevent linking to unpublished content
|
||||
return ""
|
||||
}
|
||||
|
||||
results.linked(to: page)
|
||||
|
||||
let localized = page.localized(in: language)
|
||||
let url = page.absoluteUrl(in: language)
|
||||
let title = localized.linkPreviewTitle ?? localized.title
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
image: image)
|
||||
.content
|
||||
}
|
||||
|
||||
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
|
||||
item.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
let imageSet = image.imageSet(width: size, height: size, language: language)
|
||||
results.require(imageSet: imageSet)
|
||||
return imageSet
|
||||
}
|
||||
}
|
||||
}
|
52
CHDataManagement/Generator/Commands/SvgCommand.swift
Normal file
52
CHDataManagement/Generator/Commands/SvgCommand.swift
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
struct SvgCommand: CommandProcessor {
|
||||
|
||||
static let commandType: CommandType = .svg
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 5 else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let x = Int(arguments[1]),
|
||||
let y = Int(arguments[2]),
|
||||
let partWidth = Int(arguments[3]),
|
||||
let partHeight = Int(arguments[4]) else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
let imageId = arguments[0]
|
||||
|
||||
guard let image = content.image(imageId) else {
|
||||
results.missing(file: imageId, source: "SVG command")
|
||||
return ""
|
||||
}
|
||||
guard image.type == .svg else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
return PartialSvgImage(
|
||||
imagePath: image.absoluteUrl,
|
||||
altText: image.localized(in: language),
|
||||
x: x,
|
||||
y: y,
|
||||
width: partWidth,
|
||||
height: partHeight)
|
||||
.content
|
||||
}
|
||||
}
|
55
CHDataManagement/Generator/Commands/TagLinkCommand.swift
Normal file
55
CHDataManagement/Generator/Commands/TagLinkCommand.swift
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
struct TagLinkCommand: CommandProcessor {
|
||||
|
||||
static let commandType: CommandType = .tagLink
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let tagId = arguments[0]
|
||||
|
||||
guard let tag = content.tag(tagId) else {
|
||||
results.missing(tag: tagId, source: "Tag link command")
|
||||
return ""
|
||||
}
|
||||
|
||||
let localized = tag.localized(in: language)
|
||||
let url = tag.absoluteUrl(in: language)
|
||||
let title = localized.name
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
image: image)
|
||||
.content
|
||||
}
|
||||
|
||||
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
|
||||
item.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
let imageSet = image.imageSet(width: size, height: size, language: language)
|
||||
results.require(imageSet: imageSet)
|
||||
return imageSet
|
||||
}
|
||||
}
|
||||
}
|
129
CHDataManagement/Generator/Commands/VideoCommand+Option.swift
Normal file
129
CHDataManagement/Generator/Commands/VideoCommand+Option.swift
Normal file
@ -0,0 +1,129 @@
|
||||
|
||||
extension VideoCommand {
|
||||
|
||||
/// HTML video options
|
||||
enum Option {
|
||||
|
||||
/// Specifies that video controls should be displayed (such as a play/pause button etc).
|
||||
case controls
|
||||
|
||||
/// Specifies that the video will start playing as soon as it is ready
|
||||
case autoplay
|
||||
|
||||
/// Specifies that the video will start over again, every time it is finished
|
||||
case loop
|
||||
|
||||
/// Specifies that the audio output of the video should be muted
|
||||
case muted
|
||||
|
||||
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
|
||||
case playsinline
|
||||
|
||||
/// Sets the height of the video player
|
||||
case height(Int)
|
||||
|
||||
/// Sets the width of the video player
|
||||
case width(Int)
|
||||
|
||||
/// Specifies if and how the author thinks the video should be loaded when the page loads
|
||||
case preload(Preload)
|
||||
|
||||
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
|
||||
case poster(image: String)
|
||||
|
||||
/// Specifies the URL of the video file
|
||||
case src(String)
|
||||
|
||||
init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "controls":
|
||||
self = .controls
|
||||
return
|
||||
case "autoplay":
|
||||
self = .autoplay
|
||||
return
|
||||
case "muted":
|
||||
self = .muted
|
||||
return
|
||||
case "loop":
|
||||
self = .loop
|
||||
return
|
||||
case "playsinline":
|
||||
self = .playsinline
|
||||
return
|
||||
default: break
|
||||
}
|
||||
|
||||
let parts = rawValue.components(separatedBy: "=")
|
||||
guard parts.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let optionName = parts[0]
|
||||
let value = parts[1].removingSurroundingQuotes
|
||||
|
||||
switch optionName {
|
||||
case "height":
|
||||
guard let height = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .height(height)
|
||||
case "width":
|
||||
guard let width = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .width(width)
|
||||
case "preload":
|
||||
guard let preloadOption = Preload(rawValue: value) else {
|
||||
return nil
|
||||
}
|
||||
self = .preload(preloadOption)
|
||||
case "poster":
|
||||
self = .poster(image: value)
|
||||
case "src":
|
||||
self = .src(value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .controls: return "controls"
|
||||
case .autoplay: return "autoplay"
|
||||
case .muted: return "muted"
|
||||
case .loop: return "loop"
|
||||
case .playsinline: return "playsinline"
|
||||
case .height(let height): return "height='\(height)'"
|
||||
case .width(let width): return "width='\(width)'"
|
||||
case .preload(let option): return "preload='\(option)'"
|
||||
case .poster(let image): return "poster='\(image)'"
|
||||
case .src(let url): return "src='\(url)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoCommand.Option {
|
||||
|
||||
/**
|
||||
The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads.
|
||||
|
||||
The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience.
|
||||
This attribute may be ignored in some instances.
|
||||
|
||||
Note: The `preload` attribute is ignored if `autoplay` is present.
|
||||
*/
|
||||
enum Preload: String {
|
||||
|
||||
/// The author thinks that the browser should load the entire video when the page loads
|
||||
case auto
|
||||
|
||||
/// The author thinks that the browser should load only metadata when the page loads
|
||||
case metadata
|
||||
|
||||
/// The author thinks that the browser should NOT load the video when the page loads
|
||||
case none
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
|
||||
struct VideoCommandProcessor: CommandProcessor {
|
||||
struct VideoCommand: CommandProcessor {
|
||||
|
||||
let commandType: ShorthandMarkdownKey = .video
|
||||
static let commandType: CommandType = .video
|
||||
|
||||
let content: Content
|
||||
|
||||
@ -17,7 +17,7 @@ struct VideoCommandProcessor: CommandProcessor {
|
||||
*/
|
||||
func process(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count >= 1 else {
|
||||
results.invalid(command: .video, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0].trimmed
|
||||
@ -28,11 +28,10 @@ struct VideoCommandProcessor: CommandProcessor {
|
||||
results.missing(file: fileId, source: "Video command")
|
||||
return ""
|
||||
}
|
||||
#warning("Create/specify video alternatives")
|
||||
results.require(file: file)
|
||||
|
||||
guard let videoType = file.type.htmlType else {
|
||||
results.invalid(command: .video, markdown)
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -43,12 +42,12 @@ struct VideoCommandProcessor: CommandProcessor {
|
||||
.content
|
||||
}
|
||||
|
||||
private func convertVideoOption(_ videoOption: String, markdown: Substring) -> VideoOption? {
|
||||
private func convertVideoOption(_ videoOption: String, markdown: Substring) -> Option? {
|
||||
guard let optionText = videoOption.trimmed.nonEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let option = VideoOption(rawValue: optionText) else {
|
||||
results.invalid(command: .video, markdown)
|
||||
guard let option = Option(rawValue: optionText) else {
|
||||
invalid(markdown)
|
||||
return nil
|
||||
}
|
||||
switch option {
|
@ -1,9 +1,10 @@
|
||||
import Ink
|
||||
|
||||
struct CodeBlockProcessor {
|
||||
struct MarkdownCodeProcessor: MarkdownProcessor {
|
||||
|
||||
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
|
||||
static let modifier: Modifier.Target = .codeBlocks
|
||||
|
||||
let results: PageGenerationResults
|
||||
private let results: PageGenerationResults
|
||||
|
||||
private let blocks: [ContentBlock : BlockProcessor]
|
||||
|
||||
@ -18,16 +19,14 @@ struct CodeBlockProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
func process(_ html: String, markdown: Substring) -> String {
|
||||
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
|
||||
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let input = String(markdown)
|
||||
let rawBlockId = input.dropAfterFirst("\n").dropBeforeFirst("```").trimmed
|
||||
guard let blockId = ContentBlock(rawValue: rawBlockId) else {
|
||||
return other.process(html: html)
|
||||
}
|
||||
guard let processor = self.blocks[blockId] else {
|
||||
results.invalid(block: blockId, markdown)
|
||||
return ""
|
||||
}
|
||||
return processor.process(markdown)
|
||||
return blocks[blockId]!.process(markdown)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import Ink
|
||||
|
||||
struct MarkdownHeadlineProcessor: MarkdownProcessor {
|
||||
|
||||
static let modifier: Modifier.Target = .headings
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
/**
|
||||
Modify headlines by extracting an id from the headline and adding it into the html element
|
||||
|
||||
Format: ##<title>#<id>
|
||||
|
||||
The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores
|
||||
*/
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let id = markdown
|
||||
.last(after: "#")
|
||||
.trimmed
|
||||
.filter { $0.isNumber || $0.isLetter || $0 == " " }
|
||||
.lowercased()
|
||||
.components(separatedBy: " ")
|
||||
.filter { $0 != "" }
|
||||
.joined(separator: "-")
|
||||
let parts = html.components(separatedBy: ">")
|
||||
return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">")
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import Ink
|
||||
|
||||
struct MarkdownImageProcessor: MarkdownProcessor {
|
||||
|
||||
static var modifier: Modifier.Target = .images
|
||||
|
||||
private let content: Content
|
||||
|
||||
private let results: PageGenerationResults
|
||||
|
||||
private let language: ContentLanguage
|
||||
|
||||
private let commands: [CommandType : CommandProcessor]
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
|
||||
self.commands = CommandType.allCases.reduce(into: [:]) { commands, command in
|
||||
commands[command] = command.processor.init(content: content, results: results, language: language)
|
||||
}
|
||||
}
|
||||
|
||||
var html: HtmlCommand {
|
||||
commands[.includedHtml] as! HtmlCommand
|
||||
}
|
||||
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let argumentList = markdown.between(first: "](", andLast: ")").percentDecoded()
|
||||
let arguments = argumentList.components(separatedBy: ";")
|
||||
|
||||
let rawCommand = markdown.between("![", and: "]").trimmed.percentDecoded()
|
||||
guard rawCommand != "" else {
|
||||
return commands[.image]!.process(arguments, markdown: markdown)
|
||||
}
|
||||
|
||||
guard let command = CommandType(rawValue: rawCommand) else {
|
||||
// Treat unknown commands as normal links
|
||||
results.invalid(command: nil, markdown)
|
||||
return html
|
||||
}
|
||||
return commands[command]!.process(arguments, markdown: markdown)
|
||||
}
|
||||
}
|
@ -1,5 +1,20 @@
|
||||
import Ink
|
||||
|
||||
struct InlineLinkProcessor {
|
||||
struct MarkdownLinkProcessor: MarkdownProcessor {
|
||||
|
||||
static let modifier: Modifier.Target = .links
|
||||
|
||||
private let content: Content
|
||||
|
||||
private let results: PageGenerationResults
|
||||
|
||||
private let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
private let pageLinkMarker = "page:"
|
||||
|
||||
@ -7,12 +22,6 @@ struct InlineLinkProcessor {
|
||||
|
||||
private let fileLinkMarker = "file:"
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let url = markdown.between("(", and: ")")
|
||||
if url.hasPrefix(pageLinkMarker) {
|
10
CHDataManagement/Generator/Markdown/MarkdownProcessor.swift
Normal file
10
CHDataManagement/Generator/Markdown/MarkdownProcessor.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Ink
|
||||
|
||||
protocol MarkdownProcessor {
|
||||
|
||||
static var modifier: Modifier.Target { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
|
||||
|
||||
func process(html: String, markdown: Substring) -> String
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
|
||||
protocol CommandProcessor {
|
||||
|
||||
var commandType: ShorthandMarkdownKey { get }
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage)
|
||||
|
||||
func process(_ arguments: [String], markdown: Substring) -> String
|
||||
}
|
@ -9,257 +9,35 @@ final class PageContentParser {
|
||||
|
||||
private let results: PageGenerationResults
|
||||
|
||||
// MARK: Command handlers
|
||||
// MARK: Markdown handlers
|
||||
|
||||
private let buttonHandler: ButtonCommandProcessor
|
||||
private let code: MarkdownCodeProcessor
|
||||
|
||||
private let labelHandler: LabelsCommandProcessor
|
||||
private let headlines: MarkdownHeadlineProcessor
|
||||
|
||||
private let audioPlayer: AudioPlayerCommandProcessor
|
||||
private let image: MarkdownImageProcessor
|
||||
|
||||
private let icons: IconCommandProcessor
|
||||
|
||||
private let box: BoxCommandProcessor
|
||||
|
||||
private let html: PageHtmlProcessor
|
||||
|
||||
private let video: VideoCommandProcessor
|
||||
|
||||
private let imageCompare: ImageCompareCommandProcessor
|
||||
|
||||
private let images: ImageCommandProcessor
|
||||
|
||||
// MARK: Other handlers
|
||||
|
||||
private let inlineLink: InlineLinkProcessor
|
||||
|
||||
private let code: CodeBlockProcessor
|
||||
private let link: MarkdownLinkProcessor
|
||||
|
||||
init(content: Content, language: ContentLanguage, results: PageGenerationResults) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
self.buttonHandler = .init(content: content, results: results, language: language)
|
||||
self.labelHandler = .init(content: content, results: results, language: language)
|
||||
self.audioPlayer = .init(content: content, results: results, language: language)
|
||||
self.icons = .init(content: content, results: results, language: language)
|
||||
self.box = .init(content: content, results: results, language: language)
|
||||
self.html = .init(content: content, results: results, language: language)
|
||||
self.video = .init(content: content, results: results, language: language)
|
||||
self.imageCompare = .init(content: content, results: results, language: language)
|
||||
self.images = .init(content: content, results: results, language: language)
|
||||
|
||||
self.inlineLink = .init(content: content, results: results, language: language)
|
||||
self.code = .init(content: content, results: results, language: language)
|
||||
self.headlines = .init(content: content, results: results, language: language)
|
||||
self.image = .init(content: content, results: results, language: language)
|
||||
self.link = .init(content: content, results: results, language: language)
|
||||
}
|
||||
|
||||
func generatePage(from content: String) -> String {
|
||||
let parser = MarkdownParser(modifiers: [
|
||||
Modifier(target: .images, closure: processMarkdownImage),
|
||||
Modifier(target: .images, closure: image.process),
|
||||
Modifier(target: .codeBlocks, closure: code.process),
|
||||
Modifier(target: .links, closure: inlineLink.process),
|
||||
Modifier(target: .html, closure: html.process),
|
||||
Modifier(target: .headings, closure: handleHeadlines)
|
||||
Modifier(target: .links, closure: link.process),
|
||||
Modifier(target: .html, closure: image.html.process),
|
||||
Modifier(target: .headings, closure: headlines.process)
|
||||
])
|
||||
return parser.html(from: content)
|
||||
}
|
||||
|
||||
/**
|
||||
Modify headlines by extracting an id from the headline and adding it into the html element
|
||||
|
||||
Format: ##<title>#<id>
|
||||
|
||||
The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores
|
||||
*/
|
||||
private func handleHeadlines(html: String, markdown: Substring) -> String {
|
||||
let id = markdown
|
||||
.last(after: "#")
|
||||
.trimmed
|
||||
.filter { $0.isNumber || $0.isLetter || $0 == " " }
|
||||
.lowercased()
|
||||
.components(separatedBy: " ")
|
||||
.filter { $0 != "" }
|
||||
.joined(separator: "-")
|
||||
let parts = html.components(separatedBy: ">")
|
||||
return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">")
|
||||
}
|
||||
|
||||
private func processMarkdownImage(html: String, markdown: Substring) -> String {
|
||||
//
|
||||
let argumentList = markdown.between(first: "](", andLast: ")").percentDecoded()
|
||||
let arguments = argumentList.components(separatedBy: ";")
|
||||
|
||||
let rawCommand = markdown.between("![", and: "]").trimmed.percentDecoded()
|
||||
guard rawCommand != "" else {
|
||||
return images.process(arguments, markdown: markdown)
|
||||
}
|
||||
|
||||
guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else {
|
||||
// Treat unknown commands as normal links
|
||||
results.invalid(command: nil, markdown)
|
||||
return html
|
||||
}
|
||||
|
||||
switch command {
|
||||
case .image:
|
||||
return images.process(arguments, markdown: markdown)
|
||||
case .labels:
|
||||
return labelHandler.process(arguments, markdown: markdown)
|
||||
case .buttons:
|
||||
return buttonHandler.process(arguments, markdown: markdown)
|
||||
case .video:
|
||||
return video.process(arguments, markdown: markdown)
|
||||
case .pageLink:
|
||||
return handlePageLink(arguments, markdown: markdown)
|
||||
case .includedHtml:
|
||||
return self.html.process(arguments, markdown: markdown)
|
||||
case .box:
|
||||
return box.process(arguments, markdown: markdown)
|
||||
case .model:
|
||||
return handleModel(arguments, markdown: markdown)
|
||||
case .svg:
|
||||
return handleSvg(arguments, markdown: markdown)
|
||||
case .audioPlayer:
|
||||
return audioPlayer.process(arguments, markdown: markdown)
|
||||
case .tagLink:
|
||||
return handleTagLink(arguments, markdown: markdown)
|
||||
case .icons:
|
||||
return icons.process(arguments, markdown: markdown)
|
||||
case .imageCompare:
|
||||
return imageCompare.process(arguments, markdown: markdown)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handlePageLink(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .pageLink, markdown)
|
||||
return ""
|
||||
}
|
||||
let pageId = arguments[0]
|
||||
|
||||
guard let page = content.page(pageId) else {
|
||||
results.missing(page: pageId, source: "Page link command")
|
||||
return ""
|
||||
}
|
||||
guard !page.isDraft else {
|
||||
// Prevent linking to unpublished content
|
||||
return ""
|
||||
}
|
||||
|
||||
results.linked(to: page)
|
||||
|
||||
let localized = page.localized(in: language)
|
||||
let url = page.absoluteUrl(in: language)
|
||||
let title = localized.linkPreviewTitle ?? localized.title
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
image: image)
|
||||
.content
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleTagLink(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .tagLink, markdown)
|
||||
return ""
|
||||
}
|
||||
let tagId = arguments[0]
|
||||
|
||||
guard let tag = content.tag(tagId) else {
|
||||
results.missing(tag: tagId, source: "Tag link command")
|
||||
return ""
|
||||
}
|
||||
|
||||
let localized = tag.localized(in: language)
|
||||
let url = tag.absoluteUrl(in: language)
|
||||
let title = localized.name
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
image: image)
|
||||
.content
|
||||
}
|
||||
|
||||
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
|
||||
item.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
let imageSet = image.imageSet(width: size, height: size, language: language)
|
||||
results.require(imageSet: imageSet)
|
||||
return imageSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
private func handleModel(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 1 else {
|
||||
results.invalid(command: .model, markdown)
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0]
|
||||
guard fileId.hasSuffix(".glb") else {
|
||||
results.invalid(command: .model, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "Model command")
|
||||
return ""
|
||||
}
|
||||
results.require(file: file)
|
||||
results.require(header: .modelViewer)
|
||||
|
||||
let description = file.localized(in: language)
|
||||
return ModelViewer(file: file.absoluteUrl, description: description).content
|
||||
}
|
||||
|
||||
private func handleSvg(_ arguments: [String], markdown: Substring) -> String {
|
||||
guard arguments.count == 5 else {
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let x = Int(arguments[1]),
|
||||
let y = Int(arguments[2]),
|
||||
let partWidth = Int(arguments[3]),
|
||||
let partHeight = Int(arguments[4]) else {
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
let imageId = arguments[0]
|
||||
|
||||
guard let image = content.image(imageId) else {
|
||||
results.missing(file: imageId, source: "SVG command")
|
||||
return ""
|
||||
}
|
||||
guard image.type == .svg else {
|
||||
results.invalid(command: .svg, markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
return PartialSvgImage(
|
||||
imagePath: image.absoluteUrl,
|
||||
altText: image.localized(in: language),
|
||||
x: x,
|
||||
y: y,
|
||||
width: partWidth,
|
||||
height: partHeight)
|
||||
.content
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ enum GenerationAnomaly {
|
||||
case missingFile(file: String, markdown: String)
|
||||
case missingPage(page: String, markdown: String)
|
||||
case missingTag(tag: String, markdown: String)
|
||||
case invalidCommand(command: ShorthandMarkdownKey?, markdown: String)
|
||||
case invalidCommand(command: CommandType?, markdown: String)
|
||||
case warning(String)
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
/// The image versions required for this page
|
||||
private(set) var imagesToGenerate: Set<ImageVersion>
|
||||
|
||||
private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)]
|
||||
private(set) var invalidCommands: [(command: CommandType?, markdown: String)]
|
||||
|
||||
private(set) var invalidBlocks: [(block: ContentBlock?, markdown: String)]
|
||||
|
||||
@ -132,7 +132,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
delegate.inaccessibleContent(file: file)
|
||||
}
|
||||
|
||||
func invalid(command: ShorthandMarkdownKey?, _ markdown: Substring) {
|
||||
func invalid(command: CommandType?, _ markdown: Substring) {
|
||||
let markdown = String(markdown)
|
||||
invalidCommands.append((command, markdown))
|
||||
delegate.invalidCommand(markdown)
|
@ -1,123 +0,0 @@
|
||||
|
||||
/// HTML video options
|
||||
enum VideoOption {
|
||||
|
||||
/// Specifies that video controls should be displayed (such as a play/pause button etc).
|
||||
case controls
|
||||
|
||||
/// Specifies that the video will start playing as soon as it is ready
|
||||
case autoplay
|
||||
|
||||
/// Specifies that the video will start over again, every time it is finished
|
||||
case loop
|
||||
|
||||
/// Specifies that the audio output of the video should be muted
|
||||
case muted
|
||||
|
||||
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
|
||||
case playsinline
|
||||
|
||||
/// Sets the height of the video player
|
||||
case height(Int)
|
||||
|
||||
/// Sets the width of the video player
|
||||
case width(Int)
|
||||
|
||||
/// Specifies if and how the author thinks the video should be loaded when the page loads
|
||||
case preload(VideoPreloadOption)
|
||||
|
||||
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
|
||||
case poster(image: String)
|
||||
|
||||
/// Specifies the URL of the video file
|
||||
case src(String)
|
||||
|
||||
init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "controls":
|
||||
self = .controls
|
||||
return
|
||||
case "autoplay":
|
||||
self = .autoplay
|
||||
return
|
||||
case "muted":
|
||||
self = .muted
|
||||
return
|
||||
case "loop":
|
||||
self = .loop
|
||||
return
|
||||
case "playsinline":
|
||||
self = .playsinline
|
||||
return
|
||||
default: break
|
||||
}
|
||||
|
||||
let parts = rawValue.components(separatedBy: "=")
|
||||
guard parts.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let optionName = parts[0]
|
||||
let value = parts[1].removingSurroundingQuotes
|
||||
|
||||
switch optionName {
|
||||
case "height":
|
||||
guard let height = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .height(height)
|
||||
case "width":
|
||||
guard let width = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .width(width)
|
||||
case "preload":
|
||||
guard let preloadOption = VideoPreloadOption(rawValue: value) else {
|
||||
return nil
|
||||
}
|
||||
self = .preload(preloadOption)
|
||||
case "poster":
|
||||
self = .poster(image: value)
|
||||
case "src":
|
||||
self = .src(value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .controls: return "controls"
|
||||
case .autoplay: return "autoplay"
|
||||
case .muted: return "muted"
|
||||
case .loop: return "loop"
|
||||
case .playsinline: return "playsinline"
|
||||
case .height(let height): return "height='\(height)'"
|
||||
case .width(let width): return "width='\(width)'"
|
||||
case .preload(let option): return "preload='\(option)'"
|
||||
case .poster(let image): return "poster='\(image)'"
|
||||
case .src(let url): return "src='\(url)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads.
|
||||
|
||||
The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience.
|
||||
This attribute may be ignored in some instances.
|
||||
|
||||
Note: The `preload` attribute is ignored if `autoplay` is present.
|
||||
*/
|
||||
enum VideoPreloadOption: String {
|
||||
|
||||
/// The author thinks that the browser should load the entire video when the page loads
|
||||
case auto
|
||||
|
||||
/// The author thinks that the browser should load only metadata when the page loads
|
||||
case metadata
|
||||
|
||||
/// The author thinks that the browser should NOT load the video when the page loads
|
||||
case none
|
||||
}
|
Reference in New Issue
Block a user