diff --git a/Sources/Parma/Composer/Down.swift b/Sources/Parma/Composer/Down.swift new file mode 100644 index 0000000..e0d7b49 --- /dev/null +++ b/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 + } +} diff --git a/Sources/Parma/ParmaCore.swift b/Sources/Parma/ParmaCore.swift index 053b640..8b4ca90 100644 --- a/Sources/Parma/ParmaCore.swift +++ b/Sources/Parma/ParmaCore.swift @@ -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() @@ -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 = [] - + // Temporary storage private var texts: Array = [] private var foundCharacters = "" private var concatenatedText: Text { return texts.reduce(Text(""), +) } - + // MARK: - Public property var composedView: AnyView { AnyView( @@ -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, @@ -94,11 +95,12 @@ class ParmaCore: NSObject { .item : listItemElementComposer, .unknown : unknownElementComposer ] + } - + /// Start composing views. func start() { - parser.parse() + process(rootNode) } } @@ -106,12 +108,12 @@ class ParmaCore: NSObject { /// 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: @@ -120,43 +122,55 @@ private func escapeContent(_ rawContent: String) -> String { return ">" } } - + 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 { @@ -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) @@ -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) - } }