import Foundation import HTML import Saga func uniqueTagsWithCount(_ articles: [Item]) -> [(String, Int)] { let tags = articles.flatMap { $0.metadata.tags } let tagsWithCounts = tags.reduce(into: [:]) { $0[$1, default: 0] += 1 } return tagsWithCounts.sorted { $0.1 > $1.1 } } func renderArticles(context: ItemsRenderingContext) -> Node { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy" let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) }) let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key } return baseLayout(canocicalURL: "/articles/", section: .articles, title: "Articles", rssLink: "", extraHeader: "") { // TODO: Add list of tags here that can be navigated to. sortedByYearDescending.map { year, articles in div { div(class: "border-b border-light flex flex-row gap-4 mb-12") { img(src: "/static/img/calendar.svg", width: "40") h1(class: "text-4xl font-extrabold pt-3") { year } } div(class: "grid gap-10 mb-16") { articles.map { renderArticleForGrid(article: $0) } } } } } } func renderTag(context: PartitionedRenderingContext) -> Node { let extraHeader = link( href: "/articles/tag/\(context.key.slugified)/feed.xml", rel: "alternate", title: "\(SiteMetadata.name): articles with tag \(context.key)", type: "application/rss+xml" ) return baseRenderArticles( context.items, canocicalURL: "/articles/tag/\(context.key.slugified)/", title: "Articles in \(context.key)", rssLink: "tag/\(context.key.slugified)/", extraHeader: extraHeader ) { div(class: "border-b border-light grid lg:grid-cols-2 mb-12") { a(href: "/articles") { div(class: "flex flex-row gap-2") { img(src: "/static/img/tag.svg", width: "40") h1(class: "text-4xl font-extrabold") { "\(context.key)" } h1(class: "[&:hover]:border-b border-green text-2xl font-extrabold text-orange px-4") { "«" } } } } } } func renderYear(context: PartitionedRenderingContext) -> Node { baseRenderArticles(context.items, canocicalURL: "/articles/\(context.key)/", title: "Articles in \(context.key)") } private struct SearchData: Encodable { let url: String let title: String let body: String init(article: Item) throws { self.url = article.url self.title = article.title let rawContent: String = try article.absoluteSource.read() self.body = Self.parse(rawContent) } /// Grabs the metadata (wrapped within `---`), the first title, and the body of the document. static func parts(from content: String) -> (String?, String?, String) { let scanner = Scanner(string: content) var header: String? = nil var title: String? = nil if scanner.scanString("---") == "---" { header = scanner.scanUpToString("---") _ = scanner.scanString("---") } if scanner.scanString("# ") == "# " { title = scanner.scanUpToString("\n") } let body = String(scanner.string[scanner.currentIndex...]) return (header, title, body) } static func parse(_ content: String) -> String { let (_, _, body) = parts(from: content) return body .replacingOccurrences(of: "\n", with: " ") .replacingOccurrences(of: "#", with: "") } } func renderJson(_ articles: ItemsRenderingContext) throws -> String { print(articles.items.count) print(articles.items) let data = try jsonEncoder.encode(articles.items.map(SearchData.init(article:))) return String(data: data, encoding: .utf8)! } private let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] return encoder }() private func baseRenderArticles( _ articles: [Item], canocicalURL: String, title pageTitle: String, rssLink: String = "", extraHeader: NodeConvertible = Node.fragment([]), @NodeBuilder label: () -> Node = { Node.fragment([]) } ) -> Node { return baseLayout( canocicalURL: canocicalURL, section: .articles, title: pageTitle, rssLink: rssLink, extraHeader: extraHeader ) { label() articles.map { article in section(class: "mb-10") { h1(class: "post-title text-2xl font-bold mb-2") { a(class: "[&:hover]:border-b border-orange", href: article.url) { article.title } } renderArticleInfo(article) p(class: "mt-4") { a(href: article.url) { article.summary } } } } } }