#!/usr/bin/env swift
import AppKit
import Foundation
func fail(_ message: String) -> Never {
FileHandle.standardError.write(Data((message + "\n").utf8))
exit(1)
}
func htmlEscaped(_ value: String) -> String {
var escaped = ""
escaped.reserveCapacity(value.count)
for character in value {
switch character {
case "&":
escaped += "&"
case "<":
escaped += "<"
case ">":
escaped += ">"
case "\"":
escaped += """
default:
escaped.append(character)
}
}
return escaped
}
func renderInlineMarkdown(_ value: String) -> String {
let escaped = htmlEscaped(value)
let pieces = escaped.split(separator: "`", omittingEmptySubsequences: false)
guard pieces.count > 1 else {
return escaped
}
return pieces.enumerated().map { index, piece in
index.isMultiple(of: 2) ? String(piece) : "\(piece)"
}.joined()
}
func absoluteFileURL(_ path: String) -> URL {
if path.hasPrefix("/") {
return URL(fileURLWithPath: path).standardizedFileURL
}
return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
.appendingPathComponent(path)
.standardizedFileURL
}
func markdownToHTML(_ markdown: String) -> String {
var body: [String] = []
var paragraph: [String] = []
var inCodeBlock = false
var codeLines: [String] = []
var previousListItem = false
func flushParagraph() {
guard !paragraph.isEmpty else {
return
}
body.append("
\(paragraph.map(renderInlineMarkdown).joined(separator: " "))
") paragraph.removeAll() previousListItem = false } func flushCodeBlock() { guard !codeLines.isEmpty else { return } body.append("\(htmlEscaped(codeLines.joined(separator: "\n")))")
codeLines.removeAll()
previousListItem = false
}
let headingPattern = try! NSRegularExpression(pattern: "^(#{1,6})\\s+(.+)$")
let orderedListPattern = try! NSRegularExpression(pattern: "^\\s*(\\d+)\\.\\s+(.+)$")
let unorderedListPattern = try! NSRegularExpression(pattern: "^\\s*-\\s+(.+)$")
for rawLine in markdown.components(separatedBy: .newlines) {
let line = rawLine.trimmingCharacters(in: .whitespaces)
if line.hasPrefix("```") {
if inCodeBlock {
flushCodeBlock()
inCodeBlock = false
} else {
flushParagraph()
inCodeBlock = true
previousListItem = false
}
continue
}
if inCodeBlock {
codeLines.append(rawLine)
continue
}
if line.isEmpty {
flushParagraph()
previousListItem = false
continue
}
let lineRange = NSRange(line.startIndex..") } body.append("
\(String(repeating: "─", count: 85))
") } previousListItem = false continue } if let match = orderedListPattern.firstMatch(in: line, range: lineRange), let numberRange = Range(match.range(at: 1), in: line), let itemRange = Range(match.range(at: 2), in: line) { flushParagraph() let marker = htmlEscaped(String(line[numberRange])) + "." body.append( "\(marker) \(renderInlineMarkdown(String(line[itemRange])))
" ) previousListItem = true continue } if let match = unorderedListPattern.firstMatch(in: line, range: lineRange), let itemRange = Range(match.range(at: 1), in: line) { flushParagraph() body.append("• \(renderInlineMarkdown(String(line[itemRange])))
") previousListItem = true continue } if previousListItem && rawLine.hasPrefix(" ") { body.append("\(renderInlineMarkdown(line))
") } else { paragraph.append(line) previousListItem = false } } if inCodeBlock { flushCodeBlock() } flushParagraph() return """ \(body.joined(separator: "\n")) """ } let arguments = CommandLine.arguments guard arguments.count == 3 else { fail("Usage: scripts/ios-app-review-notes-pdf.swift