fix: import a list indented less than the level base

A list's own indent is the offset from the level's 1.27cm base, and the
importers dropped it when the file put the list left of that (w:ind w:left="360"
is a common one), so every item rendered 6.3mm too far right. The offset is
signed now, floored at the base so the list stays inside the text column.
This commit is contained in:
Steffen Becker committed 2026-08-07 17:32:59 +00:00
1 parent 38e852190d
commit 8b9ba74a48
3 files changed
+12 -12

No files matched your search

+4 -8
View File
@@ -110,14 +110,10 @@ function clampFirst(cm: number): number {
return Math.round(Math.max(-INDENT_MAX_CM, Math.min(INDENT_MAX_CM, cm)) * 100) / 100;
}
// Signed: a first-line indent may be negative (a hanging indent), and on a list the
// value is the offset from the level's 1.27cm base, so a file whose list sits closer
// to the margin than that imports a negative one.
function parseIndent(value: string | null): number | null {
if (value == null || value === '') return null;
const n = parseFloat(value);
return Number.isFinite(n) && n > 0 ? n : null;
}
// A first-line indent may be negative (a hanging indent), unlike a left margin.
function parseSignedIndent(value: string | null): number | null {
if (value == null || value === '') return null;
const n = parseFloat(value);
return Number.isFinite(n) && n !== 0 ? n : null;
@@ -171,7 +167,7 @@ export const Indent = Extension.create({
// `indent`, which is the whole block's left margin.
indentFirst: {
default: null,
parseHTML: (element: HTMLElement) => parseSignedIndent(element.getAttribute('data-indent-first')),
parseHTML: (element: HTMLElement) => parseIndent(element.getAttribute('data-indent-first')),
renderHTML: (attributes: Record<string, unknown>) => {
if (attributes.indentFirst == null) return {};
const cm = Number(attributes.indentFirst);
+4 -2
View File
@@ -451,8 +451,10 @@ function makeListNode(ctx: Ctx, numId: number, ilvl: number): Node {
if (def.start != null && def.start > 1) attrs.start = def.start;
}
if (ilvl === 0 && def.leftTwip != null) {
const extra = round2(twipToCm(def.leftTwip) - LIST_LEFT_STEP_CM);
if (extra > LIST_INDENT_EPS_CM) attrs.indent = extra;
// Signed: a level's w:ind w:left may sit left of the editor's 1.27cm base
// (w:left="360" is a common one). Floored there so the list stays in the text column.
const extra = round2(Math.max(-LIST_LEFT_STEP_CM, twipToCm(def.leftTwip) - LIST_LEFT_STEP_CM));
if (Math.abs(extra) > LIST_INDENT_EPS_CM) attrs.indent = extra;
}
const node: Node = { type: bullet ? 'bulletList' : 'orderedList', content: [] };
if (Object.keys(attrs).length) node.attrs = attrs;
+4 -2
View File
@@ -1355,8 +1355,10 @@ function convertList(el: Element, ctx: Ctx, inheritedStyleName: string | null, d
if (depth === 1) {
const mlCm = listLevelMarginLeftCm(levelDef);
if (mlCm != null) {
const extra = Math.round((mlCm - LIST_BASE_MARGIN_CM) * 100) / 100;
if (extra > LIST_INDENT_EPS_CM) indent = extra;
// Signed, floored at the base: a list may sit left of the editor's 1.27cm level
// margin, but never left of the text column.
const extra = Math.round(Math.max(-LIST_BASE_MARGIN_CM, mlCm - LIST_BASE_MARGIN_CM) * 100) / 100;
if (Math.abs(extra) > LIST_INDENT_EPS_CM) indent = extra;
}
}