| ... | ... | @@ -19,6 +19,7 @@ class FakeText { |
| 19 | 19 | |
| 20 | 20 | class FakeElement { |
| 21 | 21 | readonly nodeType = ELEMENT_NODE; |
| 22 | checked = false; |
| 22 | 23 | readonly childNodes: Array<FakeElement | FakeText> = []; |
| 23 | 24 | readonly classList = { |
| 24 | 25 | contains: (name: string) => this.classNames.includes(name), |
| ... | ... | @@ -61,18 +62,31 @@ class FakeElement { |
| 61 | 62 | |
| 62 | 63 | /** Supports only the selectors markdown-clipboard actually asks for. */ |
| 63 | 64 | querySelector(selector: string): FakeElement | null { |
| 65 | if (selector.includes(", ")) { |
| 66 | for (const part of selector.split(", ")) { |
| 67 | const match = this.querySelector(part); |
| 68 | if (match) return match; |
| 69 | } |
| 70 | return null; |
| 71 | } |
| 64 | 72 | const childOnly = selector.startsWith(":scope > "); |
| 65 | | const target = childOnly ? selector.slice(":scope > ".length) : selector; |
| 73 | const [target, ...rest] = (childOnly ? selector.slice(":scope > ".length) : selector).split( |
| 74 | " > ", |
| 75 | ); |
| 66 | 76 | const matches = (element: FakeElement): boolean => { |
| 67 | 77 | if (target === 'input[type="checkbox"]') { |
| 68 | 78 | return element.tagName === "INPUT" && element.getAttribute("type") === "checkbox"; |
| 69 | 79 | } |
| 70 | | return element.tagName === target.toUpperCase(); |
| 80 | return element.tagName === target?.toUpperCase(); |
| 71 | 81 | }; |
| 72 | 82 | const search = (parent: FakeElement): FakeElement | null => { |
| 73 | 83 | for (const child of parent.childNodes) { |
| 74 | 84 | if (!(child instanceof FakeElement)) continue; |
| 75 | | if (matches(child)) return child; |
| 85 | if (matches(child)) { |
| 86 | if (rest.length === 0) return child; |
| 87 | const nested = child.querySelector(`:scope > ${rest.join(" > ")}`); |
| 88 | if (nested) return nested; |
| 89 | } |
| 76 | 90 | if (!childOnly) { |
| 77 | 91 | const nested = search(child); |
| 78 | 92 | if (nested) return nested; |
| ... | ... | @@ -143,6 +157,57 @@ describe("serializeRenderedMarkdownFragment", () => { |
| 143 | 157 | expect(serializeRenderedMarkdownFragment(asNode(container))).toBe("run `git status` first"); |
| 144 | 158 | }); |
| 145 | 159 | |
| 160 | describe.each([ |
| 161 | { parentLayout: "tight", childLayout: "tight" }, |
| 162 | { parentLayout: "tight", childLayout: "loose" }, |
| 163 | { parentLayout: "loose", childLayout: "tight" }, |
| 164 | { parentLayout: "loose", childLayout: "loose" }, |
| 165 | ])("$parentLayout parent with $childLayout child", ({ parentLayout, childLayout }) => { |
| 166 | it.each([ |
| 167 | { parentChecked: null, childChecked: true, parent: "- Parent", child: " - [x] Child" }, |
| 168 | { |
| 169 | parentChecked: false, |
| 170 | childChecked: true, |
| 171 | parent: "- [ ] Parent", |
| 172 | child: " - [x] Child", |
| 173 | }, |
| 174 | { |
| 175 | parentChecked: true, |
| 176 | childChecked: false, |
| 177 | parent: "- [x] Parent", |
| 178 | child: " - [ ] Child", |
| 179 | }, |
| 180 | ])("copies $parent with $child", ({ parentChecked, childChecked, parent, child }) => { |
| 181 | const parentContent = parentLayout === "loose" ? new FakeElement("P") : new FakeElement("LI"); |
| 182 | if (parentChecked !== null) { |
| 183 | const checkbox = new FakeElement("INPUT", [], { type: "checkbox" }); |
| 184 | checkbox.checked = parentChecked; |
| 185 | parentContent.append(checkbox, new FakeText(" ")); |
| 186 | } |
| 187 | parentContent.append(new FakeText("Parent")); |
| 188 | const parentItem = |
| 189 | parentLayout === "loose" ? new FakeElement("LI").append(parentContent) : parentContent; |
| 190 | const checkbox = new FakeElement("INPUT", [], { type: "checkbox" }); |
| 191 | checkbox.checked = childChecked; |
| 192 | const childContent = new FakeElement(childLayout === "loose" ? "P" : "LI").append( |
| 193 | checkbox, |
| 194 | new FakeText(" Child"), |
| 195 | ); |
| 196 | const childItem = |
| 197 | childLayout === "loose" ? new FakeElement("LI").append(childContent) : childContent; |
| 198 | parentItem.append(new FakeText("\n"), new FakeElement("UL").append(childItem)); |
| 199 | const container = new FakeElement("DIV").append( |
| 200 | new FakeElement("P").append(new FakeText("Before")), |
| 201 | new FakeElement("UL").append(parentItem), |
| 202 | new FakeElement("P").append(new FakeText("After")), |
| 203 | ); |
| 204 | |
| 205 | expect(serializeRenderedMarkdownFragment(asNode(container))).toBe( |
| 206 | `Before\n\n${parent}${parentLayout === "loose" ? "\n\n" : "\n"}${child}\n\nAfter`, |
| 207 | ); |
| 208 | }); |
| 209 | }); |
| 210 | |
| 146 | 211 | it("copies the complete quote, source, and comment instead of the comment-only chip label", () => { |
| 147 | 212 | const citation = { |
| 148 | 213 | version: 1 as const, |