Created
January 10, 2025 13:23
-
-
Save baleboy/57993c9cc595cd2c80cef98b2af7683d to your computer and use it in GitHub Desktop.
AI generated static site generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Foundation | |
| // MARK: - Models and Types | |
| struct Post { | |
| let title: String | |
| let date: Date | |
| let content: String | |
| var htmlFileName: String { | |
| let dateFormatter = DateFormatter() | |
| dateFormatter.dateFormat = "yyyy-MM-dd" | |
| let dateString = dateFormatter.string(from: date) | |
| // Create URL-friendly filename | |
| let safeTitleText = title.lowercased() | |
| .replacingOccurrences(of: " ", with: "-") | |
| .components(separatedBy: CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-"))) | |
| .joined() | |
| return "\(dateString)-\(safeTitleText).html" | |
| } | |
| } | |
| class StaticSiteGenerator { | |
| private let inputDirectory: URL | |
| private let outputDirectory: URL | |
| private let dateFormatter: DateFormatter | |
| init(inputDirectory: URL, outputDirectory: URL) { | |
| self.inputDirectory = inputDirectory | |
| self.outputDirectory = outputDirectory | |
| self.dateFormatter = DateFormatter() | |
| self.dateFormatter.dateFormat = "yyyy-MM-dd" | |
| } | |
| // MARK: - Main Build Process | |
| func build() throws { | |
| // Create output directory if it doesn't exist | |
| try FileManager.default.createDirectory(at: outputDirectory, | |
| withIntermediateDirectories: true) | |
| // Get all markdown files | |
| let files = try FileManager.default.contentsOfDirectory(at: inputDirectory, | |
| includingPropertiesForKeys: nil) | |
| .filter { $0.pathExtension == "md" } | |
| // Process each file and collect posts | |
| var posts: [Post] = [] | |
| for file in files { | |
| if let post = try processMarkdownFile(at: file) { | |
| posts.append(post) | |
| try generatePostHTML(post) | |
| } | |
| } | |
| // Generate index page | |
| try generateIndexHTML(with: posts) | |
| // Copy assets if they exist | |
| try copyAssets() | |
| } | |
| // MARK: - Markdown Processing | |
| private func processMarkdownFile(at url: URL) throws -> Post? { | |
| let content = try String(contentsOf: url) | |
| // Parse frontmatter | |
| guard let (frontmatter, markdown) = parseFrontmatter(from: content) else { | |
| print("Warning: No valid frontmatter found in \(url.lastPathComponent)") | |
| return nil | |
| } | |
| // Extract required metadata | |
| guard let title = frontmatter["title"] as? String, | |
| let dateString = frontmatter["date"] as? String, | |
| let date = dateFormatter.date(from: dateString.replacingOccurrences(of: "\"", with: "")) else { | |
| print("Warning: Missing required frontmatter in \(url.lastPathComponent)") | |
| return nil | |
| } | |
| return Post(title: title, date: date, content: markdown) | |
| } | |
| private func parseFrontmatter(from content: String) -> ([String: Any], String)? { | |
| let components = content.components(separatedBy: "+++") | |
| guard components.count >= 3 else { return nil } | |
| let frontmatterString = components[1].trimmingCharacters(in: .whitespacesAndNewlines) | |
| let markdown = components[2].trimmingCharacters(in: .whitespacesAndNewlines) | |
| // Parse TOML-style frontmatter | |
| var frontmatter: [String: Any] = [:] | |
| let lines = frontmatterString.components(separatedBy: .newlines) | |
| for line in lines { | |
| let parts = line.components(separatedBy: "=").map { $0.trimmingCharacters(in: .whitespaces) } | |
| if parts.count == 2 { | |
| frontmatter[parts[0]] = parts[1] | |
| } | |
| } | |
| return (frontmatter, markdown) | |
| } | |
| // MARK: - HTML Generation | |
| private func generatePostHTML(_ post: Post) throws { | |
| let html = generateHTML(for: post) | |
| let outputFile = outputDirectory.appendingPathComponent(post.htmlFileName) | |
| try html.write(to: outputFile, atomically: true, encoding: .utf8) | |
| } | |
| private func generateHTML(for post: Post) -> String { | |
| // Convert markdown to HTML (basic implementation - you might want to use a proper markdown parser) | |
| let content = convertMarkdownToHTML(post.content) | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>\(post.title)</title> | |
| \(generateCSS()) | |
| </head> | |
| <body> | |
| <article> | |
| <h1>\(post.title)</h1> | |
| <time datetime="\(dateFormatter.string(from: post.date))"> | |
| \(dateFormatter.string(from: post.date)) | |
| </time> | |
| <div class="content"> | |
| \(content) | |
| </div> | |
| </article> | |
| </body> | |
| </html> | |
| """ | |
| } | |
| private func generateIndexHTML(with posts: [Post]) throws { | |
| let sortedPosts = posts.sorted { $0.date > $1.date } | |
| var postsHTML = "" | |
| for post in sortedPosts { | |
| postsHTML += """ | |
| <div class="post-preview"> | |
| <h2><a href="\(post.htmlFileName)">\(post.title)</a></h2> | |
| <time datetime="\(dateFormatter.string(from: post.date))"> | |
| \(dateFormatter.string(from: post.date)) | |
| </time> | |
| </div> | |
| """ | |
| } | |
| let indexHTML = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>My Blog</title> | |
| \(generateCSS()) | |
| </head> | |
| <body> | |
| <h1>My Blog</h1> | |
| <div class="posts"> | |
| \(postsHTML) | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| let indexFile = outputDirectory.appendingPathComponent("index.html") | |
| try indexHTML.write(to: indexFile, atomically: true, encoding: .utf8) | |
| } | |
| // MARK: - Utilities | |
| private func convertMarkdownToHTML(_ markdown: String) -> String { | |
| // This is a very basic implementation | |
| // You might want to use a proper markdown parser library | |
| var html = markdown | |
| // Convert headers | |
| html = html.replacingOccurrences(of: #"^##\s+(.+)$"#, with: "<h2>$1</h2>", options: .regularExpression, range: nil) | |
| html = html.replacingOccurrences(of: #"^#\s+(.+)$"#, with: "<h1>$1</h1>", options: .regularExpression, range: nil) | |
| // Convert code blocks | |
| html = html.replacingOccurrences(of: "```swift\n([^`]+)\n```", with: "<pre><code class=\"swift\">$1</code></pre>", options: .regularExpression, range: nil) | |
| // Convert links | |
| html = html.replacingOccurrences(of: #"\[([^\]]+)\]\(([^\)]+)\)"#, with: "<a href=\"$2\">$1</a>", options: .regularExpression, range: nil) | |
| // Convert images | |
| html = html.replacingOccurrences(of: #"!\[([^\]]*)\]\(([^\)]+)\)"#, with: "<img src=\"$2\" alt=\"$1\">", options: .regularExpression, range: nil) | |
| return html | |
| } | |
| private func generateCSS() -> String { | |
| return """ | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| color: #333; | |
| } | |
| pre { | |
| background: #f6f8fa; | |
| padding: 1rem; | |
| border-radius: 4px; | |
| overflow-x: auto; | |
| } | |
| img { | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 4px; | |
| margin: 1rem 0; | |
| } | |
| h1, h2, h3 { | |
| color: #2c3e50; | |
| } | |
| time { | |
| color: #666; | |
| display: block; | |
| margin-bottom: 2rem; | |
| } | |
| .post-preview { | |
| margin-bottom: 2rem; | |
| } | |
| .post-preview h2 { | |
| margin-bottom: 0.5rem; | |
| } | |
| .post-preview a { | |
| color: #2c3e50; | |
| text-decoration: none; | |
| } | |
| .post-preview a:hover { | |
| text-decoration: underline; | |
| } | |
| </style> | |
| """ | |
| } | |
| private func copyAssets() throws { | |
| let assetsDirectory = inputDirectory.appendingPathComponent("assets") | |
| let outputAssetsDirectory = outputDirectory.appendingPathComponent("assets") | |
| if FileManager.default.fileExists(atPath: assetsDirectory.path) { | |
| try FileManager.default.copyItem(at: assetsDirectory, to: outputAssetsDirectory) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment