mirror of https://github.com/usememos/memos
fix(markdown): split mixed task and bullet lists
parent
c268551a16
commit
e2c60845ea
@ -0,0 +1,57 @@
|
||||
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);
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,45 @@
|
||||
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[^"]*"/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue