import SwiftSoup /** Handles both inline HTML and the external HTML command */ struct HtmlCommand: CommandProcessor { static let commandType: CommandType = .includedHtml let results: PageGenerationResults let content: Content init(content: Content, results: PageGenerationResults, language: ContentLanguage) { self.content = content self.results = results } /** Handle the HTML command Format: `![html]()` */ func process(_ arguments: [String], markdown: Substring) -> String { guard arguments.count == 1 else { invalid(markdown) return "" } let fileId = arguments[0] guard let file = content.file(fileId) else { results.missing(file: fileId, source: "External HTML command") return "" } let content = file.textContent() checkResources(in: content) return content } /** Handle inline HTML */ func process(_ html: String, markdown: Substring) -> String { checkResources(in: html) return html } private func checkResources(in html: String) { let document: Document do { document = try SwiftSoup.parse(html) } catch { results.warning("Failed to parse inline HTML: \(error)") return } checkImages(in: document) checkLinks(in: document) checkSourceSets(in: document) } private func checkImages(in document: Document) { let srcAttributes: [String] do { let imgElements = try document.select("img") srcAttributes = try imgElements.array() .compactMap { try $0.attr("src") } .filter { !$0.trimmed.isEmpty } } catch { results.warning("Failed to check 'src' attributes of elements in inline HTML: \(error)") return } for src in srcAttributes { findFile(path: src, source: "src of ") } } private func checkLinks(in document: Document) { let hrefs: [String] do { let linkElements = try document.select("a") hrefs = try linkElements.array() .compactMap { try $0.attr("href").trimmed } .filter { !$0.isEmpty } } catch { results.warning("Failed to check 'href' attributes of elements in inline HTML: \(error)") return } for url in hrefs { if url.hasPrefix("http://") || url.hasPrefix("https://") { results.externalLink(to: url) } else { findFile(path: url, source: "href of ") } } } private func checkSourceSets(in document: Document) { let sources: [Element] do { sources = try document.select("source").array() } catch { results.warning("Failed to find elements in inline HTML: \(error)") return } checkSourceSetAttributes(sources: sources) checkSourceAttributes(sources: sources) } private func checkSourceSetAttributes(sources: [Element]) { let srcSets: [String] do { srcSets = try sources .compactMap { try $0.attr("srcset") } .filter { !$0.trimmed.isEmpty } } catch { results.warning("Failed to check 'srcset' attributes of elements in inline HTML: \(error)") return } for src in srcSets { findFile(path: src, source: "srcset of ") } } private func checkSourceAttributes(sources: [Element]) { let srcAttributes: [String] do { srcAttributes = try sources .compactMap { try $0.attr("src") } .filter { !$0.trimmed.isEmpty } } catch { results.warning("Failed to check 'src' attributes of elements in inline HTML: \(error)") return } for src in srcAttributes { findFile(path: src, source: "src of ") } } private func findFile(path: String, source: String) { let type = FileType(fileExtension: path.fileExtension) guard path.hasPrefix("/") else { findFileWith(relativePath: path, type: type, source: source) return } guard !type.generatesImageVersions else { // Try to determine image version needed findImageVersion(path: path, type: type, source: source) return } if findFile(withAbsolutePath: path) { return } let fileId = path.dropBeforeLast("/") if content.isValidIdForFile(fileId) { results.missing(file: fileId, source: "HTML: \(source)") } else { results.warning("Could not find file '\(path)' for \(source)") } } private func findFile(withAbsolutePath absolutePath: String) -> Bool { guard let file = content.file(withOutputPath: absolutePath) else { return false } results.require(file: file) return true } private func findImageVersion(path: String, type: FileType, source: String) { // First check if image original should be used if findFile(withAbsolutePath: path) { return } let fileId = path.dropAfterLast("/").dropBeforeLast("/") guard let file = content.file(fileId) else { results.missing(file: fileId, source: "HTML: \(source)") return } results.warning("Could not determine image version for file '\(file.id)' for \(source)") } private func findFileWith(relativePath: String, type: FileType, source: String) { results.warning("Could not determine relative file '\(relativePath)' for \(source)") } }