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. 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/Supporting Types/Visitable.swift b/Sources/CommonMark/Supporting Types/Visitable.swift new file mode 100644 index 0000000..70b69b0 --- /dev/null +++ b/Sources/CommonMark/Supporting Types/Visitable.swift @@ -0,0 +1,229 @@ +public protocol Visitable { + func accept(visitor: T) +} + +// MARK: - Document + +extension Document: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(document: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(document: self) + } +} + +// MARK: - Container Blocks + +extension BlockQuote: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(blockQuote: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(blockQuote: self) + } +} + +extension List: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(list: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(list: self) + } +} + +extension List.Item: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(listItem: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(listItem: self) + } +} + +// MARK: - Leaf Blocks + +extension Heading: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(heading: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(heading: self) + } +} + +extension Paragraph: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(paragraph: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(paragraph: self) + } +} + +extension HTMLBlock: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(htmlBlock: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(htmlBlock: self) + } +} + +extension CodeBlock: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(codeBlock: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(codeBlock: self) + } +} + +extension ThematicBreak: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(thematicBreak: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(thematicBreak: self) + } +} + +// MARK: - Inline + +extension Text: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(text: self)) + visitor.visitPost(text: self) + } +} + +extension Strong: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(strong: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(strong: self) + } +} + +extension Emphasis: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(emphasis: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(emphasis: self) + } +} + +extension Link: Visitable { + public func accept(visitor: T) { + let continueKind = visitor.visit(self, by: visitor.visit(link: self)) + if continueKind == .visitChildren { + self.walkVisitableChildren(with: visitor) + } + visitor.visitPost(link: self) + } +} + +extension Image: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(image: self)) + visitor.visitPost(image: self) + } +} + +extension Code: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(code: self)) + visitor.visitPost(code: self) + } +} + +extension RawHTML: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(rawHTML: self)) + visitor.visitPost(rawHTML: self) + } +} + +extension SoftLineBreak: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(softLineBreak: self)) + visitor.visitPost(softLineBreak: self) + } +} + +extension HardLineBreak: Visitable { + public func accept(visitor: T) { + _ = visitor.visit(self, by: visitor.visit(hardLineBreak: self)) + visitor.visitPost(hardLineBreak: self) + } +} + +// 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/Supporting Types/Visitor.swift b/Sources/CommonMark/Supporting Types/Visitor.swift new file mode 100644 index 0000000..3e93d3d --- /dev/null +++ b/Sources/CommonMark/Supporting Types/Visitor.swift @@ -0,0 +1,397 @@ +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 visitor should inherit the behavior from the current context. + 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. +/// +/// 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. +/// +/// The default implementation of `func visit(…:)`returns `.inherit`, +/// resulting in a deep walk over the entire document. +/// +/// ## 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 { + /// Walks a visitable structure. + /// - Parameter visitable: The structure to walk. + func walk(_ visitable: T) + + // 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 { + public func walk(_ visitable: T) { + visitable.accept(visitor: self) + } + + // MARK: - Document + + public func visit(document: Document) -> VisitorContinueKind { + 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. + public func visit(leafBlock: LeafBlock) -> VisitorContinueKind { + 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 + } +} + +extension Visitor { + internal func visit( + _ visitable: T, + by visitLeafTypeOf: @autoclosure () -> VisitorContinueKind + ) -> VisitorContinueKind { + var continueKind = VisitorContinueKind.visitChildren + + let inheritedContinueKind = self.visitSuperTypesOf(visitable: visitable) + let leafTypeContinueKind = visitLeafTypeOf() + + 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/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) + } +} diff --git a/Tests/CommonMarkTests/StatisticsVisitor.swift b/Tests/CommonMarkTests/StatisticsVisitor.swift new file mode 100644 index 0000000..515e302 --- /dev/null +++ b/Tests/CommonMarkTests/StatisticsVisitor.swift @@ -0,0 +1,161 @@ +import CommonMark + +struct DocumentStatistics: Equatable { + var documents: Int = 0 + + // Node: + + var nodes: Int = 0 + + // Blocks: + + var blocks: Int = 0 + + // Container Blocks: + + var containerBlocks: Int = 0 + + var blockQuotes: Int = 0 + var lists: Int = 0 + var listItems: Int = 0 + + // Leaf Blocks: + + var leafBlocks: Int = 0 + + var headings: Int = 0 + var paragraphs: Int = 0 + var htmlBlocks: Int = 0 + var codeBlocks: Int = 0 + var thematicBreaks: Int = 0 + + // Inline: + + 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 +} + +class StatisticsVisitor: Visitor { + var statistics: DocumentStatistics = .init() + + func visit(document: Document) -> VisitorContinueKind { + self.statistics.documents += 1 + return .inherit + } + + // MARK: - Node + + func visit(node: Node) -> VisitorContinueKind { + self.statistics.nodes += 1 + return .inherit + } + + // MARK: - Blocks + + func visit(block: Block) -> VisitorContinueKind { + self.statistics.blocks += 1 + return .inherit + } + + // MARK: - Container Blocks + + func visit(containerBlock: ContainerBlock) -> VisitorContinueKind { + self.statistics.containerBlocks += 1 + return .inherit + } + + func visit(blockQuote: BlockQuote) -> VisitorContinueKind { + self.statistics.blockQuotes += 1 + return .inherit + } + func visit(list: List) -> VisitorContinueKind { + self.statistics.lists += 1 + return .inherit + } + func visit(listItem: List.Item) -> VisitorContinueKind { + self.statistics.listItems += 1 + return .inherit + } + + // MARK: - Leaf Blocks + + /// A block that can only contain inline elements. + func visit(leafBlock: LeafBlock) -> VisitorContinueKind { + self.statistics.leafBlocks += 1 + return .inherit + } + + func visit(heading: Heading) -> VisitorContinueKind { + self.statistics.headings += 1 + return .inherit + } + func visit(paragraph: Paragraph) -> VisitorContinueKind { + self.statistics.paragraphs += 1 + return .inherit + } + func visit(htmlBlock: HTMLBlock) -> VisitorContinueKind { + self.statistics.htmlBlocks += 1 + return .inherit + } + func visit(codeBlock: CodeBlock) -> VisitorContinueKind { + self.statistics.codeBlocks += 1 + return .inherit + } + func visit(thematicBreak: ThematicBreak) -> VisitorContinueKind { + self.statistics.thematicBreaks += 1 + return .inherit + } + + // MARK: - Inline + + func visit(inline: Inline) -> VisitorContinueKind { + self.statistics.inlines += 1 + return .inherit + } + + func visit(text: Text) -> VisitorContinueKind { + self.statistics.texts += 1 + return .inherit + } + func visit(strong: Strong) -> VisitorContinueKind { + self.statistics.strongs += 1 + return .inherit + } + func visit(emphasis: Emphasis) -> VisitorContinueKind { + self.statistics.emphasises += 1 + return .inherit + } + func visit(link: Link) -> VisitorContinueKind { + self.statistics.links += 1 + return .inherit + } + func visit(image: Image) -> VisitorContinueKind { + self.statistics.images += 1 + return .inherit + } + func visit(code: Code) -> VisitorContinueKind { + self.statistics.codes += 1 + return .inherit + } + func visit(rawHTML: RawHTML) -> VisitorContinueKind { + self.statistics.rawHTMLs += 1 + return .inherit + } + func visit(softLineBreak: SoftLineBreak) -> VisitorContinueKind { + self.statistics.softLineBreaks += 1 + return .inherit + } + func visit(hardLineBreak: HardLineBreak) -> VisitorContinueKind { + self.statistics.hardLineBreaks += 1 + return .inherit + } +} diff --git a/Tests/CommonMarkTests/VisitorTests.swift b/Tests/CommonMarkTests/VisitorTests.swift new file mode 100644 index 0000000..a92a8df --- /dev/null +++ b/Tests/CommonMarkTests/VisitorTests.swift @@ -0,0 +1,176 @@ +import XCTest +import CommonMark + +final class VisitorTests: XCTestCase { + func testVisitorVisitingChildren() throws { + 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) + + 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 testVisitorSkippingChildren() throws { + final class TestVisitor: StatisticsVisitor { + override func visit(link: Link) -> VisitorContinueKind { + let _ = super.visit(link: link) + + return .skipChildren + } + + override func visit(paragraph: Paragraph) -> VisitorContinueKind { + let _ = super.visit(paragraph: paragraph) + + return .skipChildren + } + } + + // 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) + + 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 testVisitorOverrides() throws { + final class TestVisitor: StatisticsVisitor { + override func visit(block: Block) -> VisitorContinueKind { + let _ = super.visit(block: block) + + return .skipChildren + } + + // Given that `Paragraph` is a subtype of `Block` it overrides: + override func visit(paragraph: Paragraph) -> VisitorContinueKind { + let _ = super.visit(paragraph: paragraph) + + return .visitChildren + } + } + + // 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) + + 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) + } +}