Add more file properties, organize storage, add video block

This commit is contained in:
Christoph Hagen
2025-01-06 15:31:19 +01:00
parent 96c0a75c2f
commit 6cf310d849
25 changed files with 712 additions and 219 deletions

View File

@ -0,0 +1,63 @@
struct AudioBlock: KeyedBlockProcessor {
enum Key: String {
case name
case artist
case album
case file
case cover
}
static let blockId: ContentBlock = .audio
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: [Key : String], markdown: Substring) -> String {
guard let name = arguments[.name],
let artist = arguments[.artist],
let album = arguments[.album],
let fileId = arguments[.file],
let cover = arguments[.cover] else {
invalid(markdown)
return ""
}
guard let image = content.image(cover) else {
results.missing(file: cover, source: "Audio Block")
return ""
}
guard let file = content.file(fileId) else {
results.missing(file: fileId, source: "Audio Block")
return ""
}
let coverSize = 2 * content.settings.audioPlayer.smallCoverImageSize
let coverImage = image.imageVersion(width: coverSize, height: coverSize, type: image.type)
let footer = SingleFilePlayer.footer(
name: name,
artist: artist,
album: album,
url: file.absoluteUrl,
cover: coverImage.outputPath)
results.require(file: file)
results.require(image: coverImage)
results.require(footer: footer)
results.require(headers: .audioPlayerJs, .audioPlayerCss)
results.require(icons: .audioPlayerPlay, .audioPlayerPause)
return SingleFilePlayer().content
}
}

View File

@ -1,5 +1,5 @@
struct AudioBlockProcessor: KeyedBlockProcessor {
struct AudioBlock: KeyedBlockProcessor {
enum Key: String {
case name

View File

@ -5,10 +5,13 @@ enum ContentBlock: String, CaseIterable {
case swift
case video
var processor: BlockProcessor.Type {
switch self {
case .audio: return AudioBlockProcessor.self
case .swift: return SwiftBlockProcessor.self
case .audio: return AudioBlock.self
case .swift: return SwiftBlock.self
case .video: return VideoBlock.self
}
}
}

View File

@ -1,5 +1,5 @@
struct OtherCodeProcessor {
struct OtherCodeBlock {
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"

View File

@ -1,6 +1,6 @@
import Splash
struct SwiftBlockProcessor: BlockProcessor {
struct SwiftBlock: BlockProcessor {
static let blockId: ContentBlock = .swift

View File

@ -8,8 +8,8 @@ extension BlockLineProcessor {
func process(_ markdown: Substring) -> String {
let lines = markdown
.between("```\(Self.blockId.self)", and: "```")
.components(separatedBy: "\n")
return process(lines, markdown: markdown)
.dropFirst().dropLast()
return process(Array(lines), markdown: markdown)
}
}

View File

@ -0,0 +1,267 @@
struct VideoBlock: OrderedKeyBlockProcessor {
static let blockId: ContentBlock = .audio
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: [(key: Key, value: String)], markdown: Substring) -> String {
var options: [Option] = []
var sources: [Source] = []
for (key, value) in arguments {
guard let sourceType = key.sourceType else {
guard let option = makeOption(key: key, value: value) else {
invalid(markdown)
continue
}
options.append(option)
continue
}
let fileId = value.removingSurroundingQuotes
guard let file = content.file(fileId) else {
results.missing(file: fileId, source: "Video Block: \(key)")
continue
}
let source = Source(file: file, type: sourceType)
sources.append(source)
}
guard !sources.isEmpty else {
invalid(markdown)
return ""
}
return VersionedVideo(sources: sources, options: options).content
}
private func makeOption(key: Key, value: String) -> Option? {
switch key {
case .controls: return .controls
case .autoplay: return .autoplay
case .muted: return .muted
case .loop: return .loop
case .playsinline: return .playsinline
default: break
}
let value = value.removingSurroundingQuotes
guard value != "" else {
return nil
}
switch key {
case .height:
guard let height = Int(value) else {
return nil
}
return .height(height)
case .width:
guard let width = Int(value) else {
return nil
}
return .width(width)
case .preload:
guard let preloadOption = Option.Preload(rawValue: value) else {
return nil
}
return .preload(preloadOption)
case .poster:
guard let image = content.image(value) else {
results.missing(file: value, source: "Video Block: poster")
return nil
}
let width = 2 * content.settings.pages.contentWidth
let version = image.imageVersion(width: width, height: width, type: .jpg)
results.require(image: version)
return .poster(image: version.outputPath)
case .src:
guard let file = content.file(value) else {
results.missing(file: value, source: "Video Block: src")
return nil
}
results.warning("Use 'h264' and 'h265' instead of 'src'")
return .src(file.absoluteUrl)
default:
return nil
}
}
}
extension VideoBlock {
enum Key: String {
/// The H264 video file to use
case h264
/// The H265 video file to use
case h265
/// The WebM video file to use
case webm
// MARK: Video options
/// 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
/// Sets the width of the video player
case width
/// Specifies if and how the author thinks the video should be loaded when the page loads
case preload
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
case poster
/// Specifies the URL of the video file
case src
var isOption: Bool {
switch self {
case .controls, .autoplay, .loop, .muted, .playsinline, .height, .width, .preload, .poster, .src:
return true
default:
return false
}
}
var sourceType: SourceType? {
switch self {
case .h264: .h264
case .h265: .h265
case .webm: .webm
default: nil
}
}
}
enum SourceType {
case h264
case h265
case webm
var order: Int {
switch self {
case .h265: 1
case .webm: 2
case .h264: 3
}
}
var mimeType: String {
switch self {
case .h265, .h264: "video/mp4"
case .webm: "video/webm"
}
}
}
struct Source {
let file: FileResource
let type: SourceType
}
}
extension VideoBlock {
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)
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 VideoBlock.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
}
}