Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## vNEXT (not yet published)

### `@liveblocks/react-ui`

- Improve Markdown lists in `AiChat`: better spacing and support for arbitrary
starting numbers in ordered lists. (e.g. `3.` instead of `1.`)

## v3.2.0

### `@liveblocks/react-ui`
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/api-reference/liveblocks-react-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ Override specific parts of `AiChat` with custom components.
</PropertiesListItem>
<PropertiesListItem
name="markdown.List"
detailedType={`({ type: "ordered" | "unordered", items: { checked?: boolean, children: ReactNode }[] }) => ReactNode`}
detailedType={`({ type: "ordered" | "unordered", items: { checked?: boolean, children: ReactNode }[], start?: number }) => ReactNode`}
>
The component used to render lists.
</PropertiesListItem>
Expand Down
36 changes: 36 additions & 0 deletions e2e/next-ai-kitchen-sink/app/styles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,42 @@ Just some plain text

---

1. A numbered list item
- A "nested" list item
- Another "nested" list item

2. Another numbered list item
- A "nested" list item
- Another "nested" list item

3. Yet another numbered list item
- A "nested" list item
- Another "nested" list item

---

1. A numbered list item

\`\`\`
const a = 2;
\`\`\`

2. Another numbered list item

> A quote.

3. Yet another numbered list item

A paragraph.

---

1. A numbered list item
1. Another numbered list item
1. Yet another numbered list item

---

The abbreviation for HyperText Markup Language is <abbr title="HyperText Markup Language">HTML</abbr>.

Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy.
Expand Down
32 changes: 22 additions & 10 deletions packages/liveblocks-react-ui/src/primitives/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertNever, sanitizeUrl } from "@liveblocks/core";
import { assertNever, type Relax, sanitizeUrl } from "@liveblocks/core";
import { Slot } from "@radix-ui/react-slot";
import { Lexer, type MarkedToken, type Token, type Tokens } from "marked";
import {
Expand Down Expand Up @@ -159,10 +159,10 @@ export type MarkdownComponents = {
* ```tsx
* <Markdown
* components={{
* List: ({ type, items }) => {
* List: ({ type, items, start }) => {
* const List = type === "ordered" ? "ol" : "ul";
* return (
* <List>
* <List start={start}>
* {items.map((item, index) => (
* <li key={index}>
* {item.checked !== undefined && (
Expand Down Expand Up @@ -257,8 +257,18 @@ interface MarkdownComponentsListItem {
children: ReactNode;
}

export interface MarkdownComponentsListProps {
type: "ordered" | "unordered";
export type MarkdownComponentsListProps = Relax<
MarkdownComponentsOrderedListProps | MarkdownComponentsUnorderedListProps
>;

interface MarkdownComponentsOrderedListProps {
type: "ordered";
items: MarkdownComponentsListItem[];
start: number;
}

interface MarkdownComponentsUnorderedListProps {
type: "unordered";
items: MarkdownComponentsListItem[];
}

Expand Down Expand Up @@ -368,11 +378,11 @@ const defaultComponents: MarkdownComponents = {
</table>
);
},
List: ({ type, items }) => {
List: ({ type, items, start }) => {
const List = type === "ordered" ? "ol" : "ul";

return (
<List>
<List start={start === 1 ? undefined : start}>
{items.map((item, index) => (
<li key={index}>
{item.checked !== undefined && (
Expand Down Expand Up @@ -586,9 +596,11 @@ export function MarkdownToken({
};
});

return (
<List type={token.ordered ? "ordered" : "unordered"} items={items} />
);
const props: MarkdownComponentsListProps = token.ordered
? { type: "ordered", items, start: token.start || 1 }
: { type: "unordered", items };

return <List {...props} />;
}

case "table": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,95 @@ describe("Markdown", () => {
);
},
},
{
description: "numbered lists with arbitrary start indices",
content: dedent`
1. A numbered list item
- A "nested" list item
- Another "nested" list item

2. Another numbered list item
- A "nested" list item
- Another "nested" list item

3. Yet another numbered list item
- A "nested" list item
- Another "nested" list item

---

1. A numbered list item

\`\`\`
const a = 2;
\`\`\`

2. Another numbered list item

> A quote.

3. Yet another numbered list item

A paragraph.

---

1. A numbered list item
1. Another numbered list item
1. Yet another numbered list item
`,
assertions: (element) => {
const listItems = element.querySelectorAll("li");
expect(listItems).toHaveLength(15);

// 1. A numbered list item
// - A "nested" list item
// - Another "nested" list item
//
// 2. Another numbered list item
// - A "nested" list item
// - Another "nested" list item
//
// 3. Yet another numbered list item
// - A "nested" list item
// - Another "nested" list item
const firstListFirstItem = listItems[0]?.parentElement;
expect(firstListFirstItem).not.toHaveAttribute("start");
const firstListSecondItem = listItems[3]?.parentElement;
expect(firstListSecondItem).toHaveAttribute("start", "2");
const firstListThirdItem = listItems[6]?.parentElement;
expect(firstListThirdItem).toHaveAttribute("start", "3");

// 1. A numbered list item
//
// \`\`\`
// const a = 2;
// \`\`\`
//
// 2. Another numbered list item
//
// > A quote.
//
// 3. Yet another numbered list item
//
// A paragraph.
const secondListFirstItem = listItems[9]?.parentElement;
expect(secondListFirstItem).not.toHaveAttribute("start");
const secondListSecondItem = listItems[10]?.parentElement;
expect(secondListSecondItem).toHaveAttribute("start", "2");
const secondListThirdItem = listItems[11]?.parentElement;
expect(secondListThirdItem).toHaveAttribute("start", "3");

// 1. A numbered list item
// 1. Another numbered list item
// 1. Yet another numbered list item
const thirdList = document.querySelector("ol:last-of-type");
expect(listItems[12]?.parentElement).toBe(thirdList);
expect(listItems[13]?.parentElement).toBe(thirdList);
expect(listItems[14]?.parentElement).toBe(thirdList);
expect(thirdList).not.toHaveAttribute("start");
},
},
{
description: "blockquotes",
content: dedent`
Expand Down Expand Up @@ -664,11 +753,11 @@ describe("Markdown", () => {
+ [x] Yet another list item
`,
components: {
List: ({ items, type }) => {
List: ({ items, type, start }) => {
const List = type === "ordered" ? "ol" : "ul";

return (
<List data-list={type}>
<List start={start} data-list={type}>
{items.map((item, index) => (
<li key={index}>
{item.checked !== undefined && (
Expand Down
2 changes: 1 addition & 1 deletion packages/liveblocks-react-ui/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@
flex-direction: column;
gap: 0.25em;
margin-block: 0.75em;
padding-inline-start: 1.5em;
padding-inline-start: 1.125em;
list-style-position: outside;

:where(ol, ul) {
Expand Down
Loading