Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] Directly deal with the Down AST, skip detour via XML #19

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 127 additions & 0 deletions Sources/Parma/Composer/Down.swift
@@ -0,0 +1,127 @@
import class Down.Document
import class Down.BlockQuote
import class Down.List
import class Down.Item
import class Down.CodeBlock
import class Down.Paragraph
import class Down.Heading
import class Down.Text
import class Down.Code
import class Down.Emphasis
import class Down.Strong
import class Down.Link
import class Down.Image
import protocol Down.Node

typealias DownNode = Down.Node

typealias DownDocument = Down.Document
typealias DownBlockQuote = Down.BlockQuote
typealias DownList = Down.List
typealias DownItem = Down.Item
typealias DownCodeBlock = Down.CodeBlock
typealias DownParagraph = Down.Paragraph
typealias DownHeading = Down.Heading
typealias DownText = Down.Text
typealias DownCode = Down.Code
typealias DownEmphasis = Down.Emphasis
typealias DownStrong = Down.Strong
typealias DownLink = Down.Link
typealias DownImage = Down.Image

extension DownNode {
var element: Element {
switch self {

case is DownBlockQuote: return .blockQuote
case is DownList: return .list
case is DownItem: return .item
case is DownCodeBlock: return .codeBlock
case is DownParagraph: return .paragraph
case is DownHeading: return .heading
case is DownText: return .text
case is DownCode: return .code
case is DownEmphasis: return .emphasis
case is DownStrong: return .strong
case is DownLink: return .link
case is DownImage: return .image

default: return .unknown
}
}

var attributes: [String: String] {
switch self {
case let node as DownList:
return node.attributes

case let node as DownHeading:
return node.attributes

case let node as DownLink:
return node.attributes

case let node as DownImage:
return node.attributes

default: return [:]
}
}
}

private extension Dictionary where Key == String, Value == String {
init(linkUrl url: String?, title: String?) {
var dict: [String: String] = [:]
if let url = url {
dict["destination"] = url
}

if let title = title {
dict["title"] = title
}

self = dict
}
}

extension DownLink {
var attributes: [String: String] {
return .init(linkUrl: url, title: title)
}
}

extension DownImage {
var attributes: [String: String] {
return .init(linkUrl: url, title: title)
}
}

extension DownList {
var attributes: [String: String] {
var result: [String: String] = [:]
switch listType {
case .bullet:
result["type"] = "bullet"

case .ordered(start: let start):
result["type"] = "ordered"
result["start"] = "\(start)"
}

switch delimiter {
case .paren: result["delim"] = "paren"
case .period: result["delim"] = "period"
case nil: break
}

return result
}
}

extension DownHeading {
var attributes: [String: String] {
var result: [String: String] = [:]
result["level"] = "\(headingLevel)"
return result
}
}
111 changes: 60 additions & 51 deletions Sources/Parma/ParmaCore.swift
Expand Up @@ -17,11 +17,11 @@ public typealias Text = SwiftUI.Text
/// The main logic of Parma.
class ParmaCore: NSObject {
// MARK: - Class property

// Composer collections
private var inlineComposers: [Element : InlineElementComposer] = [:]
private var blockComposers: [Element : BlockElementComposer] = [:]

// Composers
private let plaintTextComposer = PlainTextComposer()
private let strongElementComposer = StrongElementComposer()
Expand All @@ -34,19 +34,18 @@ class ParmaCore: NSObject {
private let listElementComposer = ListElementComposer()
private let listItemElementComposer = ListItemElementComposer()
private let unknownElementComposer = UnknownElementComposer()

private let parser: XMLParser



// Generated views
private var views: Array<AnyView> = []

// Temporary storage
private var texts: Array<Text> = []
private var foundCharacters = ""
private var concatenatedText: Text {
return texts.reduce(Text(""), +)
}

// MARK: - Public property
var composedView: AnyView {
AnyView(
Expand All @@ -55,28 +54,30 @@ class ParmaCore: NSObject {
}
)
}

/// The render for views.
var render: ParmaRenderable = ParmaRender()

/// The context for element composing.
let context = ComposingContext()


let rootNode: Document

// MARK: - Initialization
convenience init(_ markdown: String) throws {
let down = Down(markdownString: escapeContent(markdown))
let xml = try down.toXML()
self.init(xmlData: Data(xml.utf8))
let ast = try down.toAST()

guard let document = ast.wrap() as? DownDocument else {
fatalError("Expecting document. Bug in Down?")
}

self.init(rootNode: document)
}

/// Create a Parma core.
/// - Parameter xmlData: The xml data generated by Down.
init(xmlData: Data) {
parser = XMLParser(data: xmlData)
super.init()
parser.delegate = self

// Register composers

init(rootNode: DownDocument) {
self.rootNode = rootNode

inlineComposers =
[
.text : plaintTextComposer,
Expand All @@ -94,24 +95,25 @@ class ParmaCore: NSObject {
.item : listItemElementComposer,
.unknown : unknownElementComposer
]

}

/// Start composing views.
func start() {
parser.parse()
process(rootNode)
}
}

// MARK: - Private function

/// Sanitize input to prevent `<` and `>` from causing problems
private func escapeContent(_ rawContent: String) -> String {

enum EscapedCharacters: String, CaseIterable {

case leftAngleBracket = "<",
rightAngleBracket = ">"

func replacement() -> String {
switch self {
case .leftAngleBracket:
Expand All @@ -120,43 +122,55 @@ private func escapeContent(_ rawContent: String) -> String {
return "&gt;"
}
}

static func escapeString(_ string: String) -> String {
var escapedValue = string

self.allCases.forEach {
escapedValue = escapedValue.replacingOccurrences(of: $0.rawValue, with: $0.replacement())
}

return escapedValue
}
}

return EscapedCharacters.escapeString(rawContent)
}

// MARK: - XML parsing logic
extension ParmaCore: XMLParserDelegate {
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
// Start new element
let element = Element.element(elementName)


func process(_ node: Node) {
didStartElement(node)
if let text = node as? DownText, let content = text.literal {
context.foundCharacters += content
} else {
for child in node.childSequence {
process(child)
}
}
didEndElement(node)
}

func didStartElement(_ node: Node) {
let element = node.element

if element != .unknown {
context.enter(in: element)
}
context.attributes = attributeDict

context.attributes = node.attributes

if element.isInline {
inlineComposers[element]?.willStart(in: context)
} else {
blockComposers[element]?.willStart(in: context)
}
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
let element = Element.element(elementName)

func didEndElement(_ node: Node) {
let element = node.element

if element.isInline {
if let text = inlineComposers[element]?.text(in: context, render: render) {
if let superEl = context.superElement, superEl.isInline {
Expand All @@ -178,10 +192,10 @@ extension ParmaCore: XMLParserDelegate {
context.views.append(AnyView(concatenatedText))
}
}

texts = []
context.texts = []

if let view = blockComposers[element]?.view(in: context, render: render) {
if context.stack.count > 1 {
context.views.append(view)
Expand All @@ -190,19 +204,14 @@ extension ParmaCore: XMLParserDelegate {
views.append(view)
}
}

blockComposers[element]?.willStop(in: context)
}

context.foundCharacters = ""

if element != .unknown {
context.leaveElement()
}
}

func parser(_ parser: XMLParser, foundCharacters string: String) {
guard string.trimmingCharacters(in: .whitespacesAndNewlines) != "" else { return }
context.foundCharacters += string.trimmingCharacters(in: .newlines)
}
}