| ... | @@ -19,6 +19,7 @@ class FakeText { | ... | @@ -19,6 +19,7 @@ class FakeText { |
| 19 | | 19 | |
| 20 | class FakeElement { | 20 | class FakeElement { |
| 21 | readonly nodeType = ELEMENT_NODE; | 21 | readonly nodeType = ELEMENT_NODE; |
| | 22 | checked = false; |
| 22 | readonly childNodes: Array<FakeElement | FakeText> = []; | 23 | readonly childNodes: Array<FakeElement | FakeText> = []; |
| 23 | readonly classList = { | 24 | readonly classList = { |
| 24 | contains: (name: string) => this.classNames.includes(name), | 25 | contains: (name: string) => this.classNames.includes(name), |
| ... | @@ -61,18 +62,31 @@ class FakeElement { | ... | @@ -61,18 +62,31 @@ class FakeElement { |
| 61 | | 62 | |
| 62 | /** Supports only the selectors markdown-clipboard actually asks for. */ | 63 | /** Supports only the selectors markdown-clipboard actually asks for. */ |
| 63 | querySelector(selector: string): FakeElement | null { | 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 | const childOnly = selector.startsWith(":scope > "); | 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 | const matches = (element: FakeElement): boolean => { | 76 | const matches = (element: FakeElement): boolean => { |
| 67 | if (target === 'input[type="checkbox"]') { | 77 | if (target === 'input[type="checkbox"]') { |
| 68 | return element.tagName === "INPUT" && element.getAttribute("type") === "checkbox"; | 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 | const search = (parent: FakeElement): FakeElement | null => { | 82 | const search = (parent: FakeElement): FakeElement | null => { |
| 73 | for (const child of parent.childNodes) { | 83 | for (const child of parent.childNodes) { |
| 74 | if (!(child instanceof FakeElement)) continue; | 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 | if (!childOnly) { | 90 | if (!childOnly) { |
| 77 | const nested = search(child); | 91 | const nested = search(child); |
| 78 | if (nested) return nested; | 92 | if (nested) return nested; |
| ... | @@ -143,6 +157,57 @@ describe("serializeRenderedMarkdownFragment", () => { | ... | @@ -143,6 +157,57 @@ describe("serializeRenderedMarkdownFragment", () => { |
| 143 | expect(serializeRenderedMarkdownFragment(asNode(container))).toBe("run `git status` first"); | 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 | it("copies the complete quote, source, and comment instead of the comment-only chip label", () => { | 211 | it("copies the complete quote, source, and comment instead of the comment-only chip label", () => { |
| 147 | const citation = { | 212 | const citation = { |
| 148 | version: 1 as const, | 213 | version: 1 as const, |