From 6560757cb7725c2f8f2ce9c81263a365d42ea8ae Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 20 Sep 2026 17:20:20 +0800 Subject: [PATCH 1/2] Clarify mask input omission in minimal descriptions --- README.md | 12 +++++ .../EncodingReference.swift | 2 +- ...DisplayListDescriptionConverterTests.swift | 52 +++++++++++++++++++ ...ListMinimalDescriptionConverterTests.swift | 15 ++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5771545..691b9b3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,18 @@ The CodeMirror decoration and statistics interaction patterns are adapted from [ `DisplayList.description` prints identity effects, empty property effects, and platform effects with the same empty `(effect …)` form, while `minimalDescription` encodes them differently. Because the source text has already discarded that distinction, the converter renders an empty effect as identity and calls out the ambiguity in the interface. +### Mask input subtrees + +OpenSwiftUI prints the mask input DisplayList recursively in `description`, but prints only `M` +for the mask effect in `minimalDescription`. The items that follow `M` inside `E` are the masked +content, not the mask input. The converter follows this format. For example, identities repeated +only inside two mask inputs will not appear in the compact output or its item statistics. + +The `M` source mapping covers the full `(mask …)` expression, including its input subtree. +Inspect that source range to check mask identities. Reverse conversion uses `(mask *)`, because +the compact encoding does not contain the input tree. Shared links also retain only the compact +encoding, so use the full description when investigating repeated identities. + ## Local development Prerequisites: diff --git a/Sources/DisplayListDescription/EncodingReference.swift b/Sources/DisplayListDescription/EncodingReference.swift index 3e53626..dd7d945 100644 --- a/Sources/DisplayListDescription/EncodingReference.swift +++ b/Sources/DisplayListDescription/EncodingReference.swift @@ -49,7 +49,7 @@ public struct DisplayListEncodingReference: Equatable, Sendable { .init(id: "effect.opacity", token: "O", name: "Opacity", detail: "An opacity effect; the numeric value is omitted.", category: .effect), .init(id: "effect.blend-mode", token: "B", name: "Blend mode", detail: "A blend-mode effect. B is context-dependent: inside E it is not backdrop content.", category: .effect), .init(id: "effect.clip", token: "C", name: "Clip", detail: "A clipping path and style. C is context-dependent: inside E it is not color content.", category: .effect), - .init(id: "effect.mask", token: "M", name: "Mask", detail: "A mask effect.", category: .effect), + .init(id: "effect.mask", token: "M", name: "Mask", detail: "The mask input subtree and its item identities are omitted. Following items in E are the masked content. Inspect the full source to check mask identities.", category: .effect), .init(id: "effect.transform", token: "T", name: "Transform", detail: "A geometry transform. T is context-dependent: inside E it is not text content.", category: .effect), .init(id: "effect.filter", token: "F", name: "Graphics filter", detail: "Any graphics filter; the specific filter parameters are omitted.", category: .effect), .init(id: "effect.animation", token: "AN", name: "Animation", detail: "An animation effect.", category: .effect), diff --git a/Tests/DisplayListDescriptionTests/DisplayListDescriptionConverterTests.swift b/Tests/DisplayListDescriptionTests/DisplayListDescriptionConverterTests.swift index 4926d0b..b77b129 100644 --- a/Tests/DisplayListDescriptionTests/DisplayListDescriptionConverterTests.swift +++ b/Tests/DisplayListDescriptionTests/DisplayListDescriptionConverterTests.swift @@ -146,6 +146,58 @@ final class DisplayListDescriptionConverterTests: XCTestCase { XCTAssertTrue(result.usedEncodingIDs.contains("effect.view")) } + func testMaskInputsAreOmittedButRemainInSourceMappings() throws { + // The full printer can place options before or after the mask input items. + for optionsFirst in [true, false] { + let options = "(options ClipOptions(rawValue: 0))" + func maskedItem(identity: Int, maskIdentity: Int, contentIdentity: Int) -> String { + """ + (item #:identity \(identity) #:version 1 + (effect + (mask + \(optionsFirst ? options : "") + (item #:identity \(maskIdentity) #:version 1 + (effect + (item #:identity 815 #:version 1 + (effect #:blend-mode normal + (item #:identity 814 #:version 1 (content-seed 1) (shape *)))) + (item #:identity 816 #:version 1 + (effect (transform *) + (item #:identity 817 #:version 1 + (effect (transform *) + (item #:identity 819 #:version 1 + (effect #:blend-mode normal + (item #:identity 818 #:version 1 (content-seed 1) (shape *)))))))))) + \(optionsFirst ? "" : options)) + (item #:identity \(contentIdentity) #:version 1 (content-seed 1) (color *)))) + """ + } + let description = """ + (display-list + \(maskedItem(identity: 823, maskIdentity: 820, contentIdentity: 821)) + \(maskedItem(identity: 834, maskIdentity: 831, contentIdentity: 832))) + """ + + let result = try DisplayListDescriptionConverter.convert(description) + + XCTAssertEqual(result.minimalDescription, "(DL(I:823(E M(I:821 C)))(I:834(E M(I:832 C))))") + XCTAssertEqual(result.spans.filter { $0.encodingID == "structure.item" }.count, 4) + let masks = result.spans.filter { $0.encodingID == "effect.mask" } + XCTAssertEqual(masks.count, 2) + for mask in masks { + let source = utf16Slice(description, mask.sourceStart, mask.sourceEnd) + XCTAssertTrue(source.hasPrefix("(mask")) + XCTAssertTrue(source.contains(options)) + for identity in 814...819 { + XCTAssertTrue(source.contains("#:identity \(identity) ")) + } + XCTAssertFalse(source.contains("(color")) + XCTAssertEqual(utf16Slice(result.minimalDescription, mask.outputStart, mask.outputEnd), "M") + } + XCTAssertEqual(Set(masks.map(\.occurrenceID)).count, 2) + } + } + func testConvertsStates() throws { let description = """ (display-list diff --git a/Tests/DisplayListDescriptionTests/DisplayListMinimalDescriptionConverterTests.swift b/Tests/DisplayListDescriptionTests/DisplayListMinimalDescriptionConverterTests.swift index b698409..fa7db41 100644 --- a/Tests/DisplayListDescriptionTests/DisplayListMinimalDescriptionConverterTests.swift +++ b/Tests/DisplayListDescriptionTests/DisplayListMinimalDescriptionConverterTests.swift @@ -63,6 +63,21 @@ final class DisplayListMinimalDescriptionConverterTests: XCTestCase { ) } + func testReconstructsMaskInputAsUnknownAndPreservesMaskedContent() throws { + let minimalDescription = "(DL(I:823(E M(I:821 C)))(I:834(E M(I:832 C))))" + + let result = try DisplayListMinimalDescriptionConverter.convert(minimalDescription) + + XCTAssertEqual(result.description.components(separatedBy: "(mask *)").count - 1, 2) + XCTAssertEqual(result.description.components(separatedBy: "(item ").count - 1, 4) + XCTAssertTrue(result.description.contains("#:identity 821 ")) + XCTAssertTrue(result.description.contains("#:identity 832 ")) + XCTAssertEqual( + try DisplayListDescriptionConverter.convert(result.description).minimalDescription, + minimalDescription + ) + } + func testRejectsNonMinimalDescriptionRoot() { XCTAssertThrowsError(try DisplayListMinimalDescriptionConverter.convert("(display-list)")) { error in XCTAssertEqual( From e89a0fc05b6c62fffd160d90e73fc97df80ffb73 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 20 Sep 2026 17:36:36 +0800 Subject: [PATCH 2/2] Use SwiftUI consistently in format documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 691b9b3..53e03df 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The CodeMirror decoration and statistics interaction patterns are adapted from [ ### Mask input subtrees -OpenSwiftUI prints the mask input DisplayList recursively in `description`, but prints only `M` +SwiftUI prints the mask input DisplayList recursively in `description`, but prints only `M` for the mask effect in `minimalDescription`. The items that follow `M` inside `E` are the masked content, not the mask input. The converter follows this format. For example, identities repeated only inside two mask inputs will not appear in the compact output or its item statistics.