Skip to content

Commit

Permalink
In progress: parsing code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
khlopko committed Jun 12, 2024
1 parent 014dd22 commit 5628a17
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 10 deletions.
20 changes: 18 additions & 2 deletions Cli/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions Markdown/HTMLRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
public struct HTMLRenderer {
private let markdown: Markdown

public init(markdown: Markdown) {
self.markdown = markdown
}

public func render() -> String {
"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>\(markdown.title)</title>
<style>
body {
font-family: sans-serif;
color: #333;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
pre {
background-color: #f4f4f4;
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
\(markdown.blocks.map { render($0) }.joined(separator: "\n"))
</body>
</html>
"""
}

private func render(_ block: Block) -> String {
switch block {
case let .p(text):
"<p>\(text.map { render($0) }.joined())</p>"
case let .text(value, style):
"<span class=\"\(style)\">\(value)</span>"
case let .list(blocks):
"<ul>\(blocks.map { "<li>\(render($0))</li>" }.joined())</ul>"
case let .code(value):
"<pre><code>\(value)</code></pre>"
case let .h(level, blocks):
"<h\(level.rawValue)>\(blocks.map { render($0) }.joined())</h\(level.rawValue)>"
}
}
}
35 changes: 35 additions & 0 deletions Markdown/Lexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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..<lastPos])
print(lastPos, lang)
*/
return .codeBlock(lang: lang)
default:
return line(start: start)
}
Expand Down
20 changes: 17 additions & 3 deletions Markdown/Markdown.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Parsed markdown representation
// Parsed markdown representation
// (c) Kyrylo Khlopko

public struct Markdown {
public let title: String
public let blocks: [Block]

public init(contents: String) throws {
public init(title: String, contents: String) throws {
self.title = title
var parser = Parser(contents: Array(contents))
self.blocks = parser.parse()
}
Expand All @@ -25,7 +27,9 @@ extension Markdown: CustomDebugStringConvertible {
public enum Block: Equatable {
case p([Block])
case text(String, TextStyle)
indirect case h(HeaderLevel, Block)
case list([Block])
case code(String)
indirect case h(HeaderLevel, [Block])
}

extension Block: CustomStringConvertible {
Expand All @@ -37,6 +41,12 @@ extension Block: CustomStringConvertible {
}.joined(separator: "") + "\n"
case let .text(value, _):
return value
case let .list(blocks):
return blocks.map { block in
block.description
}.joined(separator: "") + "\n"
case let .code(value):
return value
case let .h(level, block):
return "\(String(Array(repeating: "#", count: level.rawValue))) \(block.description)"
}
Expand All @@ -50,6 +60,10 @@ extension Block: CustomDebugStringConvertible {
return "p(\(blocks.map(\.debugDescription).joined(separator: ", ")))"
case let .text(value, style):
return "text(\(value), \(style))"
case let .list(blocks):
return "list(\(blocks.map(\.debugDescription).joined(separator: ", ")))"
case let .code(value):
return "cade(\(value))"
case let .h(level, block):
return "h(\(level), \(block.debugDescription))"
}
Expand Down
47 changes: 42 additions & 5 deletions Markdown/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ internal struct Parser {
mutating func parse() -> [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()
Expand All @@ -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)]))
Expand Down

0 comments on commit 5628a17

Please sign in to comment.