Skip to content

Commit 7a7c59d

Browse files
Improve performance of Markup.child(at:) method (#44)
Improves the performance of `Markup.child(at:)` by refactoring to removing the need to iterate over all previous elements in the child array.
1 parent 36cf89e commit 7a7c59d

File tree

2 files changed

+112
-3
lines changed

2 files changed

+112
-3
lines changed

Sources/Markdown/Base/Markup.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,31 @@ extension Markup {
186186
/// - Complexity: `O(childCount)`
187187
public func child(at position: Int) -> Markup? {
188188
precondition(position >= 0, "Cannot retrieve a child at negative index: \(position)")
189-
guard position <= raw.markup.childCount else {
189+
guard position < raw.markup.childCount else {
190190
return nil
191191
}
192-
var iterator = children.dropFirst(position).makeIterator()
193-
return iterator.next()
192+
193+
let childMetadata: MarkupMetadata
194+
if position == 0 {
195+
childMetadata = raw.metadata.firstChild()
196+
} else {
197+
let siblingSubtreeCount = (0..<position).reduce(0) { partialSubtreeCount, currentPosition in
198+
return partialSubtreeCount + raw.markup.child(at: currentPosition).subtreeCount
199+
}
200+
201+
let firstChildID = raw.metadata.firstChild().id
202+
let childID = MarkupIdentifier(
203+
rootId: firstChildID.rootId,
204+
childId: firstChildID.childId + siblingSubtreeCount
205+
)
206+
207+
childMetadata = MarkupMetadata(id: childID, indexInParent: indexInParent + position)
208+
}
209+
210+
let rawChild = raw.markup.child(at: position)
211+
let absoluteRawMarkup = AbsoluteRawMarkup(markup: rawChild, metadata: childMetadata)
212+
let data = _MarkupData(absoluteRawMarkup, parent: self)
213+
return makeMarkup(data)
194214
}
195215

196216
/// Traverse this markup tree by descending into the child at the index of each path element, returning `nil` if there is no child at that index or if the expected type for that path element doesn't match.

Tests/MarkdownTests/Base/MarkupTests.swift

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,93 @@ final class MarkupTests: XCTestCase {
283283
)!.debugDescription()
284284
)
285285
}
286+
287+
func testChildAtPositionHasCorrectType() throws {
288+
let source = "This is a [*link*](github.com). And some **bold** and *italic* text."
289+
290+
/*
291+
Document
292+
└─ Paragraph
293+
*/
294+
let document = Document(parsing: source)
295+
let paragraph = try XCTUnwrap(document.child(at: 0))
296+
assertEqualType(paragraph, Paragraph.self)
297+
298+
/*
299+
├─ Text "This is a "
300+
├─ Link destination: "github.com"
301+
│ └─ Emphasis
302+
│ └─ Text "link"
303+
304+
*/
305+
assertEqualType(paragraph.child(at: 0), Text.self)
306+
assertEqualType(paragraph.child(at: 1), Link.self)
307+
assertEqualType(paragraph.child(at: 1)?.child(at: 0), Emphasis.self)
308+
assertEqualType(paragraph.child(at: 1)?.child(at: 0)?.child(at: 0), Text.self)
309+
310+
/*
311+
├─ Text ". And some "
312+
├─ Strong
313+
│ └─ Text "bold"
314+
*/
315+
assertEqualType(paragraph.child(at: 2), Text.self)
316+
assertEqualType(paragraph.child(at: 3), Strong.self)
317+
assertEqualType(paragraph.child(at: 3)?.child(at: 0), Text.self)
318+
319+
/*
320+
├─ Text " and "
321+
├─ Emphasis
322+
│ └─ Text "italic"
323+
└─ Text " text."
324+
*/
325+
assertEqualType(paragraph.child(at: 4), Text.self)
326+
assertEqualType(paragraph.child(at: 5), Emphasis.self)
327+
assertEqualType(paragraph.child(at: 5)?.child(at: 0), Text.self)
328+
assertEqualType(paragraph.child(at: 6), Text.self)
329+
330+
XCTAssertNil(paragraph.child(at: 7))
331+
}
332+
333+
func testChildAtPositionHasCorrectMetadata() throws {
334+
let source = "This is a [*link*](github.com). And some **bold** and *italic* text."
335+
336+
let document = Document(parsing: source)
337+
let paragraph = try XCTUnwrap(document.child(at: 0) as? Paragraph)
338+
339+
for (index, sequencedChild) in paragraph.children.enumerated() {
340+
let indexedChild = try XCTUnwrap(paragraph.child(at: index))
341+
342+
let indexedChildMetadata = indexedChild.raw.metadata
343+
let sequencedChildMetadata = sequencedChild.raw.metadata
344+
345+
XCTAssertEqual(indexedChildMetadata.id, sequencedChildMetadata.id)
346+
XCTAssertEqual(indexedChildMetadata.indexInParent, sequencedChildMetadata.indexInParent)
347+
XCTAssertEqual(indexedChildMetadata.indexInParent, index)
348+
}
349+
}
350+
351+
func testChildAtPositionHasCorrectDataID() throws {
352+
let source = "This is a [*link*](github.com). And some **bold** and *italic* text."
353+
354+
let document = Document(parsing: source)
355+
let paragraph = try XCTUnwrap(document.child(at: 0) as? Paragraph)
356+
357+
for (index, sequencedChild) in paragraph.children.enumerated() {
358+
let indexedChild = try XCTUnwrap(paragraph.child(at: index))
359+
360+
XCTAssertEqual(indexedChild._data.id, sequencedChild._data.id)
361+
}
362+
}
363+
364+
func assertEqualType<FirstType, SecondType>(
365+
_ first: FirstType,
366+
_ second: SecondType.Type,
367+
file: StaticString = #file,
368+
line: UInt = #line
369+
) {
370+
guard first is SecondType else {
371+
XCTFail("'\(type(of: first))' is not expected type '\(second)'", file: file, line: line)
372+
return
373+
}
374+
}
286375
}

0 commit comments

Comments
 (0)