Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5af3ec9
feat: implement context menu for lists
chittolina Apr 22, 2026
58bffc3
fix: infinite loop
chittolina Apr 23, 2026
499762a
refactor: simplified code
chittolina Apr 24, 2026
ba2bc6f
refactor: simplified continue/restart numbering logic
chittolina Apr 24, 2026
ed61181
refactor: simplified code
chittolina Apr 24, 2026
641b1f2
refactor: perf optimization
chittolina Apr 24, 2026
b170c88
refactor: simplified logic on document-section/helpers.js
chittolina Apr 24, 2026
ebe3f67
refactor: created const for list marker class
chittolina Apr 30, 2026
bcd5de5
refactor: use list marker class naem on renderTableCell
chittolina Apr 30, 2026
fae4a96
refactor: move const to dom-contract
chittolina Apr 30, 2026
16eb3a1
refactor: unify logic to create list marker element
chittolina Apr 30, 2026
9518489
fix: default to ilvl 0
chittolina Apr 30, 2026
c352203
refactor: merge w/ main
chittolina May 1, 2026
2cce922
fix: added missing imports
chittolina May 1, 2026
e870e62
fix: source anchor when rendering paragraph
chittolina May 1, 2026
6d8c9c9
fix: build
chittolina May 1, 2026
c29f393
test: failing regression for headless continue/restart numbering disp…
caio-pizzol May 6, 2026
d33d978
Merge branch 'main' into gabriel/sd-2524-feature-support-right-click-…
chittolina May 6, 2026
ef93e57
fix: tests
chittolina May 6, 2026
260042b
Merge remote-tracking branch 'origin/main' into gabriel/sd-2524-featu…
chittolina May 6, 2026
63fdda6
Merge remote-tracking branch 'origin/main' into gabriel/sd-2524-featu…
chittolina May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/layout-engine/dom-contract/src/class-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export const DOM_CLASS_NAMES = {
/** Inline image element (ImageRun inside a paragraph). */
INLINE_IMAGE: 'superdoc-inline-image',

/** Wrapper around a paragraph's list marker (bullet glyph or ordered number). */
LIST_MARKER: 'superdoc-list-marker',

/** Clip wrapper around a cropped inline image. */
INLINE_IMAGE_CLIP_WRAPPER: 'superdoc-inline-image-clip-wrapper',

Expand Down
1 change: 1 addition & 0 deletions packages/layout-engine/dom-contract/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('@superdoc/dom-contract', () => {
SDT_GROUP_HOVER: 'sdt-group-hover',
IMAGE_FRAGMENT: 'superdoc-image-fragment',
INLINE_IMAGE: 'superdoc-inline-image',
LIST_MARKER: 'superdoc-list-marker',
INLINE_IMAGE_CLIP_WRAPPER: 'superdoc-inline-image-clip-wrapper',
ANNOTATION: 'annotation',
ANNOTATION_CONTENT: 'annotation-content',
Expand Down
63 changes: 13 additions & 50 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import { applyImageClipPath } from './utils/image-clip-path.js';
import { isMinimalWordLayout as isMinimalWordLayoutShared } from '@superdoc/common/list-marker-utils';
import {
computeTabWidth,
createListMarkerElement,
resolvePainterListMarkerGeometry,
resolvePainterListTextStartPx,
} from './utils/marker-helpers.js';
Expand Down Expand Up @@ -541,7 +542,7 @@ function compactSnapshotObject<T extends Record<string, unknown>>(input: T): T {
return out;
}

function applySourceAnchorDataset(element: HTMLElement, sourceAnchor?: SourceAnchor): void {
export function applySourceAnchorDataset(element: HTMLElement, sourceAnchor?: SourceAnchor): void {
if (!sourceAnchor) {
delete element.dataset.sourceAnchor;
delete element.dataset.sourceNodeId;
Expand Down Expand Up @@ -3223,18 +3224,12 @@ export class DomPainter {
lineEl.style.paddingLeft = `${resolvedMarker.firstLinePaddingLeftPx}px`;

if (!resolvedMarker.vanish) {
const markerContainer = this.doc!.createElement('span');
markerContainer.style.display = 'inline-block';
markerContainer.style.wordSpacing = '0px';

const markerEl = this.doc!.createElement('span');
markerEl.classList.add('superdoc-paragraph-marker');
markerEl.textContent = resolvedMarker.text;
applySourceAnchorDataset(
markerEl,
resolvedMarker.sourceAnchor ?? resolvedItem?.sourceAnchor ?? fragment.sourceAnchor,
const markerContainer = createListMarkerElement(
this.doc!,
resolvedMarker.text,
resolvedMarker.run,
resolvedMarker.sourceAnchor ?? resolvedItem?.sourceAnchor ?? (fragment.sourceAnchor as SourceAnchor),
);
markerEl.style.pointerEvents = 'none';

markerContainer.style.position = 'relative';
if (resolvedMarker.justification === 'right') {
Expand All @@ -3247,19 +3242,6 @@ export class DomPainter {
parseFloat(lineEl.style.paddingLeft) + (resolvedMarker.centerPaddingAdjustPx ?? 0) + 'px';
}

markerEl.style.fontFamily =
toCssFontFamily(resolvedMarker.run.fontFamily) ?? resolvedMarker.run.fontFamily;
markerEl.style.fontSize = `${resolvedMarker.run.fontSize}px`;
markerEl.style.fontWeight = resolvedMarker.run.bold ? 'bold' : '';
markerEl.style.fontStyle = resolvedMarker.run.italic ? 'italic' : '';
if (resolvedMarker.run.color) {
markerEl.style.color = resolvedMarker.run.color;
}
if (resolvedMarker.run.letterSpacing != null) {
markerEl.style.letterSpacing = `${resolvedMarker.run.letterSpacing}px`;
}
markerContainer.appendChild(markerEl);

if (resolvedMarker.suffix === 'tab') {
const tabEl = this.doc!.createElement('span');
tabEl.className = 'superdoc-tab';
Expand Down Expand Up @@ -3441,19 +3423,12 @@ export class DomPainter {
lineEl.style.paddingLeft = `${paraIndentLeft + (paraIndent?.firstLine ?? 0) - (paraIndent?.hanging ?? 0)}px`;

if (!marker.run.vanish) {
const markerContainer = this.doc!.createElement('span');
markerContainer.style.display = 'inline-block';
markerContainer.style.wordSpacing = '0px';

const markerEl = this.doc!.createElement('span');
markerEl.classList.add('superdoc-paragraph-marker');
markerEl.textContent = marker.markerText ?? '';
applySourceAnchorDataset(
markerEl,
block.sourceAnchor ?? resolvedItem?.sourceAnchor ?? fragment.sourceAnchor,
const markerContainer = createListMarkerElement(
this.doc!,
marker.markerText ?? '',
marker.run,
block.sourceAnchor ?? resolvedItem?.sourceAnchor ?? (fragment.sourceAnchor as SourceAnchor),
);
markerEl.style.pointerEvents = 'none';

const markerJustification = marker.justification ?? 'left';

markerContainer.style.position = 'relative';
Expand All @@ -3466,18 +3441,6 @@ export class DomPainter {
lineEl.style.paddingLeft = parseFloat(lineEl.style.paddingLeft) + fragment.markerTextWidth! / 2 + 'px';
}

markerEl.style.fontFamily = toCssFontFamily(marker.run.fontFamily) ?? marker.run.fontFamily;
markerEl.style.fontSize = `${marker.run.fontSize}px`;
markerEl.style.fontWeight = marker.run.bold ? 'bold' : '';
markerEl.style.fontStyle = marker.run.italic ? 'italic' : '';
if (marker.run.color) {
markerEl.style.color = marker.run.color;
}
if (marker.run.letterSpacing != null) {
markerEl.style.letterSpacing = `${marker.run.letterSpacing}px`;
}
markerContainer.appendChild(markerEl);

const suffix = marker.suffix ?? 'tab';
if (suffix === 'tab') {
const tabEl = this.doc!.createElement('span');
Expand Down Expand Up @@ -3664,7 +3627,7 @@ export class DomPainter {
}

const markerEl = this.doc.createElement('span');
markerEl.classList.add('superdoc-list-marker');
markerEl.classList.add(DOM_CLASS_NAMES.LIST_MARKER);
applySourceAnchorDataset(
markerEl,
item.marker.sourceAnchor ?? item.sourceAnchor ?? resolvedItem?.sourceAnchor ?? fragment.sourceAnchor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import type {
WrapTextMode,
} from '@superdoc/contracts';
import { effectiveTableCellSpacing, rescaleColumnWidths, normalizeZIndex, getCellSpacingPx } from '@superdoc/contracts';
import { toCssFontFamily } from '@superdoc/font-utils';
import { createListMarkerElement,
computeTabWidth,
resolvePainterListMarkerGeometry,
resolvePainterListTextStartPx } from '../utils/marker-helpers.js';
import type { FragmentRenderContext, RenderedLineInfo } from '../renderer.js';
import { applyParagraphBorderStyles, applyParagraphShadingStyles } from '../features/paragraph-borders/index.js';
import { applySquareWrapExclusionsToLines } from '../utils/anchor-helpers';
Expand All @@ -29,11 +32,6 @@ import {
getSdtContainerKey,
type SdtBoundaryOptions,
} from '../utils/sdt-helpers.js';
import {
computeTabWidth,
resolvePainterListMarkerGeometry,
resolvePainterListTextStartPx,
} from '../utils/marker-helpers.js';
import { applyCellBorders } from './border-utils.js';
import { renderTableFragment as renderTableFragmentElement } from './renderTableFragment.js';

Expand Down Expand Up @@ -351,29 +349,7 @@ function renderListMarker(params: MarkerRenderParams): void {
return;
}

// Create marker container (inline-block to isolate from word-spacing used for justification)
const markerContainer = doc.createElement('span');
markerContainer.style.display = 'inline-block';
markerContainer.style.wordSpacing = '0px';

const markerEl = doc.createElement('span');
markerEl.classList.add('superdoc-paragraph-marker');
markerEl.textContent = markerLayout?.markerText ?? '';
markerEl.style.pointerEvents = 'none';

// Apply marker run styling
markerEl.style.fontFamily = toCssFontFamily(markerLayout?.run?.fontFamily) ?? markerLayout?.run?.fontFamily ?? '';
if (markerLayout?.run?.fontSize != null) {
markerEl.style.fontSize = `${markerLayout.run.fontSize}px`;
}
markerEl.style.fontWeight = markerLayout?.run?.bold ? 'bold' : '';
markerEl.style.fontStyle = markerLayout?.run?.italic ? 'italic' : '';
if (markerLayout?.run?.color) {
markerEl.style.color = markerLayout.run.color;
}
if (markerLayout?.run?.letterSpacing != null) {
markerEl.style.letterSpacing = `${markerLayout.run.letterSpacing}px`;
}
const markerContainer = createListMarkerElement(doc, markerLayout?.markerText ?? '', markerLayout?.run ?? {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flow markers carry source anchors via the helper now, but this call passes 3 args so table markers don't. snapshot consumers walking markers will skip them. any reason not to pass markerLayout?.sourceAnchor here, like the renderer.ts call at 3231?


// Left-justified markers stay inline (position: relative) within the text flow.
// Right/center-justified markers are absolutely positioned.
Expand All @@ -388,8 +364,6 @@ function renderListMarker(params: MarkerRenderParams): void {
lineEl.style.paddingLeft = parseFloat(lineEl.style.paddingLeft) + markerTextWidth / 2 + 'px';
}

markerContainer.appendChild(markerEl);

// Add suffix separator after marker, before text content
const suffixType = markerLayout?.suffix ?? 'tab';
if (suffixType === 'tab') {
Expand Down
55 changes: 55 additions & 0 deletions packages/layout-engine/painters/dom/src/utils/marker-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DOM_CLASS_NAMES } from '@superdoc/dom-contract';
import { toCssFontFamily } from '@superdoc/font-utils';
import {
resolveListMarkerGeometry,
resolveListTextStartPx,
Expand All @@ -6,6 +8,8 @@ import {
type MinimalWordLayout,
type ResolvedListMarkerGeometry,
} from '@superdoc/common/list-marker-utils';
import { applySourceAnchorDataset } from '../renderer';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file and renderer.ts import each other (helper pulls applySourceAnchorDataset from renderer; renderer pulls createListMarkerElement from here). works now, fragile. move applySourceAnchorDataset into its own small file. while you're there, import type { SourceAnchor } since it's only used as a type.

import { SourceAnchor } from '@superdoc/contracts';

type PainterListTextStartParams = {
wordLayout: MinimalWordLayout | undefined;
Expand Down Expand Up @@ -74,3 +78,54 @@ export const resolvePainterListTextStartPx = ({

// Re-export computeTabWidth from shared module
export { computeTabWidth };

type MarkerRunStyle = {
fontFamily?: string | null;
fontSize?: number | null;
bold?: boolean | null;
italic?: boolean | null;
color?: string | null;
letterSpacing?: number | null;
};

/**
* Build the marker container `<span class="superdoc-list-marker">` with the inner
* `<span class="superdoc-paragraph-marker">` already appended and styled from the
* given run. Callers handle positioning, suffix separators, and the final prepend.
*/
export const createListMarkerElement = (
doc: Document,
markerText: string,
run: MarkerRunStyle,
sourceAnchor?: SourceAnchor,
): HTMLElement => {
const markerContainer = doc.createElement('span');
markerContainer.classList.add(DOM_CLASS_NAMES.LIST_MARKER);
markerContainer.style.display = 'inline-block';
markerContainer.style.wordSpacing = '0px';

const markerEl = doc.createElement('span');
markerEl.classList.add('superdoc-paragraph-marker');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this PR is moving list-marker classes into DOM_CLASS_NAMES, superdoc-paragraph-marker is the obvious next one. fine as a follow-up.

markerEl.textContent = markerText;
markerEl.style.pointerEvents = 'none';
markerEl.style.fontFamily = toCssFontFamily(run.fontFamily) ?? run.fontFamily ?? '';

if (run.fontSize != null) {
markerEl.style.fontSize = `${run.fontSize}px`;
}
markerEl.style.fontWeight = run.bold ? 'bold' : '';
markerEl.style.fontStyle = run.italic ? 'italic' : '';

if (run.color) {
markerEl.style.color = run.color;
}
if (run.letterSpacing != null) {
markerEl.style.letterSpacing = `${run.letterSpacing}px`;
}

markerContainer.appendChild(markerEl);
if (sourceAnchor) {
applySourceAnchorDataset(markerEl, sourceAnchor);
}
return markerContainer;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import pasteIconSvg from '@superdoc/common/icons/paste-solid.svg?raw';
import checkIconSvg from '@superdoc/common/icons/check-solid.svg?raw';
import xMarkIconSvg from '@superdoc/common/icons/xmark-solid.svg?raw';
import paintRollerIconSvg from '@superdoc/common/icons/paint-roller-solid.svg?raw';
import indentIconSvg from '@superdoc/common/icons/indent-solid.svg?raw';
import outdentIconSvg from '@superdoc/common/icons/outdent-solid.svg?raw';
import listOlIconSvg from '@superdoc/common/icons/list-ol-solid.svg?raw';

export const ICONS = {
addRowBefore: plusIconSvg,
Expand All @@ -37,6 +40,10 @@ export const ICONS = {
trackChangesAccept: checkIconSvg,
trackChangesReject: xMarkIconSvg,
cellBackground: paintRollerIconSvg,
listRestartNumbering: listOlIconSvg,
listContinueNumbering: listOlIconSvg,
listDecreaseIndent: outdentIconSvg,
listIncreaseIndent: indentIconSvg,
};

// Table actions constant
Expand Down Expand Up @@ -65,6 +72,10 @@ export const TEXTS = {
trackChangesAccept: 'Accept change',
trackChangesReject: 'Reject change',
cellBackground: 'Cell background',
listRestartNumbering: 'Restart numbering',
listContinueNumbering: 'Continue numbering',
listDecreaseIndent: 'Decrease indent',
listIncreaseIndent: 'Increase indent',
};

export const tableActionsOptions = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,44 @@ export function getItems(context, customItems = [], includeDefaultItems = true)
},
],
},
{
id: 'list-marker',
isDefault: true,
items: [
{
id: 'list-restart-numbering',
label: TEXTS.listRestartNumbering,
icon: ICONS.listRestartNumbering,
isDefault: true,
action: (editor) => editor.commands.restartNumbering(),
showWhen: (context) => context.trigger === TRIGGERS.click && context.isOnListMarker,
},
{
id: 'list-continue-numbering',
label: TEXTS.listContinueNumbering,
icon: ICONS.listContinueNumbering,
isDefault: true,
action: (editor) => editor.commands.continueNumbering(),
showWhen: (context) => context.trigger === TRIGGERS.click && context.isOnListMarker,
},
{
id: 'list-decrease-indent',
label: TEXTS.listDecreaseIndent,
icon: ICONS.listDecreaseIndent,
isDefault: true,
action: (editor) => editor.commands.decreaseListIndent(),
showWhen: (context) => context.trigger === TRIGGERS.click && context.isOnListMarker,
},
{
id: 'list-increase-indent',
label: TEXTS.listIncreaseIndent,
icon: ICONS.listIncreaseIndent,
isDefault: true,
action: (editor) => editor.commands.increaseListIndent(),
showWhen: (context) => context.trigger === TRIGGERS.click && context.isOnListMarker,
},
],
},
{
id: 'general',
isDefault: true,
Expand Down
Loading
Loading