Commit e2c60845 authored by boojack's avatar boojack

fix(markdown): split mixed task and bullet lists

parent c268551a
...@@ -14,6 +14,7 @@ import { rehypeHeadingId } from "@/utils/rehype-plugins/rehype-heading-id"; ...@@ -14,6 +14,7 @@ import { rehypeHeadingId } from "@/utils/rehype-plugins/rehype-heading-id";
import { remarkDisableSetext } from "@/utils/remark-plugins/remark-disable-setext"; import { remarkDisableSetext } from "@/utils/remark-plugins/remark-disable-setext";
import { extractMentionUsernames, remarkMention } from "@/utils/remark-plugins/remark-mention"; import { extractMentionUsernames, remarkMention } from "@/utils/remark-plugins/remark-mention";
import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type"; import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type";
import { remarkSplitMixedTaskLists } from "@/utils/remark-plugins/remark-split-mixed-task-lists";
import { remarkTag } from "@/utils/remark-plugins/remark-tag"; import { remarkTag } from "@/utils/remark-plugins/remark-tag";
import { CodeBlock } from "./CodeBlock"; import { CodeBlock } from "./CodeBlock";
import { isMentionNode, isTagNode, isTaskListItemNode } from "./ConditionalComponent"; import { isMentionNode, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
...@@ -79,7 +80,16 @@ const MemoContent = (props: MemoContentProps) => { ...@@ -79,7 +80,16 @@ const MemoContent = (props: MemoContentProps) => {
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkDisableSetext, remarkMath, remarkGfm, remarkBreaks, remarkMention, remarkTag, remarkPreserveType]} remarkPlugins={[
remarkDisableSetext,
remarkMath,
remarkGfm,
remarkSplitMixedTaskLists,
remarkBreaks,
remarkMention,
remarkTag,
remarkPreserveType,
]}
rehypePlugins={[ rehypePlugins={[
rehypeRaw, rehypeRaw,
[rehypeSanitize, SANITIZE_SCHEMA], [rehypeSanitize, SANITIZE_SCHEMA],
......
import type { List, ListItem, Root } from "mdast";
import type { Parent } from "unist";
const isTaskListItem = (item: ListItem): boolean => typeof item.checked === "boolean";
const splitMixedList = (list: List): List[] => {
const hasTaskItem = list.children.some(isTaskListItem);
const hasRegularItem = list.children.some((item) => !isTaskListItem(item));
if (!hasTaskItem || !hasRegularItem) {
return [list];
}
const groups: Array<{ isTaskGroup: boolean; items: ListItem[] }> = [];
for (const item of list.children) {
const isTaskGroup = isTaskListItem(item);
const previousGroup = groups.at(-1);
if (previousGroup && previousGroup.isTaskGroup === isTaskGroup) {
previousGroup.items.push(item);
} else {
groups.push({ isTaskGroup, items: [item] });
}
}
return groups.map(({ isTaskGroup, items }) => ({
...list,
children: isTaskGroup ? items : items.map((item) => ({ ...item, spread: false })),
spread: isTaskGroup ? list.spread : false,
}));
};
const splitMixedTaskListsInParent = (parent: Parent): void => {
for (let index = 0; index < parent.children.length; index++) {
const child = parent.children[index];
if ("children" in child && Array.isArray(child.children)) {
splitMixedTaskListsInParent(child as Parent);
}
if (child.type !== "list") {
continue;
}
const splitLists = splitMixedList(child as List);
if (splitLists.length > 1) {
parent.children.splice(index, 1, ...splitLists);
index += splitLists.length - 1;
}
}
};
export const remarkSplitMixedTaskLists = () => {
return (tree: Root) => {
splitMixedTaskListsInParent(tree);
};
};
import { renderToStaticMarkup } from "react-dom/server";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { List, ListItem } from "@/components/MemoContent/markdown";
import { TASK_LIST_CLASS, TASK_LIST_ITEM_CLASS } from "@/components/MemoContent/constants";
import { remarkSplitMixedTaskLists } from "@/utils/remark-plugins/remark-split-mixed-task-lists";
import { describe, expect, it } from "vitest";
const renderListContent = (content: string): string =>
renderToStaticMarkup(
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkSplitMixedTaskLists]}
components={{
ul: ({ children, ...props }) => <List {...props}>{children}</List>,
li: ({ children, ...props }) => <ListItem {...props}>{children}</ListItem>,
}}
>
{content}
</ReactMarkdown>,
);
describe("memo content lists", () => {
it("keeps bullets on regular items in mixed task and bullet lists", () => {
const html = renderListContent("- [ ] pickup package\n- [ ] library returns\n\n- milk\n- eggs\n- bread");
const listOpenTags = html.match(/<ul class="[^"]*"/g) ?? [];
expect(listOpenTags).toHaveLength(2);
expect(listOpenTags[0]).toContain(TASK_LIST_CLASS);
expect(listOpenTags[0]).toContain("list-none");
expect(listOpenTags[0]).not.toContain("pl-6");
expect(listOpenTags[1]).not.toContain(TASK_LIST_CLASS);
expect(listOpenTags[1]).toContain("pl-6");
expect(listOpenTags[1]).toContain("list-disc");
expect(html).toContain('<li class="mt-0.5 leading-6">milk</li>');
expect(html).not.toContain('<li class="mt-0.5 leading-6">\n<p>milk</p>');
expect(html).toContain(TASK_LIST_ITEM_CLASS);
});
it("keeps compact styling for pure task lists", () => {
const html = renderListContent("- [ ] pickup package\n- [ ] library returns");
expect(html).toMatch(/<ul class="[^"]*\blist-none\b[^"]*"/);
expect(html).not.toMatch(/<ul class="[^"]*\blist-disc\b[^"]*"/);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment