Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,44 @@ describe('getLastParagraphFont', () => {
const result = getLastParagraphFont(blocks);
expect(result).toBeUndefined();
});

it('skips empty first runs even when they carry stale font values', () => {
const blocks: FlowBlock[] = [
{
kind: 'paragraph',
id: '0-paragraph',
runs: [
{
kind: 'text',
text: 'Valid',
fontFamily: 'ValidFont',
fontSize: 11,
pmStart: 0,
pmEnd: 5,
},
],
attrs: {},
},
{
kind: 'paragraph',
id: '1-paragraph',
runs: [
{
kind: 'text',
text: '',
fontFamily: 'StaleFont',
fontSize: 42,
pmStart: 5,
pmEnd: 5,
},
],
attrs: {},
},
];

const result = getLastParagraphFont(blocks);
expect(result).toEqual({ fontFamily: 'ValidFont', fontSize: 11 });
});
});

describe('paragraph converters', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { applyTrackedChangesModeToRuns } from '../tracked-changes.js';
import { textNodeToRun } from './inline-converters/text-run.js';
import { DEFAULT_HYPERLINK_CONFIG, TOKEN_INLINE_TYPES } from '../constants.js';
import { computeRunAttrs, hasExplicitParagraphRunProperties } from '../attributes/paragraph.js';
import { syncListMarkerFontFromParagraphRuns } from '../list-marker-font.js';
import { resolveRunProperties } from '@superdoc/style-engine/ooxml';
import { footnoteReferenceToBlock } from './inline-converters/footnote-reference.js';
import { endnoteReferenceToBlock } from './inline-converters/endnote-reference.js';
Expand Down Expand Up @@ -604,6 +605,19 @@ export function paragraphToFlowBlocks({
const defaultSize =
usePreviousFont && previousParagraphFont.fontSize ? previousParagraphFont.fontSize : extracted.defaultSize;

const finalizeParagraphBlocks = (outputBlocks: FlowBlock[]): FlowBlock[] => {
outputBlocks.forEach((block) => {
if (block.kind === 'paragraph') {
syncListMarkerFontFromParagraphRuns({
block,
converterContext,
para,
});
}
});
return outputBlocks;
};

if (paragraphAttrs.pageBreakBefore) {
blocks.push({
kind: 'pageBreak',
Expand All @@ -615,7 +629,7 @@ export function paragraphToFlowBlocks({

if (!para.content || para.content.length === 0) {
if (paragraphProps.runProperties?.vanish) {
return blocks;
return finalizeParagraphBlocks(blocks);
}
const paragraphMarkTrackedChange = getParagraphMarkTrackedChange(paragraphProps, storyKey);
// Get the PM position of the empty paragraph for caret rendering
Expand Down Expand Up @@ -650,12 +664,12 @@ export function paragraphToFlowBlocks({
sourceAnchor,
});
if (!trackedChangesConfig) {
return blocks;
return finalizeParagraphBlocks(blocks);
}

const paragraphBlock = blocks[blocks.length - 1];
if (paragraphBlock?.kind !== 'paragraph') {
return blocks;
return finalizeParagraphBlocks(blocks);
}

const filteredRuns = applyTrackedChangesModeToRuns(
Expand All @@ -682,7 +696,7 @@ export function paragraphToFlowBlocks({

if (trackedChangesConfig.enabled && (filteredRuns.length === 0 || isGhostTrackedListArtifact)) {
blocks.pop();
return blocks;
return finalizeParagraphBlocks(blocks);
}

paragraphBlock.runs = filteredRuns;
Expand All @@ -691,7 +705,7 @@ export function paragraphToFlowBlocks({
trackedChangesMode: trackedChangesConfig.mode,
trackedChangesEnabled: trackedChangesConfig.enabled,
};
return blocks;
return finalizeParagraphBlocks(blocks);
}

let currentRuns: Run[] = [];
Expand Down Expand Up @@ -914,7 +928,7 @@ export function paragraphToFlowBlocks({
});

if (!trackedChangesConfig) {
return blocks;
return finalizeParagraphBlocks(blocks);
}

const processedBlocks: FlowBlock[] = [];
Expand Down Expand Up @@ -944,7 +958,7 @@ export function paragraphToFlowBlocks({
processedBlocks.push(block);
});

return processedBlocks;
return finalizeParagraphBlocks(processedBlocks);
}

type InlineConverterSpec = {
Expand Down Expand Up @@ -1063,7 +1077,10 @@ export function getLastParagraphFont(blocks: FlowBlock[]): ParagraphFont | undef
const para = block as ParagraphBlock;
const firstRun = para.runs?.[0];
if (!firstRun) continue;
const run = firstRun as { fontFamily?: string; fontSize?: number };
const run = firstRun as { text?: string; fontFamily?: string; fontSize?: number };
if (typeof run.text === 'string' && run.text.length === 0) {
continue;
}
const fontFamily = typeof run.fontFamily === 'string' ? run.fontFamily.trim() : '';
const fontSize = typeof run.fontSize === 'number' && Number.isFinite(run.fontSize) ? run.fontSize : NaN;
if (fontFamily.length > 0 && fontSize > 0) {
Expand Down Expand Up @@ -1133,15 +1150,26 @@ export function handleParagraphNode(node: PMNode, context: NodeHandlerContext):
// get() returns both the entry (if hit) and pre-computed nodeJson to avoid double serialization
const { entry: cached, nodeJson, nodeRev } = flowBlockCache.get(prefixedStableId, node);
if (cached) {
// Cache hit: reuse blocks with position adjustment
// Cache hit reuses previously-converted blocks as-is. That means we don't
// recompute previousParagraphFont (used for empty list items without
// explicit run properties). If the user changes the font on the prior
// paragraph (e.g. paragraph A), an empty list item (paragraph B) can keep
// the old font until the cache entry is invalidated. Narrow case, but
// avoids confusing incremental-edit behavior.
// Cache hit: reuse blocks with position adjustment, then re-sync marker font
// from live PM state. Empty list items have no textStyle marks, so pass
// previousParagraphFont instead of falling back to stale cached runs.
const delta = pmStart - cached.pmStart;
const reusedBlocks = shiftCachedBlocks(cached.blocks, delta);
const paragraphProps = node.attrs?.paragraphProperties as ParagraphProperties | undefined;
const previousParagraphFont = !hasExplicitParagraphRunProperties(paragraphProps)
? getLastParagraphFont(blocks)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
: undefined;
reusedBlocks.forEach((block) => {
if (block.kind === 'paragraph') {
syncListMarkerFontFromParagraphRuns({
block,
converterContext,
para: node,
contentFontSource: 'paragraph',
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
previousParagraphFont,
});
}
});
applyTrackedGhostListAdjustments(node, reusedBlocks, context);

reusedBlocks.forEach((block) => {
Expand Down
Loading
Loading