Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Commit

Permalink
Subtext recursive descent parser (#3)
Browse files Browse the repository at this point in the history
Re-implementing Subtext via recursive descent.

- Two passes instead of 4x passes with Regexp
- Gives us a DOM to work with so we can extract titles, etc, later on
  • Loading branch information
gordonbrander authored Nov 1, 2021
1 parent 932f7d1 commit 6da9c52
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 142 deletions.
6 changes: 2 additions & 4 deletions xcode/Subconscious/Shared/Components/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,8 @@ struct AppModel: Updatable {
markup: String,
selection: NSRange
) -> NSAttributedString {
Subtext4(
markup: markup,
range: selection
).renderMarkup(url: Slashlink.slashlinkToURLString)
Subtext(markup: markup)
.renderMarkup(url: Slashlink.slashlinkToURLString)
}

// MARK: Update
Expand Down
211 changes: 211 additions & 0 deletions xcode/Subconscious/Shared/Library/Subtext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
//
// Subtext5.swift
// Subconscious
//
// Created by Gordon Brander on 10/25/21.
//

import Foundation
import SwiftUI

struct Subtext {
enum Block {
case text(span: Substring, inline: [Inline])
case list(span: Substring, inline: [Inline])
case quote(span: Substring, inline: [Inline])
case heading(span: Substring)

/// Returns the body of a block, without the leading sigil
func body() -> Substring {
switch self {
case .text(let span, _):
return span
case .quote(let span, _), .list(let span, _), .heading(let span):
return span.dropFirst()
}
}
}

struct Link {
var span: Substring
}

struct Bracketlink {
var span: Substring

func body() -> Substring {
span.dropFirst().dropLast()
}
}

struct Slashlink {
var span: Substring
}

enum Inline {
case link(Link)
case bracketlink(Bracketlink)
case slashlink(Slashlink)
}

/// Consume a well-formed bracket link, or else backtrack
private static func consumeBracketLink(tape: inout Tape<Substring>) -> Substring? {
tape.save()
while !tape.isExhausted() {
if tape.consumeMatch(" ") {
tape.backtrack()
return nil
} else if tape.consumeMatch(">") {
return tape.cut()
} else {
tape.consume()
}
}
tape.backtrack()
return nil
}

private static func consumeInlineWordBoundaryForm(
tape: inout Tape<Substring>
) -> Inline? {
if tape.consumeMatch("<") {
if let link = consumeBracketLink(tape: &tape) {
return .bracketlink(Bracketlink(span: link))
} else {
return nil
}
} else if tape.consumeMatch("https://") {
tape.consumeUntil(" ")
return .link(Link(span: tape.cut()))
} else if tape.consumeMatch("http://") {
tape.consumeUntil(" ")
return .link(Link(span: tape.cut()))
} else if tape.consumeMatch("/") {
tape.consumeUntil(" ")
return .slashlink(Slashlink(span: tape.cut()))
} else {
return nil
}
}

private static func parseInline(tape: inout Tape<Substring>) -> [Inline] {
var inlines: [Inline] = []

/// Capture word-boundary-delimited forms at beginning of line.
tape.start()
if let inline = consumeInlineWordBoundaryForm(tape: &tape) {
inlines.append(inline)
}

while !tape.isExhausted() {
tape.start()
let curr = tape.consume()
/// Capture word-boundary-delimited forms after space
if curr == " " {
tape.start()
if let inline = consumeInlineWordBoundaryForm(tape: &tape) {
inlines.append(inline)
}
}
}

return inlines
}

private static func parseLine(_ line: Substring) -> Block {
if line.hasPrefix("#") {
return Block.heading(span: line)
} else if line.hasPrefix(">") {
var tape = Tape(line)
// Discard prefix
tape.consume()
let inline = parseInline(tape: &tape)
return Block.quote(span: line, inline: inline)
} else if line.hasPrefix("-") {
var tape = Tape(line)
// Discard prefix
tape.consume()
let inline = parseInline(tape: &tape)
return Block.list(span: line, inline: inline)
} else {
var tape = Tape(line)
let inline = parseInline(tape: &tape)
return Block.list(span: line, inline: inline)
}
}

let base: String
let blocks: [Block]

init(markup: String) {
self.base = markup
self.blocks = markup.split(
omittingEmptySubsequences: false,
whereSeparator: \.isNewline
).map(Self.parseLine)
}
}

extension Subtext {
/// Render markup verbatim with syntax highlighting and links
func renderMarkup(url: (String) -> String?) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: base)
// Set default styles for entire string
attributedString.addAttribute(
.font,
value: UIFont.appText,
range: NSRange(base.startIndex..<base.endIndex, in: base)
)

for block in blocks {
switch block {
case let .heading(line):
let nsRange = NSRange(line.range, in: attributedString.string)
attributedString.addAttribute(
.font,
value: UIFont.appTextBold,
range: nsRange
)
case
.list(_, let inline),
.quote(_, let inline),
.text(_, let inline):
for inline in inline {
switch inline {
case let .link(link):
attributedString.addAttribute(
.link,
value: link.span,
range: NSRange(
link.span.range,
in: attributedString.string
)
)
case let .bracketlink(bracketlink):
attributedString.addAttribute(
.link,
value: bracketlink.body(),
range: NSRange(
bracketlink.body().range,
in: attributedString.string
)
)
case let .slashlink(slashlink):
if let url = url(String(slashlink.span)) {
attributedString.addAttribute(
.link,
value: url,
range: NSRange(
slashlink.span.range,
in: attributedString.string
)
)
}
}
}
}
}

return attributedString
}
}
130 changes: 0 additions & 130 deletions xcode/Subconscious/Shared/Library/Subtext4.swift

This file was deleted.

Loading

0 comments on commit 6da9c52

Please sign in to comment.