Skip to content

Commit

Permalink
Decompose Markdown.swift; Add simple CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
khlopko committed Mar 17, 2024
1 parent c58c6da commit 23df763
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 146 deletions.
31 changes: 26 additions & 5 deletions Cli/App.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
// Marker CLI entry point.
// (c) Kyrylo Khlopko

#if canImport(Darwin)
import Darwin
#else
import Glibc
#endif

import ArgumentParser

import DotMd

@main
struct App {
static func main() throws {
//let path = "Examples/markdown/bench.md"
let path = "README.md"
let md = try Markdown(path: path)
struct Cli: ParsableCommand {
@Argument
var inputFilePath: String

mutating func run() throws {
let file = fopen(inputFilePath, "r")
defer { fclose(file) }
var contents = ""
var buf = Array(repeating: CChar(0), count: 1024)
while fgets(&buf, Int32(1024), file) != nil {
if let chunk = String(validatingUTF8: buf) {
contents += chunk
}
}
let md = try Markdown(contents: contents)
print(md)
}
}
Expand Down
64 changes: 64 additions & 0 deletions Markdown/Lexer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Markdown lexer implementation
// (c) Kyrylo Khlopko

internal enum MarkdownToken {
case newline
case line(String)
case header(HeaderLevel)
}

internal struct Lexer {
private let contents: [Character]
private var lastPos = 0

init(contents: String) {
self.contents = Array(contents)
}

init(contents: [Character]) {
self.contents = contents
}

mutating func nextTok() -> MarkdownToken? {
guard lastPos < contents.count else {
return nil
}
let start = lastPos
switch contents[lastPos] {
case "\n":
lastPos += 1
return .newline
case "#":
return header(start: start)
default:
return line(start: start)
}
}

private mutating func header(start: Int) -> MarkdownToken {
var level = 0
while lastPos < contents.count && contents[lastPos] == "#" {
level += 1
lastPos += 1
}
guard
let headerLevel = HeaderLevel(rawValue: level),
contents[lastPos] == " "
else {
return line(start: start)
}
lastPos += 1
return .header(headerLevel)
}

private mutating func line(start: Int) -> MarkdownToken {
guard start < contents.count else {
return .line(String(contents[start...]))
}
while lastPos < contents.count && contents[lastPos] != "\n" {
lastPos += 1
}
return .line(String(contents[start..<lastPos]))
}
}

142 changes: 5 additions & 137 deletions Markdown/Markdown.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
// Markdown.swift

import Foundation
// Parsed markdown representation
// (c) Kyrylo Khlopko

public struct Markdown {
public let blocks: [Block]

public init(path: String) throws {
let contents = try String(contentsOfFile: path)
var parser = MarkdownParser(contents: Array(contents))
public init(contents: String) throws {
var parser = Parser(contents: Array(contents))
self.blocks = parser.parse()
}
}

extension Markdown: CustomStringConvertible {
public var description: String {
blocks.map { block in
block.description
}.joined(separator: "\n")
blocks.map(\.description).joined(separator: "\n")
}
}

Expand Down Expand Up @@ -54,131 +50,3 @@ public enum TextStyle: Equatable {
case regular
}

struct MarkdownParser {
private var lexer: MarkdownLexer

init(contents: String) {
lexer = MarkdownLexer(contents: contents)
}

init(contents: [Character]) {
lexer = MarkdownLexer(contents: contents)
}

private var blocks: [Block] = []
private var paragraphValue: String = ""
private var readingParagraph = false

mutating func parse() -> [Block] {
setup()
while let tok = lexer.nextTok() {
switch tok {
case .newline:
checkParagraph()
case let .line(value):
parseLine(value: value)
case let .header(level):
checkParagraph()
parseHeader(level: level)
}
}
checkParagraph()
return blocks
}

private mutating func setup() {
blocks = []
paragraphValue = ""
readingParagraph = false
}

private mutating func parseLine(value: String) {
if readingParagraph {
paragraphValue += "\n"
paragraphValue += value
} else {
readingParagraph = true
paragraphValue = value
}
_ = lexer.nextTok() // consume newline
}

private mutating func parseHeader(level: HeaderLevel) {
let nextTok = lexer.nextTok()
var headerValue: Block = .p([.text("", .regular)])
if case let .line(value) = nextTok {
headerValue = .p([.text(value, .regular)])
}
blocks.append(.h(level, headerValue))
_ = lexer.nextTok() // consume newline
}

private mutating func checkParagraph() {
if readingParagraph {
blocks.append(.p([.text(paragraphValue, .regular)]))
readingParagraph = false
}
}
}

enum MarkdownToken {
case newline
case line(String)
case header(HeaderLevel)
}

struct MarkdownLexer {
private let contents: [Character]
private var lastPos = 0

init(contents: String) {
self.contents = Array(contents)
}

init(contents: [Character]) {
self.contents = contents
}

mutating func nextTok() -> MarkdownToken? {
guard lastPos < contents.count else {
return nil
}
let start = lastPos
switch contents[lastPos] {
case "\n":
lastPos += 1
return .newline
case "#":
return header(start: start)
default:
return line(start: start)
}
}

private mutating func header(start: Int) -> MarkdownToken {
var level = 0
while lastPos < contents.count && contents[lastPos] == "#" {
level += 1
lastPos += 1
}
guard
let headerLevel = HeaderLevel(rawValue: level),
contents[lastPos] == " "
else {
return line(start: start)
}
lastPos += 1
return .header(headerLevel)
}

private mutating func line(start: Int) -> MarkdownToken {
guard start < contents.count else {
return .line(String(contents[start...]))
}
while lastPos < contents.count && contents[lastPos] != "\n" {
lastPos += 1
}
return .line(String(contents[start..<lastPos]))
}
}

69 changes: 69 additions & 0 deletions Markdown/Parser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Markdown parser implementation
// (c) Kyrylo Khlopko

internal struct Parser {
private var lexer: Lexer

init(contents: String) {
lexer = Lexer(contents: contents)
}

init(contents: [Character]) {
lexer = Lexer(contents: contents)
}

private var blocks: [Block] = []
private var paragraphValue: String = ""
private var readingParagraph = false

mutating func parse() -> [Block] {
setup()
while let tok = lexer.nextTok() {
switch tok {
case .newline:
checkParagraph()
case let .line(value):
parseLine(value: value)
case let .header(level):
checkParagraph()
parseHeader(level: level)
}
}
checkParagraph()
return blocks
}

private mutating func setup() {
blocks = []
paragraphValue = ""
readingParagraph = false
}

private mutating func parseLine(value: String) {
if readingParagraph {
paragraphValue += "\n"
paragraphValue += value
} else {
readingParagraph = true
paragraphValue = value
}
_ = lexer.nextTok() // consume newline
}

private mutating func parseHeader(level: HeaderLevel) {
let nextTok = lexer.nextTok()
var headerValue: Block = .p([.text("", .regular)])
if case let .line(value) = nextTok {
headerValue = .p([.text(value, .regular)])
}
blocks.append(.h(level, headerValue))
_ = lexer.nextTok() // consume newline
}

private mutating func checkParagraph() {
if readingParagraph {
blocks.append(.p([.text(paragraphValue, .regular)]))
readingParagraph = false
}
}
}
11 changes: 10 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"originHash" : "ccfcb9545e17b683fc71f240b434fd9e699c7ad431a4508c9f6f5aef1185ef39",
"originHash" : "8d3c5bcf931d8172b0afa33fe2a5e54c7b37c0c93c050aa2b4f6a78e0b00dc1d",
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "46989693916f56d1186bd59ac15124caef896560",
"version" : "1.3.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-testing.git", branch: "main"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
],
targets: [
.executableTarget(
name: "Marker",
dependencies: [
.target(name: "DotMd"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
path: "Cli"
),
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Marker
[![tests](https://github.com/khlopko/marker/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/khlopko/marker/actions/workflows/test.yml)

A static web site generator, written in Swift, with a focus on simplicity and ease of use.
# Marker
❤️ A static web site generator, written in Swift, with a focus on simplicity and ease of use.

## (Upcoming) Features

Expand Down

0 comments on commit 23df763

Please sign in to comment.