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

Subtext recursive descent parser #3

Merged
merged 7 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, no longer versioned 📝

.renderMarkup(url: Slashlink.slashlinkToURLString)
}

// MARK: Update
Expand Down
209 changes: 209 additions & 0 deletions xcode/Subconscious/Shared/Library/Subtext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//
// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only know about the hyperlink and slashlink link forms - what is the format/semantics of bracketlink?

Copy link
Collaborator Author

@gordonbrander gordonbrander Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cdata Covered here in the specification: https://github.com/gordonbrander/subtext/blob/main/specification.md#bracketed-urls. TLDR, they are a syntax form that allows linking to non-http/https protocols.

This won't link:
ipfs://asdfasdfasfasdf
dat://asdfasdfasfasdf

This will link:
<ipfs://asdfasdfasfasdf>
<dat://asdfasdfasfasdf>

You can write HTTP urls either way:
This will link: https://example.com
So will this: <https://example.com>

URL syntax is very very open-ended, so it is difficult to auto-link the general form of URLs. Bracket links mean we can autolink http urls, and support other protocols without forcing parsers to maintain a whitelist of exotic protocols. Easy things are easy, difficult things are possible.

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be possible to satisfy this condition as part of the loop to reduce the method's complexity a little.

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)
}

/// Render markup verbatim with syntax highlighting and links
func renderMarkup(url: (String) -> String?) -> NSAttributedString {
gordonbrander marked this conversation as resolved.
Show resolved Hide resolved
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