diff --git a/Cli/App.swift b/Cli/App.swift index 75a932b..bb29d38 100644 --- a/Cli/App.swift +++ b/Cli/App.swift @@ -16,6 +16,14 @@ struct Cli: ParsableCommand { @Argument var inputFilePath: String + enum OutputFormat: String, ExpressibleByArgument { + case html + case raw + } + + @Option(name: .shortAndLong) + var outputFormat: OutputFormat = .raw + @Flag var debug: Bool = false @@ -29,11 +37,19 @@ struct Cli: ParsableCommand { contents += chunk } } - let md = try Markdown(contents: contents) + let pathComponents = inputFilePath.split(separator: "/") + let title = String(pathComponents.last!) + let md = try Markdown(title: title, contents: contents) if debug { print(md.debugDescription) } else { - print(md.description) + switch outputFormat { + case .html: + let renderer = HTMLRenderer(markdown: md) + print(renderer.render()) + case .raw: + print(md.description) + } } } } diff --git a/Markdown/HTMLRenderer.swift b/Markdown/HTMLRenderer.swift new file mode 100644 index 0000000..1a1f214 --- /dev/null +++ b/Markdown/HTMLRenderer.swift @@ -0,0 +1,57 @@ +public struct HTMLRenderer { + private let markdown: Markdown + + public init(markdown: Markdown) { + self.markdown = markdown + } + + public func render() -> String { + """ + + + + + \(markdown.title) + + + + \(markdown.blocks.map { render($0) }.joined(separator: "\n")) + + + """ + } + + private func render(_ block: Block) -> String { + switch block { + case let .p(text): + "

\(text.map { render($0) }.joined())

" + case let .text(value, style): + "\(value)" + case let .list(blocks): + "" + case let .code(value): + "
\(value)
" + case let .h(level, blocks): + "\(blocks.map { render($0) }.joined())" + } + } +} diff --git a/Markdown/Lexer.swift b/Markdown/Lexer.swift index 01236a3..e460ef1 100644 --- a/Markdown/Lexer.swift +++ b/Markdown/Lexer.swift @@ -4,7 +4,24 @@ internal enum MarkdownToken { case newline case line(String) + case list case header(HeaderLevel) + case codeBlock(lang: String?) + + var rawValue: String { + switch self { + case .newline: + return "\n" + case let .line(value): + return value + case .list: + return "_" + case let .header(level): + return Array(repeating: "#", count: level.rawValue).joined() + case let .codeBlock(lang): + return "```\(lang ?? "")" + } + } } internal struct Lexer { @@ -30,6 +47,24 @@ internal struct Lexer { return .newline case "#": return header(start: start) + case "-": + lastPos += 1 + return .list + case "`": + // consume 3 backticks + let start = lastPos + while lastPos < contents.count && lastPos - start < 3 && contents[lastPos] == "`" { + lastPos += 1 + } + var lang: String? = nil + /* + while lastPos < contents.count && (contents[lastPos] != "\n" || contents[lastPos] != " ") { + lastPos += 1 + } + lang = String(contents[start + 3.. [Block] { setup() while let tok = lexer.nextTok() { + if case .line = tok { + // do nothing + } else { + checkParagraph() + } switch tok { case .newline: - checkParagraph() + break case let .line(value): parseLine(value: value) case let .header(level): - checkParagraph() parseHeader(level: level) + case .list: + parseList() + case .codeBlock: + parseCodeBlock() } } checkParagraph() @@ -52,14 +60,43 @@ internal struct Parser { private mutating func parseHeader(level: HeaderLevel) { let nextTok = lexer.nextTok() - var headerValue: Block = .p([.text("", .regular)]) + var components: [Block] = [] if case let .line(value) = nextTok { - headerValue = .p([.text(value, .regular)]) + components.append(.text(value, .regular)) } - blocks.append(.h(level, headerValue)) + blocks.append(.h(level, components)) _ = lexer.nextTok() // consume newline } + private mutating func parseList() { + var items: [Block] = [] + var consequentiveLines = 0 + while consequentiveLines < 2, let tok = lexer.nextTok() { + switch tok { + case .list: + consequentiveLines = 0 + case let .line(value): + items.append(.text(value, .regular)) + default: + consequentiveLines += 1 + } + } + blocks.append(.list(items)) + } + + private mutating func parseCodeBlock() { + var value = "" + while let tok = lexer.nextTok() { + switch tok { + case .codeBlock: + blocks.append(.code(value)) + return + default: + value += tok.rawValue + } + } + } + private mutating func checkParagraph() { if readingParagraph { blocks.append(.p([.text(paragraphValue, .regular)]))