import Foundation
import Ink
import Splash
typealias VideoSource = (url: String, type: VideoFileType)
final class PageContentParser {
private let pageLinkMarker = "page:"
private let tagLinkMarker = "tag:"
private static let codeHighlightFooter = ""
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
let results = PageGenerationResults()
private let content: Content
let language: ContentLanguage
var largeImageWidth: Int {
content.settings.pages.largeImageWidth
}
var thumbnailWidth: Int {
content.settings.pages.contentWidth
}
init(content: Content, language: ContentLanguage) {
self.content = content
self.language = language
}
func requestImages(_ generator: ImageGenerator) {
for request in results.imagesToGenerate {
generator.generateImageSet(
for: request.image.id,
maxWidth: CGFloat(request.size),
maxHeight: CGFloat(request.size))
}
}
func reset() {
results.reset()
}
func generatePage(from content: String) -> String {
reset()
let parser = MarkdownParser(modifiers: [
Modifier(target: .images, closure: processMarkdownImage),
Modifier(target: .codeBlocks, closure: handleCode),
Modifier(target: .links, closure: handleLink),
Modifier(target: .html, closure: handleHTML),
Modifier(target: .headings, closure: handleHeadlines)
])
return parser.html(from: content)
}
private func handleCode(html: String, markdown: Substring) -> String {
guard markdown.starts(with: "```swift") else {
results.requiredHeaders.insert(.codeHightlighting)
results.requiredFooters.insert(PageContentParser.codeHighlightFooter)
return html // Just use normal code highlighting
}
// Highlight swift code using Splash
let code = markdown.between("```swift", and: "```").trimmed
return "
" + swift.highlight(code) + "
"
}
private func handleLink(html: String, markdown: Substring) -> String {
let file = markdown.between("(", and: ")")
if file.hasPrefix(pageLinkMarker) {
return handlePageLink(file: file, html: html, markdown: markdown)
}
if file.hasPrefix(tagLinkMarker) {
return handleTagLink(file: file, html: html, markdown: markdown)
}
#warning("Check existence of linked file")
return html
}
private func handlePageLink(file: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#")
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "")
guard let page = content.page(pageId) else {
results.missingPages.insert(pageId)
// Remove link since the page can't be found
return markdown.between("[", and: "]")
}
results.linkedPages.insert(page)
let pagePath = content.absoluteUrlToPage(page, language: language)
return html.replacingOccurrences(of: textToChange, with: pagePath)
}
private func handleTagLink(file: String, html: String, markdown: Substring) -> String {
// Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#")
let tagId = textToChange.replacingOccurrences(of: tagLinkMarker, with: "")
guard let tag = content.tag(tagId) else {
results.missingTags.insert(tagId)
// Remove link since the tag can't be found
return markdown.between("[", and: "]")
}
results.linkedTags.insert(tag)
let tagPath = content.absoluteUrlToTag(tag, language: language)
return html.replacingOccurrences(of: textToChange, with: tagPath)
}
private func handleHTML(html: String, markdown: Substring) -> String {
#warning("Check HTML code in markdown for required resources")
// Things to check:
return html
}
/**
Modify headlines by extracting an id from the headline and adding it into the html element
Format: ###
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 percentDecoded(_ string: String) -> String {
guard let decoded = string.removingPercentEncoding else {
print("Invalid string: \(string)")
return string
}
return decoded
}
private func processMarkdownImage(html: String, markdown: Substring) -> String {
//
let argumentList = percentDecoded(markdown.between(first: "](", andLast: ")"))
let arguments = argumentList.components(separatedBy: ";")
let rawCommand = percentDecoded(markdown.between("![", and: "]").trimmed)
guard rawCommand != "" else {
return handleImage(arguments)
}
guard let command = ShorthandMarkdownKey(rawValue: rawCommand) else {
// Treat unknown commands as normal links
results.unknownCommands.append(rawCommand)
return html
}
switch command {
case .image:
return handleImage(arguments)
case .hikingStatistics:
return handleHikingStatistics(arguments)
case .downloadButtons:
return handleDownloadButtons(arguments)
case .video:
return handleVideo(arguments)
case .externalLink:
return handleExternalButtons(arguments)
case .gitLink:
return handleGitButtons(arguments)
case .pageLink:
return handlePageLink(arguments)
case .includedHtml:
return handleExternalHtml(arguments)
case .box:
return handleSimpleBox(arguments)
case .model:
return handleModel(arguments)
case .svg:
return handleSvg(arguments)
default:
results.unknownCommands.append(command.rawValue)
return ""
}
}
/**
Format: `[image](;]`
*/
private func handleImage(_ arguments: [String]) -> String {
guard (1...2).contains(arguments.count) else {
results.invalidCommandArguments.append((.image , arguments))
return ""
}
let imageId = arguments[0]
guard let image = content.image(imageId) else {
results.missingFiles.insert(imageId)
return ""
}
results.files.insert(image)
let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.getDescription(for: language)
let path = content.absoluteUrlToFile(image)
guard !image.type.isSvg else {
return SvgImage(imagePath: path, altText: altText).content
}
let thumbnail = FeedEntryData.Image(
rawImagePath: path,
width: thumbnailWidth,
height: thumbnailWidth,
altText: altText)
results.imagesToGenerate.insert(.init(size: thumbnailWidth, image: image))
let largeImage = FeedEntryData.Image(
rawImagePath: path,
width: largeImageWidth,
height: largeImageWidth,
altText: altText)
results.imagesToGenerate.insert(.init(size: largeImageWidth, image: image))
return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
thumbnail: thumbnail,
largeImage: largeImage,
caption: caption).content
}
/**
Format: `![hiking-stats](