feat: add increase and decrease indent
This commit is contained in:
1 parent
a8456ba42f
commit
b8ed2aa14e
5 files changed
+300
-13
No files matched your search
@@ -648,6 +648,51 @@
|
||||
if (!editor) return;
|
||||
editor.chain().focus().setTextSelection(range).insertTable({ rows, cols, withHeaderRow: false }).run();
|
||||
}
|
||||
|
||||
// --- Indent (Einzug) ---
|
||||
// Resolve the selection's list context: whether the cursor is in a list, whether
|
||||
// the whole list is targeted (cursor in the first item, or the selection spans
|
||||
// every item), and the innermost list's current indent (cm).
|
||||
function listContext(): { inList: boolean; wholeList: boolean; listIndent: number } {
|
||||
if (!editor) return { inList: false, wholeList: false, listIndent: 0 };
|
||||
const { empty } = editor.state.selection;
|
||||
const head = editor.state.selection.$from;
|
||||
const tail = editor.state.selection.$to;
|
||||
let liDepth = -1;
|
||||
for (let d = head.depth; d > 0; d--) {
|
||||
if (head.node(d).type.name === 'listItem') { liDepth = d; break; }
|
||||
}
|
||||
if (liDepth < 0) return { inList: false, wholeList: false, listIndent: 0 };
|
||||
const listDepth = liDepth - 1;
|
||||
const list = head.node(listDepth);
|
||||
const listIndent = typeof list.attrs.indent === 'number' ? list.attrs.indent : 0;
|
||||
const fromFirst = head.index(listDepth) === 0;
|
||||
// Guard: a selection ending outside the list has no index at listDepth.
|
||||
const toLast = tail.depth >= listDepth && tail.node(listDepth) === list
|
||||
&& tail.index(listDepth) === list.childCount - 1;
|
||||
const wholeList = fromFirst && (empty || toLast);
|
||||
return { inList: true, wholeList, listIndent };
|
||||
}
|
||||
|
||||
// Word/LibreOffice indent: with the whole list targeted, shift the list as a
|
||||
// block; in a non-first item, change that item's nesting level; outside a list,
|
||||
// step the paragraph's left indent (indent.ts).
|
||||
function changeIndent(dir: 1 | -1) {
|
||||
if (!editor) return;
|
||||
const c = editor.chain().focus();
|
||||
const ctx = listContext();
|
||||
if (ctx.inList) {
|
||||
if (ctx.wholeList) {
|
||||
if (dir === 1) c.indentListMore().run();
|
||||
else if (ctx.listIndent > 0) c.indentListLess().run();
|
||||
else c.liftListItem('listItem').run();
|
||||
} else {
|
||||
(dir === 1 ? c.sinkListItem('listItem') : c.liftListItem('listItem')).run();
|
||||
}
|
||||
} else {
|
||||
(dir === 1 ? c.indentMore() : c.indentLess()).run();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="toolbar-expanded">
|
||||
@@ -704,8 +749,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<div class="size-picker" use:sizePickerClickOutside>
|
||||
<div class="size-trigger-wrap">
|
||||
<input
|
||||
@@ -808,14 +851,35 @@
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button onclick={() => changeIndent(-1)} title="Decrease indent">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<line x1="5" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="9" y1="8" x2="14" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="5" y1="13" x2="14" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M5.5 8H1.5M3.5 5.5L1.5 8L3.5 10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick={() => changeIndent(1)} title="Increase indent">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<line x1="5" y1="3" x2="14" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="9" y1="8" x2="14" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="5" y1="13" x2="14" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M1.5 8H5.5M3.5 5.5L5.5 8L3.5 10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<div class="lh-picker" use:lineHeightPickerClickOutside>
|
||||
<button class="lh-trigger" onclick={openLineHeightPicker} title="Line & paragraph spacing">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<line x1="7" y1="3" x2="16" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="7" y1="8" x2="16" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="7" y1="13" x2="16" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M2 5.5V1.5M2 1.5L0.3 3.4M2 1.5L3.7 3.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 10.5V14.5M2 14.5L0.3 12.6M2 14.5L3.7 12.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="3" x2="16" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="8" y1="8" x2="16" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="8" y1="13" x2="16" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M2.8 6.5V1.6M2.8 1.6L0.7 4.1M2.8 1.6L4.9 4.1" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.8 9.5V14.4M2.8 14.4L0.7 11.9M2.8 14.4L4.9 11.9" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
|
||||
<path d="M1 2.5l3 3 3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -901,9 +965,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<div class="layout-picker" use:layoutClickOutside>
|
||||
<div class="layout-picker right-group" use:layoutClickOutside>
|
||||
<button class="layout-trigger" onclick={openLayout} title="Page layout & margins" aria-pressed={layoutOpen}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="2.5" y="1.5" width="11" height="13" rx="1" stroke="currentColor" stroke-width="1.3"/>
|
||||
@@ -1062,6 +1124,7 @@
|
||||
.unsetTextAlign()
|
||||
.setSpaceBefore(null)
|
||||
.setSpaceAfter(null)
|
||||
.unsetIndent()
|
||||
.run()}
|
||||
title="Clear formatting"
|
||||
>
|
||||
@@ -1105,6 +1168,13 @@
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Layout sits at the far right (logical separation from the rest). order:1
|
||||
renders it after the left cluster; margin-left:auto opens the gap. Its
|
||||
dropdown anchors right so it stays inside the viewport. */
|
||||
.right-group { order: 1; }
|
||||
.layout-picker.right-group { margin-left: auto; }
|
||||
.layout-picker.right-group .layout-dropdown { left: auto; right: 0; }
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
|
||||
@@ -24,6 +24,7 @@ import TextAlign from '@tiptap/extension-text-align';
|
||||
import { PageBreaks } from './pageBreaks';
|
||||
import { LineHeight } from './lineHeight';
|
||||
import { ParagraphSpacing } from './paragraphSpacing';
|
||||
import { Indent } from './indent';
|
||||
import { FormattingMarks } from './formattingMarks';
|
||||
import { TableView } from './tableView';
|
||||
import { TableColumnResize } from './tableColumnResize';
|
||||
@@ -54,6 +55,7 @@ export const extensions = [
|
||||
HardBreak,
|
||||
LineHeight,
|
||||
ParagraphSpacing,
|
||||
Indent,
|
||||
Heading.configure({ levels: [1, 2, 3] }),
|
||||
BulletList,
|
||||
OrderedList,
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Extension, type CommandProps } from '@tiptap/core';
|
||||
import type { Node as PMNode } from '@tiptap/pm/model';
|
||||
|
||||
// Left indent stored in cm so it round-trips 1:1 to ODF's fo:margin-left.
|
||||
// On a paragraph/heading it maps to odf-kit's ParagraphOptions.indentLeft; on a
|
||||
// bulletList/orderedList it shifts the whole list (the indent is added to the
|
||||
// list style's level margins on export — see export/odt.ts applyListIndents).
|
||||
// Renders as an inline margin-left. List *items* indent by nesting
|
||||
// (sinkListItem/liftListItem); this list attr shifts the list as a block.
|
||||
|
||||
export const INDENT_STEP_CM = 1.25; // LibreOffice's default indent increment
|
||||
export const INDENT_MAX_CM = 12; // keep the indent inside the text column
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
indent: {
|
||||
indentMore: () => ReturnType;
|
||||
indentLess: () => ReturnType;
|
||||
indentListMore: () => ReturnType;
|
||||
indentListLess: () => ReturnType;
|
||||
unsetIndent: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function clampIndent(cm: number): number {
|
||||
return Math.round(Math.max(0, Math.min(INDENT_MAX_CM, cm)) * 100) / 100;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export const Indent = Extension.create({
|
||||
name: 'indent',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['paragraph', 'heading'] as string[],
|
||||
listTypes: ['bulletList', 'orderedList'] as string[],
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: [...this.options.types, ...this.options.listTypes],
|
||||
attributes: {
|
||||
indent: {
|
||||
default: null,
|
||||
parseHTML: (element: HTMLElement) => parseIndent(element.getAttribute('data-indent')),
|
||||
renderHTML: (attributes: Record<string, unknown>) => {
|
||||
if (attributes.indent == null) return {};
|
||||
const cm = Number(attributes.indent);
|
||||
return {
|
||||
'data-indent': String(cm),
|
||||
style: `margin-left: ${cm}cm`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
const types = this.options.types as string[];
|
||||
const listTypes = this.options.listTypes as string[];
|
||||
|
||||
// Step every paragraph/heading in the selection by delta cm, clamped to
|
||||
// [0, MAX]. Per-node (relative) change, so a mixed selection keeps each
|
||||
// block's offset; list-item paragraphs are skipped (lists indent by nesting).
|
||||
const step = (delta: number) => ({ state, tr, dispatch }: CommandProps) => {
|
||||
const { from, to } = state.selection;
|
||||
let changed = false;
|
||||
state.doc.nodesBetween(from, to, (node: PMNode, pos: number, parent: PMNode | null) => {
|
||||
if (!types.includes(node.type.name)) return;
|
||||
if (parent?.type.name === 'listItem') return;
|
||||
const current = typeof node.attrs.indent === 'number' ? node.attrs.indent : 0;
|
||||
const next = clampIndent(current + delta);
|
||||
const value = next > 0 ? next : null;
|
||||
if (value === (node.attrs.indent ?? null)) return;
|
||||
tr.setNodeMarkup(pos, undefined, { ...node.attrs, indent: value });
|
||||
changed = true;
|
||||
});
|
||||
if (changed && dispatch) dispatch(tr);
|
||||
return changed;
|
||||
};
|
||||
|
||||
// Step the innermost list ancestor's indent (shifts the whole list as a block).
|
||||
const stepList = (delta: number) => ({ state, tr, dispatch }: CommandProps) => {
|
||||
const { $from } = state.selection;
|
||||
for (let d = $from.depth; d > 0; d--) {
|
||||
const node = $from.node(d);
|
||||
if (!listTypes.includes(node.type.name)) continue;
|
||||
const current = typeof node.attrs.indent === 'number' ? node.attrs.indent : 0;
|
||||
const next = clampIndent(current + delta);
|
||||
const value = next > 0 ? next : null;
|
||||
if (value === (node.attrs.indent ?? null)) return false;
|
||||
if (dispatch) {
|
||||
tr.setNodeMarkup($from.before(d), undefined, { ...node.attrs, indent: value });
|
||||
dispatch(tr);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
indentMore: () => step(INDENT_STEP_CM),
|
||||
indentLess: () => step(-INDENT_STEP_CM),
|
||||
indentListMore: () => stepList(INDENT_STEP_CM),
|
||||
indentListLess: () => stepList(-INDENT_STEP_CM),
|
||||
unsetIndent: () => ({ commands }) =>
|
||||
[...types, ...listTypes].map((type) => commands.resetAttributes(type, 'indent')).some((r) => r),
|
||||
};
|
||||
},
|
||||
});
|
||||
+53
-1
@@ -70,6 +70,7 @@ function hasCustomAttrs(attrs: TiptapNode['attrs']): boolean {
|
||||
if (attrs.lineHeight) return true;
|
||||
if (attrs.spaceBefore != null) return true;
|
||||
if (attrs.spaceAfter != null) return true;
|
||||
if (typeof attrs.indent === 'number' && attrs.indent > 0) return true;
|
||||
const ta = attrs.textAlign;
|
||||
return ta === 'left' || ta === 'center' || ta === 'right' || ta === 'justify';
|
||||
}
|
||||
@@ -357,6 +358,47 @@ function applyOrderedListFormats(odtBytes: Uint8Array, formats: (OrderedFmt | nu
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// Whole-list indent: the `indent` attr on a top-level bulletList/orderedList (cm)
|
||||
// shifts the entire list. odf-kit emits each level in label-alignment mode (the
|
||||
// paragraph margin is ignored), so the shift must be added to the L# list-style's
|
||||
// per-level fo:margin-left and text:list-tab-stop-position. One entry per top-level
|
||||
// list in odf-kit's L# order (0 = no shift).
|
||||
function collectListIndents(node: TiptapNode, result: number[]): void {
|
||||
for (const child of node.content ?? []) {
|
||||
if (child.type === 'bulletList' || child.type === 'orderedList') {
|
||||
const ind = child.attrs?.indent;
|
||||
result.push(typeof ind === 'number' && ind > 0 ? ind : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyListIndents(odtBytes: Uint8Array, indents: number[]): Uint8Array {
|
||||
if (indents.every(v => !v)) return odtBytes;
|
||||
|
||||
const files = unzipSync(odtBytes);
|
||||
const contentBytes = files['content.xml'];
|
||||
if (!contentBytes) return odtBytes;
|
||||
|
||||
let content = strFromU8(contentBytes);
|
||||
const bump = (cm: number) => (_m: string, attr: string, v: string) =>
|
||||
`${attr}="${(parseFloat(v) + cm).toFixed(3)}cm"`;
|
||||
|
||||
indents.forEach((cm, i) => {
|
||||
if (!cm) return;
|
||||
const re = new RegExp(`(<text:list-style style:name="L${i + 1}">)([\\s\\S]*?)(</text:list-style>)`);
|
||||
content = content.replace(re, (_m, open: string, body: string, close: string) =>
|
||||
open +
|
||||
body
|
||||
.replace(/(fo:margin-left)="([\d.]+)cm"/g, bump(cm))
|
||||
.replace(/(text:list-tab-stop-position)="([\d.]+)cm"/g, bump(cm)) +
|
||||
close,
|
||||
);
|
||||
});
|
||||
|
||||
files['content.xml'] = strToU8(content);
|
||||
return rezipOdt(files);
|
||||
}
|
||||
|
||||
// odf-kit emits nested lists as bare <text:list> sharing the top-level L# style,
|
||||
// so a nested list of a *different* kind/format (e.g. ordered inside bullets)
|
||||
// loses its type. Give those nested lists their own minted 6-level list style —
|
||||
@@ -933,6 +975,7 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
align?: AlignValue;
|
||||
spaceBefore?: string;
|
||||
spaceAfter?: string;
|
||||
indentLeft?: string;
|
||||
} = {};
|
||||
if (node.attrs?.lineHeight != null) {
|
||||
const lhRaw = String(node.attrs.lineHeight);
|
||||
@@ -947,6 +990,10 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
}
|
||||
if (node.attrs?.spaceBefore != null) opts.spaceBefore = `${node.attrs.spaceBefore}pt`;
|
||||
if (node.attrs?.spaceAfter != null) opts.spaceAfter = `${node.attrs.spaceAfter}pt`;
|
||||
// Left indent → fo:margin-left (odf-kit emits it natively from indentLeft).
|
||||
if (typeof node.attrs?.indent === 'number' && node.attrs.indent > 0) {
|
||||
opts.indentLeft = `${node.attrs.indent}cm`;
|
||||
}
|
||||
const content = node.content ?? [];
|
||||
|
||||
if (node.type === CUST_P) {
|
||||
@@ -977,9 +1024,14 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
collectListItemStyles(raw, listStyles);
|
||||
const styledLists = applyListItemStyles(numberedOdt, listStyles);
|
||||
|
||||
// Whole-list indent → added to each L# list-style's level margins.
|
||||
const listIndents: number[] = [];
|
||||
collectListIndents(raw, listIndents);
|
||||
const indentedLists = applyListIndents(styledLists, listIndents);
|
||||
|
||||
// Rebuild real headings/lists/paragraphs inside table cells. Must run after
|
||||
// applyListItemStyles (cell lists don't exist yet) and before collapseRunWhitespace.
|
||||
const styledCells = applyCellBlocks(styledLists, cellBlocks);
|
||||
const styledCells = applyCellBlocks(indentedLists, cellBlocks);
|
||||
|
||||
const rowHeights: (string | null)[] = [];
|
||||
collectTableRowHeights(raw, rowHeights);
|
||||
|
||||
+45
-2
@@ -1,5 +1,5 @@
|
||||
import { unzipSync, strFromU8 } from 'fflate';
|
||||
import { StyleResolver, NS, lengthToPt, layerTextProps, type PropMap } from './styleResolver';
|
||||
import { StyleResolver, NS, lengthToPt, lengthToCm, layerTextProps, type PropMap } from './styleResolver';
|
||||
import { HEADING_STYLE_OVERRIDES, normalizeColor } from '../export/odt';
|
||||
import { DEFAULT_ORDERED_TYPE, orderedTypeFromFormat } from '../editor/orderedListTypes';
|
||||
import { PX_PER_CM, type PageMargins } from '../storage/pageMargins';
|
||||
@@ -60,6 +60,11 @@ const DEFAULT_FONTS = new Set(['times new roman', 'liberation serif']);
|
||||
// ODF line spacing multiplies the font's natural line height; see lineHeight.ts.
|
||||
const LINE_HEIGHT_RATIO = 1.15;
|
||||
|
||||
// odf-kit emits each list level at margin-left = level × 1.27cm (label-alignment).
|
||||
// A top-level list's margin beyond this level-1 base is its whole-list indent.
|
||||
const LIST_BASE_MARGIN_CM = 1.27;
|
||||
const LIST_INDENT_EPS_CM = 0.05;
|
||||
|
||||
// ---- entry --------------------------------------------------------------------
|
||||
|
||||
export function importOdt(bytes: Uint8Array): OdtImportResult {
|
||||
@@ -240,6 +245,13 @@ function blockAttrs(paraProps: PropMap, textProps: PropMap, headingLevel: number
|
||||
if (mt != null && Math.abs(mt - defTop) > EPS_PT) attrs.spaceBefore = snapPt(mt);
|
||||
if (mb != null && Math.abs(mb - defBottom) > EPS_PT) attrs.spaceAfter = snapPt(mb);
|
||||
|
||||
// Left indent → fo:margin-left (cm). Skip lists: their indent lives in the
|
||||
// list-style level properties, not paraProps. Default 0 is suppressed.
|
||||
if (kind !== 'list') {
|
||||
const ml = lengthToPt(paraProps['fo:margin-left']);
|
||||
if (ml != null && ml > EPS_PT) attrs.indent = Math.round((ml / 72) * 2.54 * 100) / 100;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
@@ -455,7 +467,22 @@ function convertList(el: Element, ctx: Ctx, inheritedStyleName: string | null, d
|
||||
}
|
||||
if (items.length === 0) return null;
|
||||
|
||||
if (!ordered) return { type: 'bulletList', content: items };
|
||||
// Whole-list indent (top level only): margin beyond the level-1 base. Nested
|
||||
// lists inherit it visually through DOM nesting, so only depth 1 carries it.
|
||||
let indent: number | null = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ordered) {
|
||||
const node: Node = { type: 'bulletList', content: items };
|
||||
if (indent != null) node.attrs = { indent };
|
||||
return node;
|
||||
}
|
||||
|
||||
const listStyleType = orderedTypeFromFormat(
|
||||
levelDef!.getAttributeNS(NS.style, 'num-format'),
|
||||
@@ -464,11 +491,27 @@ function convertList(el: Element, ctx: Ctx, inheritedStyleName: string | null, d
|
||||
const attrs: Record<string, unknown> = {};
|
||||
if (start != null) attrs.start = start;
|
||||
if (listStyleType !== DEFAULT_ORDERED_TYPE) attrs.listStyleType = listStyleType;
|
||||
if (indent != null) attrs.indent = indent;
|
||||
const node: Node = { type: 'orderedList', content: items };
|
||||
if (Object.keys(attrs).length) node.attrs = attrs;
|
||||
return node;
|
||||
}
|
||||
|
||||
// Level's fo:margin-left (cm) from its <style:list-level-label-alignment> (the
|
||||
// label-alignment mode odf-kit/LibreOffice use). null when absent.
|
||||
function listLevelMarginLeftCm(levelDef: Element | null): number | null {
|
||||
if (!levelDef) return null;
|
||||
for (const props of Array.from(levelDef.children)) {
|
||||
if (props.namespaceURI !== NS.style || props.localName !== 'list-level-properties') continue;
|
||||
for (const la of Array.from(props.children)) {
|
||||
if (la.namespaceURI === NS.style && la.localName === 'list-level-label-alignment') {
|
||||
return lengthToCm(la.getAttributeNS(NS.fo, 'margin-left'));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function listLevelDef(listStyle: Element | null, depth: number): Element | null {
|
||||
if (!listStyle) return null;
|
||||
for (const child of Array.from(listStyle.children)) {
|
||||
|
||||
Reference in new issue
Block a user