Files
edentext/tests/unit/ordered-list-decorations.test.ts
Steffen Becker 697eea4d86 fix: a list style's level decides the depth's kind
effectiveListLevel resolved a level only for its own node species, so a
Tab-nested <ul> under a number level kept cycle bullets while the named
ODF definition (and LibreOffice) numbered it. The level's kind now wins:
the walk numbers a <ul>/bullets an <ol> (CSS keys generalized), both
exporters resolve clones and forks through eff.kind (new ODT pass
applyListLevelKinds switches L# level species), and setListStyle retypes
the subtree. Node marker attrs still override with their own species.
2026-08-26 09:21:45 +00:00

111 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { getSchema } from '@tiptap/core';
import type { Node as PmNode } from '@tiptap/pm/model';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import ListItem from '@tiptap/extension-list-item';
import { OrderedList } from '../../src/lib/editor/extensions/orderedList';
import { BulletList } from '../../src/lib/editor/extensions/bulletList';
import { ListStyle, listStyleDecos } from '../../src/lib/editor/extensions/listStyle';
import { builtinStyleSheet, type StyleSheet } from '../../src/lib/styles/styleSheet';
const schema = getSchema([Document, Paragraph, Text, ListItem, OrderedList, BulletList, ListStyle]);
const p = (t: string) => schema.nodes.paragraph.create(null, schema.text(t));
const li = (...content: PmNode[]) => schema.nodes.listItem.create(null, content);
const ol = (attrs: Record<string, unknown> | null, ...items: PmNode[]) => schema.nodes.orderedList.create(attrs, items);
const ul = (...items: PmNode[]) => schema.nodes.bulletList.create(null, items);
const doc = (...content: PmNode[]) => schema.nodes.doc.create(null, content);
// The effective data-eff-list-style the plugin tags each <ol> with, in document order.
function effStyles(d: PmNode): (string | null)[] {
const set = listStyleDecos(d, builtinStyleSheet());
const out: (string | null)[] = [];
d.descendants((node, pos) => {
if (node.type.name !== 'orderedList') return;
const deco = set.find(pos, pos + 1).find((x) => x.from === pos) as { type?: { attrs?: Record<string, string> } } | undefined;
out.push(deco?.type?.attrs?.['data-eff-list-style'] ?? null);
});
return out;
}
describe('orderedListStyleDecos (effective numbering per <ol>)', () => {
it('advances the default cycle 1. → a. → i. with plain nesting', () => {
const d = doc(ol(null, li(p('one'), ol(null, li(p('two'), ol(null, li(p('three'))))))));
expect(effStyles(d)).toEqual(['decimal', 'lower-alpha', 'lower-roman']);
});
it('re-anchors: an explicit "a., b." parent makes its child default to i.', () => {
const d = doc(ol({ listStyleType: 'lower-alpha' }, li(p('a'), ol(null, li(p('i'))))));
expect(effStyles(d)).toEqual(['lower-alpha', 'lower-roman']);
});
it('inherits the paren suffix: an explicit "a)" parent makes its child default to i)', () => {
const d = doc(ol({ listStyleType: 'lower-alpha-paren' },
li(p('a'), ol(null, li(p('i'), ol(null, li(p('1'))))))));
expect(effStyles(d)).toEqual(['lower-alpha-paren', 'lower-roman-paren', 'decimal-paren']);
});
it('propagates multilevel to attr-less chain members', () => {
const d = doc(ol({ listStyleType: 'multilevel' }, li(p('1'), ol(null, li(p('1.1'))))));
expect(effStyles(d)).toEqual(['multilevel', 'multilevel']);
});
it('counts a bullet ancestor: an ol one level inside a ul defaults to a.', () => {
const d = doc(ul(li(p('x'), ol(null, li(p('y'))))));
expect(effStyles(d)).toEqual(['lower-alpha']);
});
});
describe('named list styles decide each depth\'s kind', () => {
const uls = (attrs: Record<string, unknown> | null, ...items: PmNode[]) => schema.nodes.bulletList.create(attrs, items);
const sheet = (): StyleSheet => {
const s = builtinStyleSheet();
s.list['Mixed'] = { name: 'Mixed', levels: [
{ kind: 'bullet', bulletChar: '–' },
{ kind: 'number', numType: 'decimal' },
{ kind: 'number', numType: 'decimal', startAt: 4 },
] };
return s;
};
// Every list node's decoration attrs, in document order.
function decoAttrs(d: PmNode): Record<string, string | undefined>[] {
const set = listStyleDecos(d, sheet());
const out: Record<string, string | undefined>[] = [];
d.descendants((node, pos) => {
if (node.type.name !== 'orderedList' && node.type.name !== 'bulletList') return;
const deco = set.find(pos, pos + 1).find((x) => x.from === pos) as { type?: { attrs?: Record<string, string> } } | undefined;
out.push(deco?.type?.attrs ?? {});
});
return out;
}
it('a number level numbers a nested <ul>; the start rides the counter', () => {
const d = doc(uls({ listStyleName: 'Mixed' },
li(p('a'), uls(null, li(p('b'), uls(null, li(p('c'))))))));
const [l1, l2, l3] = decoAttrs(d);
expect(l1['data-bullet']).toBe('–');
expect(l1['data-eff-list-style']).toBeUndefined();
expect(l2['data-eff-list-style']).toBe('decimal');
expect(l3['data-eff-list-style']).toBe('decimal');
expect(l3.style).toContain('counter-reset: list-item 3');
});
it('a bullet level bullets a nested <ol>', () => {
const s = sheet();
s.list['Rev'] = { name: 'Rev', levels: [{ kind: 'number', numType: 'decimal' }, { kind: 'bullet', bulletChar: '✓' }] };
const d = doc(ol({ listStyleName: 'Rev' }, li(p('1'), ol(null, li(p('x'))))));
const set = listStyleDecos(d, s);
const out: Record<string, string | undefined>[] = [];
d.descendants((node, pos) => {
if (node.type.name !== 'orderedList') return;
const deco = set.find(pos, pos + 1).find((x) => x.from === pos) as { type?: { attrs?: Record<string, string> } } | undefined;
out.push(deco?.type?.attrs ?? {});
});
expect(out[0]['data-eff-list-style']).toBe('decimal');
expect(out[1]['data-bullet']).toBe('✓');
expect(out[1]['data-eff-list-style']).toBeUndefined();
});
});