From 765899fd7f689e73e7b3ff39bee71d9ae3f9ee91 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Tue, 28 Apr 2020 12:18:54 +0200 Subject: [PATCH 01/12] Added support for DOM walking using a visitor --- .../Supporting Types/Children.swift | 8 +- Sources/CommonMark/Visitable.swift | 229 ++++++++++++++++++ Sources/CommonMark/Visitor.swift | 132 ++++++++++ Tests/CommonMarkTests/VisitorTests.swift | 82 +++++++ 4 files changed, 449 insertions(+), 2 deletions(-) create mode 100644 Sources/CommonMark/Visitable.swift create mode 100644 Sources/CommonMark/Visitor.swift create mode 100644 Tests/CommonMarkTests/VisitorTests.swift diff --git a/Sources/CommonMark/Supporting Types/Children.swift b/Sources/CommonMark/Supporting Types/Children.swift index 36f2891..5392255 100644 --- a/Sources/CommonMark/Supporting Types/Children.swift +++ b/Sources/CommonMark/Supporting Types/Children.swift @@ -52,7 +52,9 @@ fileprivate func add(_ child: Child, with operation: () -> Int32) - // MARK: - -public protocol ContainerOfBlocks: Node {} +public protocol ContainerOfBlocks: Node { + var children: [Block & Node] { get } +} extension Document: ContainerOfBlocks {} extension BlockQuote: ContainerOfBlocks {} @@ -166,7 +168,9 @@ extension ContainerOfBlocks { // MARK: - -public protocol ContainerOfInlineElements: Node {} +public protocol ContainerOfInlineElements: Node { + var children: [Inline & Node] { get } +} extension Heading: ContainerOfInlineElements {} extension Paragraph: ContainerOfInlineElements {} diff --git a/Sources/CommonMark/Visitable.swift b/Sources/CommonMark/Visitable.swift new file mode 100644 index 0000000..082f509 --- /dev/null +++ b/Sources/CommonMark/Visitable.swift @@ -0,0 +1,229 @@ +public protocol Visitable { + func accept(visitor: T) +} + +// MARK: - Document + +extension Document: Visitable { + public func accept(visitor: T) { + guard visitor.visit(document: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +// MARK: - Container Blocks + +extension BlockQuote: Visitable { + public func accept(visitor: T) { + guard visitor.visit(blockQuote: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension List: Visitable { + public func accept(visitor: T) { + guard visitor.visit(list: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension List.Item: Visitable { + public func accept(visitor: T) { + guard visitor.visit(listItem: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +// MARK: - Leaf Blocks + +extension Heading: Visitable { + public func accept(visitor: T) { + guard visitor.visit(heading: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension Paragraph: Visitable { + public func accept(visitor: T) { + guard visitor.visit(paragraph: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension HTMLBlock: Visitable { + public func accept(visitor: T) { + guard visitor.visit(htmlBlock: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension CodeBlock: Visitable { + public func accept(visitor: T) { + guard visitor.visit(codeBlock: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension ThematicBreak: Visitable { + public func accept(visitor: T) { + guard visitor.visit(thematicBreak: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +// MARK: - Inline + +extension Text: Visitable { + public func accept(visitor: T) { + guard visitor.visit(text: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +extension Strong: Visitable { + public func accept(visitor: T) { + guard visitor.visit(strong: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension Emphasis: Visitable { + public func accept(visitor: T) { + guard visitor.visit(emphasis: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension Link: Visitable { + public func accept(visitor: T) { + guard visitor.visit(link: self) == .visitChildren else { + return + } + self.walkVisitableChildren(with: visitor) + } +} + +extension Image: Visitable { + public func accept(visitor: T) { + guard visitor.visit(image: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +extension Code: Visitable { + public func accept(visitor: T) { + guard visitor.visit(code: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +extension RawHTML: Visitable { + public func accept(visitor: T) { + guard visitor.visit(rawHTML: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +extension SoftLineBreak: Visitable { + public func accept(visitor: T) { + guard visitor.visit(softLineBreak: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +extension HardLineBreak: Visitable { + public func accept(visitor: T) { + guard visitor.visit(hardLineBreak: self) == .visitChildren else { + return + } + // type has no visitable children for now + } +} + +// MARK: - Convenience Helpers + +extension ContainerOfBlocks { + internal var visitableChildren: AnyCollection { + return AnyCollection(self.children.lazy.compactMap { + $0 as? Visitable & Block & Node + }) + } + + internal func walkVisitableChildren(with visitor: T) { + for child in self.visitableChildren { + child.accept(visitor: visitor) + } + } +} + +extension ContainerOfInlineElements { + internal var visitableChildren: AnyCollection { + return AnyCollection(self.children.lazy.compactMap { + $0 as? Visitable & Inline & Node + }) + } + + internal func walkVisitableChildren(with visitor: T) { + for child in self.visitableChildren { + child.accept(visitor: visitor) + } + } +} + +extension List { + internal var visitableChildren: AnyCollection { + return AnyCollection(self.children) + } + + internal func walkVisitableChildren(with visitor: T) { + for child in self.visitableChildren { + child.accept(visitor: visitor) + } + } +} + +extension List.Item { + internal var visitableChildren: AnyCollection { + return AnyCollection(self.children.lazy.compactMap { + $0 as? Visitable & Node + }) + } + + internal func walkVisitableChildren(with visitor: T) { + for child in self.visitableChildren { + child.accept(visitor: visitor) + } + } +} diff --git a/Sources/CommonMark/Visitor.swift b/Sources/CommonMark/Visitor.swift new file mode 100644 index 0000000..a6360ac --- /dev/null +++ b/Sources/CommonMark/Visitor.swift @@ -0,0 +1,132 @@ +public enum VisitorContinueKind { + /// The visitor should visit the descendents of the current node. + case visitChildren + + /// The visitor should avoid visiting the descendents of the current node. + case skipChildren + + /// The default is `.visitChildren` + static let `default`: Self = .visitChildren +} + +public protocol Visitor { + var defaultContinueKind: VisitorContinueKind { get } + + func walk(_ visitable: T) + + // MARK: - Document + + func visit(document: Document) -> VisitorContinueKind + + // MARK: - Container Blocks + + func visit(blockQuote: BlockQuote) -> VisitorContinueKind + func visit(list: List) -> VisitorContinueKind + func visit(listItem: List.Item) -> VisitorContinueKind + + // MARK: - Leaf Blocks + + func visit(heading: Heading) -> VisitorContinueKind + func visit(paragraph: Paragraph) -> VisitorContinueKind + func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind + func visit(codeBlock: CodeBlock) -> VisitorContinueKind + func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind + + // MARK: - Inline + + func visit(text: Text) -> VisitorContinueKind + func visit(strong: Strong) -> VisitorContinueKind + func visit(emphasis: Emphasis) -> VisitorContinueKind + func visit(link: Link) -> VisitorContinueKind + @discardableResult + func visit(image: Image) -> VisitorContinueKind + @discardableResult + func visit(code: Code) -> VisitorContinueKind + @discardableResult + func visit(rawHTML: RawHTML) -> VisitorContinueKind + @discardableResult + func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind + @discardableResult + func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind +} + +extension Visitor { + public var defaultContinueKind: VisitorContinueKind { + return.visitChildren + } + + public func walk(_ visitable: T) { + visitable.accept(visitor: self) + } + + // MARK: - Document + + public func visit(document: Document) -> VisitorContinueKind { + return self.defaultContinueKind + } + + // MARK: - Container Blocks + + public func visit(blockQuote: BlockQuote) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(list: List) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(listItem: List.Item) -> VisitorContinueKind { + return self.defaultContinueKind + } + + // MARK: - Leaf Blocks + + public func visit(heading: Heading) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(paragraph: Paragraph) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(codeBlock: CodeBlock) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { + return self.defaultContinueKind + } + + // MARK: - Inline + + public func visit(text: Text) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(strong: Strong) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(emphasis: Emphasis) -> VisitorContinueKind { + return self.defaultContinueKind + } + public func visit(link: Link) -> VisitorContinueKind { + return self.defaultContinueKind + } + @discardableResult + public func visit(image: Image) -> VisitorContinueKind { + return self.defaultContinueKind + } + @discardableResult + public func visit(code: Code) -> VisitorContinueKind { + return self.defaultContinueKind + } + @discardableResult + public func visit(rawHTML: RawHTML) -> VisitorContinueKind { + return self.defaultContinueKind + } + @discardableResult + public func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { + return self.defaultContinueKind + } + @discardableResult + public func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { + return self.defaultContinueKind + } +} diff --git a/Tests/CommonMarkTests/VisitorTests.swift b/Tests/CommonMarkTests/VisitorTests.swift new file mode 100644 index 0000000..e51caaa --- /dev/null +++ b/Tests/CommonMarkTests/VisitorTests.swift @@ -0,0 +1,82 @@ +import XCTest +import CommonMark + +final class VisitorTests: XCTestCase { + func testVisitorVisitingChildren() throws { + final class TestVisitor: Visitor { + var numberOfHeadings: Int = 0 + var numberOfParagraphs: Int = 0 + var numberOfLinks: Int = 0 + + var defaultContinueKind: VisitorContinueKind { + return .visitChildren + } + + func visit(heading: Heading) -> VisitorContinueKind { + self.numberOfHeadings += 1 + + return .visitChildren + } + + func visit(paragraph: Paragraph) -> VisitorContinueKind { + self.numberOfParagraphs += 1 + + return .visitChildren + } + + func visit(link: Link) -> VisitorContinueKind { + self.numberOfLinks += 1 + + return .visitChildren + } + } + + let document = try Document(Fixtures.uhdr) + let visitor = TestVisitor() + + visitor.walk(document) + + XCTAssertEqual(visitor.numberOfHeadings, 2) + XCTAssertEqual(visitor.numberOfParagraphs, 1) + XCTAssertEqual(visitor.numberOfLinks, 1) + } + + func testVisitorSkippingChildren() throws { + final class TestVisitor: Visitor { + var numberOfHeadings: Int = 0 + var numberOfParagraphs: Int = 0 + var numberOfLinks: Int = 0 + + var defaultContinueKind: VisitorContinueKind { + return .visitChildren + } + + func visit(heading: Heading) -> VisitorContinueKind { + self.numberOfHeadings += 1 + + return .skipChildren + } + + func visit(paragraph: Paragraph) -> VisitorContinueKind { + self.numberOfParagraphs += 1 + + return .skipChildren + } + + func visit(link: Link) -> VisitorContinueKind { + self.numberOfLinks += 1 + + return .skipChildren + } + } + + let document = try Document(Fixtures.uhdr) + let visitor = TestVisitor() + + visitor.walk(document) + + XCTAssertEqual(visitor.numberOfHeadings, 2) + XCTAssertEqual(visitor.numberOfParagraphs, 1) + XCTAssertEqual(visitor.numberOfLinks, 0) + } +} From 31c6d661ca81e287b98d7e79ad448f3731746fa8 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Tue, 28 Apr 2020 18:22:27 +0200 Subject: [PATCH 02/12] Added support for suptyping-ish overriding of visitors --- Sources/CommonMark/Visitable.swift | 108 +++++----- Sources/CommonMark/Visitor.swift | 174 +++++++++++++--- Tests/CommonMarkTests/StatisticsVisitor.swift | 161 +++++++++++++++ Tests/CommonMarkTests/VisitorTests.swift | 194 +++++++++++++----- 4 files changed, 504 insertions(+), 133 deletions(-) create mode 100644 Tests/CommonMarkTests/StatisticsVisitor.swift diff --git a/Sources/CommonMark/Visitable.swift b/Sources/CommonMark/Visitable.swift index 082f509..899ac4e 100644 --- a/Sources/CommonMark/Visitable.swift +++ b/Sources/CommonMark/Visitable.swift @@ -6,10 +6,10 @@ public protocol Visitable { extension Document: Visitable { public func accept(visitor: T) { - guard visitor.visit(document: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(document: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } @@ -17,28 +17,28 @@ extension Document: Visitable { extension BlockQuote: Visitable { public func accept(visitor: T) { - guard visitor.visit(blockQuote: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(blockQuote: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension List: Visitable { public func accept(visitor: T) { - guard visitor.visit(list: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(list: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension List.Item: Visitable { public func accept(visitor: T) { - guard visitor.visit(listItem: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(listItem: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } @@ -46,46 +46,46 @@ extension List.Item: Visitable { extension Heading: Visitable { public func accept(visitor: T) { - guard visitor.visit(heading: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(heading: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension Paragraph: Visitable { public func accept(visitor: T) { - guard visitor.visit(paragraph: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(paragraph: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension HTMLBlock: Visitable { public func accept(visitor: T) { - guard visitor.visit(htmlBlock: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(htmlBlock: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension CodeBlock: Visitable { public func accept(visitor: T) { - guard visitor.visit(codeBlock: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(codeBlock: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension ThematicBreak: Visitable { public func accept(visitor: T) { - guard visitor.visit(thematicBreak: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(thematicBreak: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } @@ -93,82 +93,82 @@ extension ThematicBreak: Visitable { extension Text: Visitable { public func accept(visitor: T) { - guard visitor.visit(text: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(text: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } extension Strong: Visitable { public func accept(visitor: T) { - guard visitor.visit(strong: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(strong: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension Emphasis: Visitable { public func accept(visitor: T) { - guard visitor.visit(emphasis: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(emphasis: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension Link: Visitable { public func accept(visitor: T) { - guard visitor.visit(link: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(link: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) } - self.walkVisitableChildren(with: visitor) } } extension Image: Visitable { public func accept(visitor: T) { - guard visitor.visit(image: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(image: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } extension Code: Visitable { public func accept(visitor: T) { - guard visitor.visit(code: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(code: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } extension RawHTML: Visitable { public func accept(visitor: T) { - guard visitor.visit(rawHTML: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(rawHTML: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } extension SoftLineBreak: Visitable { public func accept(visitor: T) { - guard visitor.visit(softLineBreak: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(softLineBreak: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } extension HardLineBreak: Visitable { public func accept(visitor: T) { - guard visitor.visit(hardLineBreak: self) == .visitChildren else { - return + let continueKind = visitor.visit(self, by: visitor.visit(hardLineBreak: self)) + if continueKind == .visitChildren { + // type has no visitable children for now } - // type has no visitable children for now } } diff --git a/Sources/CommonMark/Visitor.swift b/Sources/CommonMark/Visitor.swift index a6360ac..39ca255 100644 --- a/Sources/CommonMark/Visitor.swift +++ b/Sources/CommonMark/Visitor.swift @@ -5,27 +5,68 @@ public enum VisitorContinueKind { /// The visitor should avoid visiting the descendents of the current node. case skipChildren + case inherit + /// The default is `.visitChildren` static let `default`: Self = .visitChildren + + mutating func override(with other: Self) { + switch other { + case .visitChildren, .skipChildren: + self = other + case .inherit: + break + } + } } +/// Visitor for walking a visitable structure. +/// +/// The order of object-wise visitations is: +/// +/// 1. `func visit(node:)`, iff the visited object conforms to `Node`. +/// 2. `func visit(block:)`, iff the visited object conforms to `Block`. +/// 3. `func visit(containerBlock:)`, iff the visited object conforms to `ContainerBlock`. +/// 4. `func visit(leafBlock:)`, iff the visited object conforms to `LeafBlock`. +/// 5. `func visit(inline:)`, iff the visited object conforms to `Inline`. +/// 6. `func visit(:)`, where `` corresponds to the +/// the visited object's concrete type. +/// +/// With each visitation's returned `VisitorContinueKind` overriding the previous one. +/// public protocol Visitor { + /// The fallback for when `.inherit` var defaultContinueKind: VisitorContinueKind { get } + /// Walks a visitable structure. + /// - Parameter visitable: The structure to walk. func walk(_ visitable: T) // MARK: - Document func visit(document: Document) -> VisitorContinueKind + // MARK: - Node + + func visit(node: Node) -> VisitorContinueKind + + // MARK: - Blocks + + func visit(block: Block) -> VisitorContinueKind + // MARK: - Container Blocks + func visit(containerBlock: ContainerBlock) -> VisitorContinueKind + func visit(blockQuote: BlockQuote) -> VisitorContinueKind func visit(list: List) -> VisitorContinueKind func visit(listItem: List.Item) -> VisitorContinueKind // MARK: - Leaf Blocks + /// A block that can only contain inline elements. + func visit(leafBlock: LeafBlock) -> VisitorContinueKind + func visit(heading: Heading) -> VisitorContinueKind func visit(paragraph: Paragraph) -> VisitorContinueKind func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind @@ -34,25 +75,27 @@ public protocol Visitor { // MARK: - Inline + func visit(inline: Inline) -> VisitorContinueKind + func visit(text: Text) -> VisitorContinueKind func visit(strong: Strong) -> VisitorContinueKind func visit(emphasis: Emphasis) -> VisitorContinueKind func visit(link: Link) -> VisitorContinueKind - @discardableResult func visit(image: Image) -> VisitorContinueKind - @discardableResult func visit(code: Code) -> VisitorContinueKind - @discardableResult func visit(rawHTML: RawHTML) -> VisitorContinueKind - @discardableResult func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind - @discardableResult func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind } extension Visitor { + // Non-overridable default used as last-resort fallback. + internal static var defaultContinueKind: VisitorContinueKind { + return .visitChildren + } + public var defaultContinueKind: VisitorContinueKind { - return.visitChildren + return Self.defaultContinueKind } public func walk(_ visitable: T) { @@ -62,71 +105,144 @@ extension Visitor { // MARK: - Document public func visit(document: Document) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit + } + + // MARK: - Node + + public func visit(node: Node) -> VisitorContinueKind { + return .inherit + } + + // MARK: - Blocks + + public func visit(block: Block) -> VisitorContinueKind { + return .inherit } // MARK: - Container Blocks + public func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { + return .inherit + } + public func visit(blockQuote: BlockQuote) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(list: List) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(listItem: List.Item) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } // MARK: - Leaf Blocks + /// A block that can only contain inline elements. + public func visit(leafBlock: LeafBlock) -> VisitorContinueKind { + return .inherit + } + public func visit(heading: Heading) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(paragraph: Paragraph) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(codeBlock: CodeBlock) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } // MARK: - Inline + public func visit(inline: Inline) -> VisitorContinueKind { + return .inherit + } + public func visit(text: Text) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(strong: Strong) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(emphasis: Emphasis) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } public func visit(link: Link) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } - @discardableResult public func visit(image: Image) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } - @discardableResult public func visit(code: Code) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } - @discardableResult public func visit(rawHTML: RawHTML) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } - @discardableResult public func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit } - @discardableResult public func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { - return self.defaultContinueKind + return .inherit + } + + public func didEndVisit() { + // nothing by default + } +} + +extension Visitor { + internal func visit( + _ visitable: T, + by visitLeafTypeOf: @autoclosure () -> VisitorContinueKind + ) -> VisitorContinueKind { + let userDefaultContinueKind = self.defaultContinueKind + + let inheritedContinueKind = self.visitSuperTypesOf(visitable: visitable) + let leafTypeContinueKind = visitLeafTypeOf() + + // Abort on debug: + assert( + userDefaultContinueKind != .inherit, + "The default must not be `.inherit`" + ) + + // Don't rely on the user playing by the rules: + var continueKind = Self.defaultContinueKind + + continueKind.override(with: userDefaultContinueKind) + continueKind.override(with: inheritedContinueKind) + continueKind.override(with: leafTypeContinueKind) + + return continueKind + } + + internal func visitSuperTypesOf(visitable: T) -> VisitorContinueKind { + var continueKind: VisitorContinueKind = .inherit + + if let node = visitable as? Node { + continueKind.override(with: self.visit(node: node)) + } + if let block = visitable as? Block { + continueKind.override(with: self.visit(block: block)) + } + if let containerBlock = visitable as? ContainerBlock { + continueKind.override(with: self.visit(containerBlock: containerBlock)) + } + if let leafBlock = visitable as? LeafBlock { + continueKind.override(with: self.visit(leafBlock: leafBlock)) + } + if let inline = visitable as? Inline { + continueKind.override(with: self.visit(inline: inline)) + } + + return continueKind } } diff --git a/Tests/CommonMarkTests/StatisticsVisitor.swift b/Tests/CommonMarkTests/StatisticsVisitor.swift new file mode 100644 index 0000000..23a8cc9 --- /dev/null +++ b/Tests/CommonMarkTests/StatisticsVisitor.swift @@ -0,0 +1,161 @@ +import CommonMark + +public struct DocumentStatistics: Equatable { + public var documents: Int = 0 + + // Node: + + public var nodes: Int = 0 + + // Blocks: + + public var blocks: Int = 0 + + // Container Blocks: + + public var containerBlocks: Int = 0 + + public var blockQuotes: Int = 0 + public var lists: Int = 0 + public var listItems: Int = 0 + + // Leaf Blocks: + + public var leafBlocks: Int = 0 + + public var headings: Int = 0 + public var paragraphs: Int = 0 + public var htmlBlocks: Int = 0 + public var codeBlocks: Int = 0 + public var thematicBreaks: Int = 0 + + // Inline: + + public var inlines: Int = 0 + + public var texts: Int = 0 + public var strongs: Int = 0 + public var emphasises: Int = 0 + public var links: Int = 0 + public var images: Int = 0 + public var codes: Int = 0 + public var rawHTMLs: Int = 0 + public var softLineBreaks: Int = 0 + public var hardLineBreaks: Int = 0 +} + +open class StatisticsVisitor: Visitor { + public var statistics: DocumentStatistics = .init() + + public func visit(document: Document) -> VisitorContinueKind { + self.statistics.documents += 1 + return .inherit + } + + // MARK: - Node + + public func visit(node: Node) -> VisitorContinueKind { + self.statistics.nodes += 1 + return .inherit + } + + // MARK: - Blocks + + public func visit(block: Block) -> VisitorContinueKind { + self.statistics.blocks += 1 + return .inherit + } + + // MARK: - Container Blocks + + public func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { + self.statistics.containerBlocks += 1 + return .inherit + } + + public func visit(blockQuote: BlockQuote) -> VisitorContinueKind { + self.statistics.blockQuotes += 1 + return .inherit + } + public func visit(list: List) -> VisitorContinueKind { + self.statistics.lists += 1 + return .inherit + } + public func visit(listItem: List.Item) -> VisitorContinueKind { + self.statistics.listItems += 1 + return .inherit + } + + // MARK: - Leaf Blocks + + /// A block that can only contain inline elements. + public func visit(leafBlock: LeafBlock) -> VisitorContinueKind { + self.statistics.leafBlocks += 1 + return .inherit + } + + public func visit(heading: Heading) -> VisitorContinueKind { + self.statistics.headings += 1 + return .inherit + } + public func visit(paragraph: Paragraph) -> VisitorContinueKind { + self.statistics.paragraphs += 1 + return .inherit + } + public func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { + self.statistics.htmlBlocks += 1 + return .inherit + } + public func visit(codeBlock: CodeBlock) -> VisitorContinueKind { + self.statistics.codeBlocks += 1 + return .inherit + } + public func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { + self.statistics.thematicBreaks += 1 + return .inherit + } + + // MARK: - Inline + + public func visit(inline: Inline) -> VisitorContinueKind { + self.statistics.inlines += 1 + return .inherit + } + + public func visit(text: Text) -> VisitorContinueKind { + self.statistics.texts += 1 + return .inherit + } + public func visit(strong: Strong) -> VisitorContinueKind { + self.statistics.strongs += 1 + return .inherit + } + public func visit(emphasis: Emphasis) -> VisitorContinueKind { + self.statistics.emphasises += 1 + return .inherit + } + public func visit(link: Link) -> VisitorContinueKind { + self.statistics.links += 1 + return .inherit + } + public func visit(image: Image) -> VisitorContinueKind { + self.statistics.images += 1 + return .inherit + } + public func visit(code: Code) -> VisitorContinueKind { + self.statistics.codes += 1 + return .inherit + } + public func visit(rawHTML: RawHTML) -> VisitorContinueKind { + self.statistics.rawHTMLs += 1 + return .inherit + } + public func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { + self.statistics.softLineBreaks += 1 + return .inherit + } + public func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { + self.statistics.hardLineBreaks += 1 + return .inherit + } +} diff --git a/Tests/CommonMarkTests/VisitorTests.swift b/Tests/CommonMarkTests/VisitorTests.swift index e51caaa..a92a8df 100644 --- a/Tests/CommonMarkTests/VisitorTests.swift +++ b/Tests/CommonMarkTests/VisitorTests.swift @@ -3,80 +3,174 @@ import CommonMark final class VisitorTests: XCTestCase { func testVisitorVisitingChildren() throws { - final class TestVisitor: Visitor { - var numberOfHeadings: Int = 0 - var numberOfParagraphs: Int = 0 - var numberOfLinks: Int = 0 - - var defaultContinueKind: VisitorContinueKind { - return .visitChildren - } - - func visit(heading: Heading) -> VisitorContinueKind { - self.numberOfHeadings += 1 + typealias TestVisitor = StatisticsVisitor + + // Document nodes: + + // Document + // ├─ Heading + // │ └─ Link + // │ └─ Text + // ├─ Heading + // │ └─ Text + // └─ Paragraph + // ├─ Text + // ├─ SoftLineBreak + // ├─ Text + // ├─ SoftLineBreak + // └─ Text + let document = try Document(Fixtures.udhr) + + // Visited nodes: + + // Document + // ├─ Heading + // │ └─ Link + // │ └─ Text + // ├─ Heading + // │ └─ Text + // └─ Paragraph + // ├─ Text + // ├─ SoftLineBreak + // ├─ Text + // ├─ SoftLineBreak + // └─ Text + let visitor = TestVisitor() + visitor.walk(document) - return .visitChildren - } + let expedted = DocumentStatistics( + documents: 1, + nodes: 12, + blocks: 3, + leafBlocks: 3, + headings: 2, + paragraphs: 1, + inlines: 8, + texts: 5, + links: 1, + softLineBreaks: 2 + ) + + XCTAssertEqual(visitor.statistics, expedted) + } - func visit(paragraph: Paragraph) -> VisitorContinueKind { - self.numberOfParagraphs += 1 + func testVisitorSkippingChildren() throws { + final class TestVisitor: StatisticsVisitor { + override func visit(link: Link) -> VisitorContinueKind { + let _ = super.visit(link: link) - return .visitChildren + return .skipChildren } - func visit(link: Link) -> VisitorContinueKind { - self.numberOfLinks += 1 + override func visit(paragraph: Paragraph) -> VisitorContinueKind { + let _ = super.visit(paragraph: paragraph) - return .visitChildren + return .skipChildren } } - let document = try Document(Fixtures.uhdr) + // Document nodes: + + // Document + // ├─ Heading + // │ └─ Link + // │ └─ Text + // ├─ Heading + // │ └─ Text + // └─ Paragraph + // ├─ Text + // ├─ SoftLineBreak + // ├─ Text + // ├─ SoftLineBreak + // └─ Text + let document = try Document(Fixtures.udhr) + + // Visited nodes: + + // Document + // ├─ Heading + // │ └─ Link (children skipped) + // ├─ Heading + // │ └─ Text + // └─ Paragraph (children skipped) let visitor = TestVisitor() - visitor.walk(document) - XCTAssertEqual(visitor.numberOfHeadings, 2) - XCTAssertEqual(visitor.numberOfParagraphs, 1) - XCTAssertEqual(visitor.numberOfLinks, 1) + let expedted = DocumentStatistics( + documents: 1, + nodes: 6, + blocks: 3, + leafBlocks: 3, + headings: 2, + paragraphs: 1, + inlines: 2, + texts: 1, + links: 1, + softLineBreaks: 0 + ) + + XCTAssertEqual(visitor.statistics, expedted) } - func testVisitorSkippingChildren() throws { - final class TestVisitor: Visitor { - var numberOfHeadings: Int = 0 - var numberOfParagraphs: Int = 0 - var numberOfLinks: Int = 0 - - var defaultContinueKind: VisitorContinueKind { - return .visitChildren - } - - func visit(heading: Heading) -> VisitorContinueKind { - self.numberOfHeadings += 1 + func testVisitorOverrides() throws { + final class TestVisitor: StatisticsVisitor { + override func visit(block: Block) -> VisitorContinueKind { + let _ = super.visit(block: block) return .skipChildren } - func visit(paragraph: Paragraph) -> VisitorContinueKind { - self.numberOfParagraphs += 1 + // Given that `Paragraph` is a subtype of `Block` it overrides: + override func visit(paragraph: Paragraph) -> VisitorContinueKind { + let _ = super.visit(paragraph: paragraph) - return .skipChildren - } - - func visit(link: Link) -> VisitorContinueKind { - self.numberOfLinks += 1 - - return .skipChildren + return .visitChildren } } - let document = try Document(Fixtures.uhdr) + // Document nodes: + + // Document + // ├─ Heading + // │ └─ Link + // │ └─ Text + // ├─ Heading + // │ └─ Text + // └─ Paragraph + // ├─ Text + // ├─ SoftLineBreak + // ├─ Text + // ├─ SoftLineBreak + // └─ Text + let document = try Document(Fixtures.udhr) + + // Visited nodes: + + // Document + // ├─ Heading (children skipped) + // ├─ Heading (children skipped) + // └─ Paragraph + // ├─ Text + // ├─ SoftLineBreak + // ├─ Text + // ├─ SoftLineBreak + // └─ Text let visitor = TestVisitor() - visitor.walk(document) - XCTAssertEqual(visitor.numberOfHeadings, 2) - XCTAssertEqual(visitor.numberOfParagraphs, 1) - XCTAssertEqual(visitor.numberOfLinks, 0) + let expected = DocumentStatistics( + documents: 1, + nodes: 9, + blocks: 3, + leafBlocks: 3, + headings: 2, + paragraphs: 1, + inlines: 5, + texts: 3, + links: 0, + softLineBreaks: 2 + ) + + XCTAssertEqual(visitor.statistics, expected) } } From dcc20e59c9b6816b64262bb3fa87bb43ab3269e4 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Thu, 30 Apr 2020 21:43:16 +0200 Subject: [PATCH 03/12] Added `func visitPost` variants and corresponding default implementations to `protocol Visitor` --- Sources/CommonMark/Visitable.swift | 18 ++++ Sources/CommonMark/Visitor.swift | 143 +++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/Sources/CommonMark/Visitable.swift b/Sources/CommonMark/Visitable.swift index 899ac4e..f0851d0 100644 --- a/Sources/CommonMark/Visitable.swift +++ b/Sources/CommonMark/Visitable.swift @@ -10,6 +10,7 @@ extension Document: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(document: self) } } @@ -21,6 +22,7 @@ extension BlockQuote: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(blockQuote: self) } } @@ -30,6 +32,7 @@ extension List: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(list: self) } } @@ -39,6 +42,7 @@ extension List.Item: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(listItem: self) } } @@ -50,6 +54,7 @@ extension Heading: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(heading: self) } } @@ -59,6 +64,7 @@ extension Paragraph: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(paragraph: self) } } @@ -68,6 +74,7 @@ extension HTMLBlock: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(htmlBlock: self) } } @@ -77,6 +84,7 @@ extension CodeBlock: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(codeBlock: self) } } @@ -86,6 +94,7 @@ extension ThematicBreak: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(thematicBreak: self) } } @@ -97,6 +106,7 @@ extension Text: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(text: self) } } @@ -106,6 +116,7 @@ extension Strong: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(strong: self) } } @@ -115,6 +126,7 @@ extension Emphasis: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(emphasis: self) } } @@ -124,6 +136,7 @@ extension Link: Visitable { if continueKind == .visitChildren { self.walkVisitableChildren(with: visitor) } + visitor.visitPost(link: self) } } @@ -133,6 +146,7 @@ extension Image: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(image: self) } } @@ -142,6 +156,7 @@ extension Code: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(code: self) } } @@ -151,6 +166,7 @@ extension RawHTML: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(rawHTML: self) } } @@ -160,6 +176,7 @@ extension SoftLineBreak: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(softLineBreak: self) } } @@ -169,6 +186,7 @@ extension HardLineBreak: Visitable { if continueKind == .visitChildren { // type has no visitable children for now } + visitor.visitPost(hardLineBreak: self) } } diff --git a/Sources/CommonMark/Visitor.swift b/Sources/CommonMark/Visitor.swift index 39ca255..7926f3a 100644 --- a/Sources/CommonMark/Visitor.swift +++ b/Sources/CommonMark/Visitor.swift @@ -45,47 +45,84 @@ public protocol Visitor { // MARK: - Document func visit(document: Document) -> VisitorContinueKind + func visitPost(document: Document) // MARK: - Node func visit(node: Node) -> VisitorContinueKind + func visitPost(node: Node) // MARK: - Blocks func visit(block: Block) -> VisitorContinueKind + func visitPost(block: Block) // MARK: - Container Blocks func visit(containerBlock: ContainerBlock) -> VisitorContinueKind + func visitPost(containerBlock: ContainerBlock) func visit(blockQuote: BlockQuote) -> VisitorContinueKind + func visitPost(blockQuote: BlockQuote) + func visit(list: List) -> VisitorContinueKind + func visitPost(list: List) + func visit(listItem: List.Item) -> VisitorContinueKind + func visitPost(listItem: List.Item) // MARK: - Leaf Blocks /// A block that can only contain inline elements. func visit(leafBlock: LeafBlock) -> VisitorContinueKind + func visitPost(leafBlock: LeafBlock) func visit(heading: Heading) -> VisitorContinueKind + func visitPost(heading: Heading) + func visit(paragraph: Paragraph) -> VisitorContinueKind + func visitPost(paragraph: Paragraph) + func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind + func visitPost(htmlBlock: HTMLBlock) + func visit(codeBlock: CodeBlock) -> VisitorContinueKind + func visitPost(codeBlock: CodeBlock) + func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind + func visitPost(thematicBreak: ThematicBreak) // MARK: - Inline func visit(inline: Inline) -> VisitorContinueKind + func visitPost(inline: Inline) func visit(text: Text) -> VisitorContinueKind + func visitPost(text: Text) + func visit(strong: Strong) -> VisitorContinueKind + func visitPost(strong: Strong) + func visit(emphasis: Emphasis) -> VisitorContinueKind + func visitPost(emphasis: Emphasis) + func visit(link: Link) -> VisitorContinueKind + func visitPost(link: Link) + func visit(image: Image) -> VisitorContinueKind + func visitPost(image: Image) + func visit(code: Code) -> VisitorContinueKind + func visitPost(code: Code) + func visit(rawHTML: RawHTML) -> VisitorContinueKind + func visitPost(rawHTML: RawHTML) + func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind + func visitPost(softLineBreak: SoftLineBreak) + func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind + func visitPost(hardLineBreak: HardLineBreak) } extension Visitor { @@ -108,34 +145,64 @@ extension Visitor { return .inherit } + public func visitPost(document: Document) { + // nothing by default + } + // MARK: - Node public func visit(node: Node) -> VisitorContinueKind { return .inherit } + public func visitPost(node: Node) { + // nothing by default + } + // MARK: - Blocks public func visit(block: Block) -> VisitorContinueKind { return .inherit } + public func visitPost(block: Block) { + // nothing by default + } + // MARK: - Container Blocks public func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { return .inherit } + public func visitPost(containerBlock: ContainerBlock) { + // nothing by default + } + public func visit(blockQuote: BlockQuote) -> VisitorContinueKind { return .inherit } + + public func visitPost(blockQuote: BlockQuote) { + // nothing by default + } + public func visit(list: List) -> VisitorContinueKind { return .inherit } + + public func visitPost(list: List) { + // nothing by default + } + public func visit(listItem: List.Item) -> VisitorContinueKind { return .inherit } + public func visitPost(listItem: List.Item) { + // nothing by default + } + // MARK: - Leaf Blocks /// A block that can only contain inline elements. @@ -143,56 +210,132 @@ extension Visitor { return .inherit } + public func visitPost(leafBlock: LeafBlock) { + // nothing by default + } + public func visit(heading: Heading) -> VisitorContinueKind { return .inherit } + + public func visitPost(heading: Heading) { + // nothing by default + } + public func visit(paragraph: Paragraph) -> VisitorContinueKind { return .inherit } + + public func visitPost(paragraph: Paragraph) { + // nothing by default + } + public func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { return .inherit } + + public func visitPost(htmlBlock: HTMLBlock) { + // nothing by default + } + public func visit(codeBlock: CodeBlock) -> VisitorContinueKind { return .inherit } + + public func visitPost(codeBlock: CodeBlock) { + // nothing by default + } + public func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { return .inherit } + public func visitPost(thematicBreak: ThematicBreak) { + // nothing by default + } + // MARK: - Inline public func visit(inline: Inline) -> VisitorContinueKind { return .inherit } + public func visitPost(inline: Inline) { + // nothing by default + } + public func visit(text: Text) -> VisitorContinueKind { return .inherit } + + public func visitPost(text: Text) { + // nothing by default + } + public func visit(strong: Strong) -> VisitorContinueKind { return .inherit } + + public func visitPost(strong: Strong) { + // nothing by default + } + public func visit(emphasis: Emphasis) -> VisitorContinueKind { return .inherit } + + public func visitPost(emphasis: Emphasis) { + // nothing by default + } + public func visit(link: Link) -> VisitorContinueKind { return .inherit } + + public func visitPost(link: Link) { + // nothing by default + } + public func visit(image: Image) -> VisitorContinueKind { return .inherit } + + public func visitPost(image: Image) { + // nothing by default + } + public func visit(code: Code) -> VisitorContinueKind { return .inherit } + + public func visitPost(code: Code) { + // nothing by default + } + public func visit(rawHTML: RawHTML) -> VisitorContinueKind { return .inherit } + + public func visitPost(rawHTML: RawHTML) { + // nothing by default + } + public func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { return .inherit } + + public func visitPost(softLineBreak: SoftLineBreak) { + // nothing by default + } + public func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { return .inherit } + public func visitPost(hardLineBreak: HardLineBreak) { + // nothing by default + } + public func didEndVisit() { // nothing by default } From a1666917b03dd347c26c1b58eb421c3ed9740efb Mon Sep 17 00:00:00 2001 From: Mattt Date: Sun, 24 May 2020 07:01:55 -0700 Subject: [PATCH 04/12] Remove no-op when visiting leaf nodes --- Sources/CommonMark/Visitable.swift | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/Sources/CommonMark/Visitable.swift b/Sources/CommonMark/Visitable.swift index f0851d0..70b69b0 100644 --- a/Sources/CommonMark/Visitable.swift +++ b/Sources/CommonMark/Visitable.swift @@ -102,10 +102,7 @@ extension ThematicBreak: Visitable { extension Text: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(text: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(text: self)) visitor.visitPost(text: self) } } @@ -142,50 +139,35 @@ extension Link: Visitable { extension Image: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(image: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(image: self)) visitor.visitPost(image: self) } } extension Code: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(code: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(code: self)) visitor.visitPost(code: self) } } extension RawHTML: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(rawHTML: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(rawHTML: self)) visitor.visitPost(rawHTML: self) } } extension SoftLineBreak: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(softLineBreak: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(softLineBreak: self)) visitor.visitPost(softLineBreak: self) } } extension HardLineBreak: Visitable { public func accept(visitor: T) { - let continueKind = visitor.visit(self, by: visitor.visit(hardLineBreak: self)) - if continueKind == .visitChildren { - // type has no visitable children for now - } + _ = visitor.visit(self, by: visitor.visit(hardLineBreak: self)) visitor.visitPost(hardLineBreak: self) } } From 26cbb6f11dcf8af79f0badc4a815f4d9064ec88d Mon Sep 17 00:00:00 2001 From: Mattt Date: Sun, 24 May 2020 07:04:25 -0700 Subject: [PATCH 05/12] Remove unused didEndVisit Visitor method --- Sources/CommonMark/Visitor.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/CommonMark/Visitor.swift b/Sources/CommonMark/Visitor.swift index 7926f3a..08d0def 100644 --- a/Sources/CommonMark/Visitor.swift +++ b/Sources/CommonMark/Visitor.swift @@ -335,10 +335,6 @@ extension Visitor { public func visitPost(hardLineBreak: HardLineBreak) { // nothing by default } - - public func didEndVisit() { - // nothing by default - } } extension Visitor { From 219d5b25872009eb88008f59b57ee34241e98ec4 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 17:51:12 +0200 Subject: [PATCH 06/12] Made `DocumentStatistics` & `StatisticsVisitor` implicitly internal --- Tests/CommonMarkTests/StatisticsVisitor.swift | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/Tests/CommonMarkTests/StatisticsVisitor.swift b/Tests/CommonMarkTests/StatisticsVisitor.swift index 23a8cc9..515e302 100644 --- a/Tests/CommonMarkTests/StatisticsVisitor.swift +++ b/Tests/CommonMarkTests/StatisticsVisitor.swift @@ -1,87 +1,87 @@ import CommonMark -public struct DocumentStatistics: Equatable { - public var documents: Int = 0 +struct DocumentStatistics: Equatable { + var documents: Int = 0 // Node: - public var nodes: Int = 0 + var nodes: Int = 0 // Blocks: - public var blocks: Int = 0 + var blocks: Int = 0 // Container Blocks: - public var containerBlocks: Int = 0 + var containerBlocks: Int = 0 - public var blockQuotes: Int = 0 - public var lists: Int = 0 - public var listItems: Int = 0 + var blockQuotes: Int = 0 + var lists: Int = 0 + var listItems: Int = 0 // Leaf Blocks: - public var leafBlocks: Int = 0 + var leafBlocks: Int = 0 - public var headings: Int = 0 - public var paragraphs: Int = 0 - public var htmlBlocks: Int = 0 - public var codeBlocks: Int = 0 - public var thematicBreaks: Int = 0 + var headings: Int = 0 + var paragraphs: Int = 0 + var htmlBlocks: Int = 0 + var codeBlocks: Int = 0 + var thematicBreaks: Int = 0 // Inline: - public var inlines: Int = 0 - - public var texts: Int = 0 - public var strongs: Int = 0 - public var emphasises: Int = 0 - public var links: Int = 0 - public var images: Int = 0 - public var codes: Int = 0 - public var rawHTMLs: Int = 0 - public var softLineBreaks: Int = 0 - public var hardLineBreaks: Int = 0 + var inlines: Int = 0 + + var texts: Int = 0 + var strongs: Int = 0 + var emphasises: Int = 0 + var links: Int = 0 + var images: Int = 0 + var codes: Int = 0 + var rawHTMLs: Int = 0 + var softLineBreaks: Int = 0 + var hardLineBreaks: Int = 0 } -open class StatisticsVisitor: Visitor { - public var statistics: DocumentStatistics = .init() +class StatisticsVisitor: Visitor { + var statistics: DocumentStatistics = .init() - public func visit(document: Document) -> VisitorContinueKind { + func visit(document: Document) -> VisitorContinueKind { self.statistics.documents += 1 return .inherit } // MARK: - Node - public func visit(node: Node) -> VisitorContinueKind { + func visit(node: Node) -> VisitorContinueKind { self.statistics.nodes += 1 return .inherit } // MARK: - Blocks - public func visit(block: Block) -> VisitorContinueKind { + func visit(block: Block) -> VisitorContinueKind { self.statistics.blocks += 1 return .inherit } // MARK: - Container Blocks - public func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { + func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { self.statistics.containerBlocks += 1 return .inherit } - public func visit(blockQuote: BlockQuote) -> VisitorContinueKind { + func visit(blockQuote: BlockQuote) -> VisitorContinueKind { self.statistics.blockQuotes += 1 return .inherit } - public func visit(list: List) -> VisitorContinueKind { + func visit(list: List) -> VisitorContinueKind { self.statistics.lists += 1 return .inherit } - public func visit(listItem: List.Item) -> VisitorContinueKind { + func visit(listItem: List.Item) -> VisitorContinueKind { self.statistics.listItems += 1 return .inherit } @@ -89,72 +89,72 @@ open class StatisticsVisitor: Visitor { // MARK: - Leaf Blocks /// A block that can only contain inline elements. - public func visit(leafBlock: LeafBlock) -> VisitorContinueKind { + func visit(leafBlock: LeafBlock) -> VisitorContinueKind { self.statistics.leafBlocks += 1 return .inherit } - public func visit(heading: Heading) -> VisitorContinueKind { + func visit(heading: Heading) -> VisitorContinueKind { self.statistics.headings += 1 return .inherit } - public func visit(paragraph: Paragraph) -> VisitorContinueKind { + func visit(paragraph: Paragraph) -> VisitorContinueKind { self.statistics.paragraphs += 1 return .inherit } - public func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { + func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { self.statistics.htmlBlocks += 1 return .inherit } - public func visit(codeBlock: CodeBlock) -> VisitorContinueKind { + func visit(codeBlock: CodeBlock) -> VisitorContinueKind { self.statistics.codeBlocks += 1 return .inherit } - public func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { + func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { self.statistics.thematicBreaks += 1 return .inherit } // MARK: - Inline - public func visit(inline: Inline) -> VisitorContinueKind { + func visit(inline: Inline) -> VisitorContinueKind { self.statistics.inlines += 1 return .inherit } - public func visit(text: Text) -> VisitorContinueKind { + func visit(text: Text) -> VisitorContinueKind { self.statistics.texts += 1 return .inherit } - public func visit(strong: Strong) -> VisitorContinueKind { + func visit(strong: Strong) -> VisitorContinueKind { self.statistics.strongs += 1 return .inherit } - public func visit(emphasis: Emphasis) -> VisitorContinueKind { + func visit(emphasis: Emphasis) -> VisitorContinueKind { self.statistics.emphasises += 1 return .inherit } - public func visit(link: Link) -> VisitorContinueKind { + func visit(link: Link) -> VisitorContinueKind { self.statistics.links += 1 return .inherit } - public func visit(image: Image) -> VisitorContinueKind { + func visit(image: Image) -> VisitorContinueKind { self.statistics.images += 1 return .inherit } - public func visit(code: Code) -> VisitorContinueKind { + func visit(code: Code) -> VisitorContinueKind { self.statistics.codes += 1 return .inherit } - public func visit(rawHTML: RawHTML) -> VisitorContinueKind { + func visit(rawHTML: RawHTML) -> VisitorContinueKind { self.statistics.rawHTMLs += 1 return .inherit } - public func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { + func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { self.statistics.softLineBreaks += 1 return .inherit } - public func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { + func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { self.statistics.hardLineBreaks += 1 return .inherit } From 1b14b9165123b4ed73a67f4cec95f6b61ab8bb82 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 17:52:12 +0200 Subject: [PATCH 07/12] Moved 'Visitable.swift' & 'Visitor.swift' to 'Supporting Types' directory --- Sources/CommonMark/{ => Supporting Types}/Visitable.swift | 0 Sources/CommonMark/{ => Supporting Types}/Visitor.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Sources/CommonMark/{ => Supporting Types}/Visitable.swift (100%) rename Sources/CommonMark/{ => Supporting Types}/Visitor.swift (100%) diff --git a/Sources/CommonMark/Visitable.swift b/Sources/CommonMark/Supporting Types/Visitable.swift similarity index 100% rename from Sources/CommonMark/Visitable.swift rename to Sources/CommonMark/Supporting Types/Visitable.swift diff --git a/Sources/CommonMark/Visitor.swift b/Sources/CommonMark/Supporting Types/Visitor.swift similarity index 100% rename from Sources/CommonMark/Visitor.swift rename to Sources/CommonMark/Supporting Types/Visitor.swift From 996cf870780b2a3080a6483a64c85b1d95f0b42c Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 18:31:38 +0200 Subject: [PATCH 08/12] Added missing documentation for `VisitorContinueKind.inherit` --- Sources/CommonMark/Supporting Types/Visitor.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CommonMark/Supporting Types/Visitor.swift b/Sources/CommonMark/Supporting Types/Visitor.swift index 08d0def..3719c59 100644 --- a/Sources/CommonMark/Supporting Types/Visitor.swift +++ b/Sources/CommonMark/Supporting Types/Visitor.swift @@ -5,6 +5,7 @@ public enum VisitorContinueKind { /// The visitor should avoid visiting the descendents of the current node. case skipChildren + /// The visitor should inherit the behavior from the current context. case inherit /// The default is `.visitChildren` From 578c22238af37b4a038711449f3836d142a506d6 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 18:32:07 +0200 Subject: [PATCH 09/12] Improved documentation for `protocol Visitor` --- .../CommonMark/Supporting Types/Visitor.swift | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/Sources/CommonMark/Supporting Types/Visitor.swift b/Sources/CommonMark/Supporting Types/Visitor.swift index 3719c59..35249e0 100644 --- a/Sources/CommonMark/Supporting Types/Visitor.swift +++ b/Sources/CommonMark/Supporting Types/Visitor.swift @@ -23,18 +23,49 @@ public enum VisitorContinueKind { /// Visitor for walking a visitable structure. /// -/// The order of object-wise visitations is: +/// Override the appropriate `func visit(…:)`'s return value +/// to customize the behavior (e.g. skip a given element's children), +/// with sub-types overriding their super-type's behavior. /// -/// 1. `func visit(node:)`, iff the visited object conforms to `Node`. -/// 2. `func visit(block:)`, iff the visited object conforms to `Block`. -/// 3. `func visit(containerBlock:)`, iff the visited object conforms to `ContainerBlock`. -/// 4. `func visit(leafBlock:)`, iff the visited object conforms to `LeafBlock`. -/// 5. `func visit(inline:)`, iff the visited object conforms to `Inline`. -/// 6. `func visit(:)`, where `` corresponds to the -/// the visited object's concrete type. +/// The default implementation of `func visit(…:)`returns `.inherit`, +/// resulting in a deep walk over the entire document. /// -/// With each visitation's returned `VisitorContinueKind` overriding the previous one. +/// ## Sub-type inheritance /// +/// ```plain +/// Node +/// ├── Block +/// │   ├── ContainerBlock +/// │   │   ├── BlockQuote +/// │   │   ├── List +/// │   │   └── List.Item +/// │   │ +/// │   └── LeafBlock +/// │   ├── CodeBlock +/// │   ├── HTMLBlock +/// │   ├── Heading +/// │   ├── Paragraph +/// │   └── ThematicBreak +/// │ +/// └── Inline +/// ├── Code +/// ├── Emphasis +/// ├── HardLineBreak +/// ├── Image +/// ├── Link +/// ├── RawHTML +/// ├── SoftLineBreak +/// ├── Strong +/// └── Text +/// ``` +/// +/// ## Order of Visitation +/// +/// The order of object-wise visitations is: super-type before sub-type. +/// +/// ## Order of Post-Visitation +/// +/// The order of object-wise visitations is: sub-type before super-type. public protocol Visitor { /// The fallback for when `.inherit` var defaultContinueKind: VisitorContinueKind { get } From 9bcf1d0f656758374ef7c53ccd397be2679a2720 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 18:32:46 +0200 Subject: [PATCH 10/12] Removed `var defaultContinueKind: VisitorContinueKind` --- .../CommonMark/Supporting Types/Visitor.swift | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Sources/CommonMark/Supporting Types/Visitor.swift b/Sources/CommonMark/Supporting Types/Visitor.swift index 35249e0..3e93d3d 100644 --- a/Sources/CommonMark/Supporting Types/Visitor.swift +++ b/Sources/CommonMark/Supporting Types/Visitor.swift @@ -67,9 +67,6 @@ public enum VisitorContinueKind { /// /// The order of object-wise visitations is: sub-type before super-type. public protocol Visitor { - /// The fallback for when `.inherit` - var defaultContinueKind: VisitorContinueKind { get } - /// Walks a visitable structure. /// - Parameter visitable: The structure to walk. func walk(_ visitable: T) @@ -158,15 +155,6 @@ public protocol Visitor { } extension Visitor { - // Non-overridable default used as last-resort fallback. - internal static var defaultContinueKind: VisitorContinueKind { - return .visitChildren - } - - public var defaultContinueKind: VisitorContinueKind { - return Self.defaultContinueKind - } - public func walk(_ visitable: T) { visitable.accept(visitor: self) } @@ -374,21 +362,11 @@ extension Visitor { _ visitable: T, by visitLeafTypeOf: @autoclosure () -> VisitorContinueKind ) -> VisitorContinueKind { - let userDefaultContinueKind = self.defaultContinueKind + var continueKind = VisitorContinueKind.visitChildren let inheritedContinueKind = self.visitSuperTypesOf(visitable: visitable) let leafTypeContinueKind = visitLeafTypeOf() - // Abort on debug: - assert( - userDefaultContinueKind != .inherit, - "The default must not be `.inherit`" - ) - - // Don't rely on the user playing by the rules: - var continueKind = Self.defaultContinueKind - - continueKind.override(with: userDefaultContinueKind) continueKind.override(with: inheritedContinueKind) continueKind.override(with: leafTypeContinueKind) From 781d6724bdfd9848f12df782f108566b923fd715 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 19:09:42 +0200 Subject: [PATCH 11/12] Added unit tests for element sub-typing --- Tests/CommonMarkTests/ProtocolTests.swift | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 Tests/CommonMarkTests/ProtocolTests.swift diff --git a/Tests/CommonMarkTests/ProtocolTests.swift b/Tests/CommonMarkTests/ProtocolTests.swift new file mode 100644 index 0000000..060b09c --- /dev/null +++ b/Tests/CommonMarkTests/ProtocolTests.swift @@ -0,0 +1,70 @@ +import XCTest +import CommonMark + +/// Make sure element types do not conform to more protocols than they should. +/// +/// - An inline element should not also be a block element and vice versa. +/// - A leaf block element should not also be a container block element and vice versa. +/// +/// Breaking these invariants would be a breaking change for the `Visitor`'s assumptions about element sub-typing. +final class ProtocolTests: XCTestCase { + func XCTAssertValidInline(_ type: T.Type, file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue(T.self is Inline.Type, "Expected `Inline`", file: file, line: line) + XCTAssertFalse(T.self is Block.Type, "Did not expect `Block`", file: file, line: line) + } + + func XCTAssertValidLeafBlock(_ type: T.Type, file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue(T.self is LeafBlock.Type, "Expected `LeafBlock`", file: file, line: line) + XCTAssertFalse(T.self is ContainerBlock.Type, "Did not expect `ContainerBlock`", file: file, line: line) + XCTAssertFalse(T.self is Inline.Type, "Did not expect `Inline`", file: file, line: line) + } + + func XCTAssertValidContainerBlock(_ type: T.Type, file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue(T.self is ContainerBlock.Type, "Expected `ContainerBlock`", file: file, line: line) + XCTAssertFalse(T.self is LeafBlock.Type, "Did not expect `LeafBlock`", file: file, line: line) + XCTAssertFalse(T.self is Inline.Type, "Did not expect `Inline`", file: file, line: line) + } + + // MARK: - Node + + func testNode(){ + typealias ConcreteType = Node + + XCTAssertFalse(ConcreteType.self is Inline.Type) + XCTAssertFalse(ConcreteType.self is Block.Type) + XCTAssertFalse(ConcreteType.self is LeafBlock.Type) + XCTAssertFalse(ConcreteType.self is ContainerBlock.Type) + } + + // MARK: - Inline + + func testInline(){ + XCTAssertValidInline(HardLineBreak.self) + XCTAssertValidInline(SoftLineBreak.self) + XCTAssertValidInline(RawHTML.self) + XCTAssertValidInline(Code.self) + XCTAssertValidInline(Image.self) + XCTAssertValidInline(Link.self) + XCTAssertValidInline(Emphasis.self) + XCTAssertValidInline(Text.self) + XCTAssertValidInline(Strong.self) + } + + // MARK: - Leaf Block + + func testLeafBlock(){ + XCTAssertValidLeafBlock(ThematicBreak.self) + XCTAssertValidLeafBlock(CodeBlock.self) + XCTAssertValidLeafBlock(HTMLBlock.self) + XCTAssertValidLeafBlock(Paragraph.self) + XCTAssertValidLeafBlock(Heading.self) + } + + // MARK: - Container Block + + func testContainerBlock(){ + XCTAssertValidContainerBlock(List.Item.self) + XCTAssertValidContainerBlock(List.self) + XCTAssertValidContainerBlock(BlockQuote.self) + } +} From 8d845e35ded7392822df0f301bbf8101cb4a6b34 Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Sun, 24 May 2020 19:16:53 +0200 Subject: [PATCH 12/12] Added change log entry --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index ef5c5c1..e30f6b6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for the visitor pattern, + by adding `protocol Visitor`/`protocol Visitable` & `enum VisitorContinueKind`. - Added a changelog. #17 by @mattt.