| author | |
| committer | |
| log | eaa6c4712fe11f0396e549b1873f163dc202d229 |
| tree | 8dfda75f414e3363ed9b7298c03f50782ca7808a |
| parent | a6ac27e7fd965f26ee0c61946e056f0fd889818c |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 77 insertions(+), 2 deletions(-)
apps/web/src/components/ChatMarkdown.test.tsx created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { orderedListGutterStyle } from "./ChatMarkdown"; | |
| 4 | ||
| 5 | describe("orderedListGutterStyle", () => { | |
| 6 | it("leaves the default gutter alone for single-digit lists", () => { | |
| 7 | expect(orderedListGutterStyle(9, undefined)).toBeUndefined(); | |
| 8 | }); | |
| 9 | ||
| 10 | it("leaves the default gutter alone for two-digit lists", () => { | |
| 11 | expect(orderedListGutterStyle(99, undefined)).toBeUndefined(); | |
| 12 | }); | |
| 13 | ||
| 14 | it("leaves the default gutter alone for a two-digit list that starts above 1", () => { | |
| 15 | // start=50 + 49 items => last marker is "98", still two digits. | |
| 16 | expect(orderedListGutterStyle(49, 50)).toBeUndefined(); | |
| 17 | }); | |
| 18 | ||
| 19 | it("widens the gutter once the last marker reaches three digits", () => { | |
| 20 | // item 100 is the bug from #6512: a 100-item list starting at 1. | |
| 21 | expect(orderedListGutterStyle(100, undefined)).toEqual({ "--list-gutter": "4ch" }); | |
| 22 | }); | |
| 23 | ||
| 24 | it("accounts for a non-default start attribute", () => { | |
| 25 | // start=95 + 9 items => last marker is "103", three digits. | |
| 26 | expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" }); | |
| 27 | }); | |
| 28 | ||
| 29 | it("scales further for four-digit markers", () => { | |
| 30 | expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" }); | |
| 31 | }); | |
| 32 | ||
| 33 | it("treats a missing/zero item count as a single item", () => { | |
| 34 | expect(orderedListGutterStyle(0, undefined)).toBeUndefined(); | |
| 35 | }); | |
| 36 | }); |
apps/web/src/components/ChatMarkdown.tsx+29| ... | ... | @@ -146,6 +146,26 @@ function findTaskListMarkerOffset(markdown: string, listItemStart: number): numb |
| 146 | 146 | if (!match?.[1]) return null; |
| 147 | 147 | return listItemStart + firstLine.indexOf(match[1]); |
| 148 | 148 | } |
| 149 | ||
| 150 | /** | |
| 151 | * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits two-digit | |
| 152 | * decimal markers. Once a list's last item reaches three digits (item 100+), | |
| 153 | * `list-style-position: outside` paints the marker wider than that gutter and | |
| 154 | * the leading digit gets clipped by the item's own overflow. Rather than | |
| 155 | * widening the gutter for every list, only lists whose last marker is 3+ | |
| 156 | * digits get a wider `--list-gutter`, sized to that marker's digit count. | |
| 157 | */ | |
| 158 | export function orderedListGutterStyle( | |
| 159 | itemCount: number, | |
| 160 | start: number | undefined, | |
| 161 | ): { "--list-gutter": string } | undefined { | |
| 162 | const firstNumber = typeof start === "number" && Number.isFinite(start) ? start : 1; | |
| 163 | const lastNumber = firstNumber + Math.max(itemCount - 1, 0); | |
| 164 | const digits = String(Math.abs(lastNumber)).length; | |
| 165 | if (digits <= 2) return undefined; | |
| 166 | return { "--list-gutter": `${digits + 1}ch` }; | |
| 167 | } | |
| 168 | ||
| 149 | 169 | const CHAT_MARKDOWN_SANITIZE_SCHEMA = { |
| 150 | 170 | ...defaultSchema, |
| 151 | 171 | attributes: { |
| ... | ... | @@ -1506,6 +1526,15 @@ function ChatMarkdown({ |
| 1506 | 1526 | </div> |
| 1507 | 1527 | ); |
| 1508 | 1528 | }, |
| 1529 | ol({ node, start, style, ...props }) { | |
| 1530 | const itemCount = | |
| 1531 | node?.children?.filter((child) => child.type === "element" && child.tagName === "li") | |
| 1532 | .length ?? 0; | |
| 1533 | const gutterStyle = orderedListGutterStyle(itemCount, start); | |
| 1534 | return ( | |
| 1535 | <ol {...props} start={start} style={gutterStyle ? { ...style, ...gutterStyle } : style} /> | |
| 1536 | ); | |
| 1537 | }, | |
| 1509 | 1538 | li({ node, children, ...props }) { |
| 1510 | 1539 | const listItemStart = node?.position?.start.offset; |
| 1511 | 1540 | const markerOffset = |
apps/web/src/index.css+12-2| ... | ... | @@ -1630,12 +1630,22 @@ code { |
| 1630 | 1630 | } |
| 1631 | 1631 | |
| 1632 | 1632 | .chat-markdown ul { |
| 1633 | /* Reset for nested uls under a widened ol — --list-gutter is an inherited | |
| 1634 | custom property, so without this a task-list under a 3+ digit ordered | |
| 1635 | list would inherit the outer gutter instead of its own default. */ | |
| 1636 | --list-gutter: 1.25rem; | |
| 1633 | 1637 | padding-left: 1.25rem; |
| 1634 | 1638 | list-style-type: disc; |
| 1635 | 1639 | } |
| 1636 | 1640 | |
| 1641 | /* --list-gutter defaults to the same 1.25rem as .chat-markdown ul, but | |
| 1642 | ChatMarkdown's `ol` renderer widens it (via inline style) for lists whose | |
| 1643 | last marker is 3+ digits, so item 100+ isn't clipped by list-style-position: | |
| 1644 | outside painting the marker past the padding box. Reset it here too so a | |
| 1645 | nested ol without its own widened marker doesn't inherit the outer one. */ | |
| 1637 | 1646 | .chat-markdown ol { |
| 1638 | padding-left: 1.25rem; | |
| 1647 | --list-gutter: 1.25rem; | |
| 1648 | padding-left: var(--list-gutter, 1.25rem); | |
| 1639 | 1649 | list-style-type: decimal; |
| 1640 | 1650 | } |
| 1641 | 1651 | |
| ... | ... | @@ -1665,7 +1675,7 @@ code { |
| 1665 | 1675 | } |
| 1666 | 1676 | |
| 1667 | 1677 | .chat-markdown li.task-list-item input[type="checkbox"] { |
| 1668 | margin: 0 0.35em 0.15em -1.25rem; | |
| 1678 | margin: 0 0.35em 0.15em calc(-1 * var(--list-gutter, 1.25rem)); | |
| 1669 | 1679 | vertical-align: middle; |
| 1670 | 1680 | } |
| 1671 | 1681 |