import Foundation import Saga extension Date { func formatted(_ format: String) -> String { let formatter = DateFormatter() formatter.dateFormat = format return formatter.string(from: self) } } extension Item where M == ArticleMetadata { /// The article summary, which is used when displaying a list of articles. var summary: String { // Use the summary if supplied in the articles front-matter. if let summary = metadata.summary { return summary } // Generate the summary from the first 255 words of the article. return String(body.withoutHtmlTags.truncate()) } /// The articles banner image path. var imagePath: String { let image = metadata.image ?? "\(filenameWithoutExtension).png" return "/articles/images/\(image)" } /// An easy way to only get public articles, since ArticleMetadata.public is optional var `public`: Bool { #if DEBUG return true #else return metadata.public ?? true #endif } func getPrimaryTag() -> String? { guard let primaryTag = metadata.primaryTag else { guard metadata.tags.count == 1 else { return nil } return metadata.tags[0] } return primaryTag } func getDate() -> Date { guard let date = metadata.date else { return date } return date } func getUpdatedDate() -> Date? { return metadata.updated } } // NOTE: Most of these are taken from https://github.com/loopwerk/loopwerk.io extension String { /// Used to generate the word count of an article, to be displayed as metadata about /// the article. var numberOfWords: Int { let characterSet = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters) let components = self.components(separatedBy: characterSet) return components.filter { !$0.isEmpty }.count } // This is a sloppy implementation but sadly `NSAttributedString(data:options:documentAttributes:)` // is not available in CoreFoundation, and as such can't run on Linux (blocking CI builds). var withoutHtmlTags: String { return replacingOccurrences(of: "(?m)
[\\s\\S]+?", with: "", options: .regularExpression, range: nil)
.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// See https://jinja2docs.readthedocs.io/en/stable/templates.html#truncate
func truncate(length: Int = 255, killWords: Bool = false, end: String = "...", leeway: Int = 5) -> String {
if count <= length + leeway {
return self
}
if killWords {
return prefix(length - end.count) + end
}
return prefix(length - end.count).split(separator: " ").dropLast().joined(separator: " ") + end
}
/// Removes unwanted breaks that are caused by the way markdown files are formatted by
/// prettier in my neovim setup. When not applied then paragraphs get split up improperly
/// causing them to display funny on the site.
var removeBreaks: String {
replacingOccurrences(of: "