Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function exportSchemaToJson(params) {
sequenceField: sdSequenceFieldTranslator,
documentStatField: sdDocumentStatFieldTranslator,
tableOfContents: sdTableOfContentsTranslator,
tableOfContentsInline: sdTableOfContentsTranslator,
index: sdIndexTranslator,
indexEntry: sdIndexEntryTranslator,
mathBlock: translatePassthroughNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const INLINE_FALLBACK_TYPES = new Set([
'sequenceField',
'indexEntry',
'tableOfContentsEntry',
'tableOfContentsInline',
]);

export function isInlineNode(node, schema) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,30 @@ const encode = (params) => {
const { nodes = [], nodeListHandler } = params || {};
const node = nodes[0];

const processedContent = nodeListHandler.handler({
let processedContent = nodeListHandler.handler({
...params,
nodes: node.elements || [],
});
const parentAcceptsBlocks = params?.extraParams?.parentAcceptsBlocks !== false;
const hasParagraphBlocks = (processedContent || []).some((child) => child?.type === 'paragraph');
if (parentAcceptsBlocks && !hasParagraphBlocks) {
processedContent = [
{
type: 'paragraph',
content: processedContent.filter((child) => Boolean(child && child.type)),
},
];
}
const inlineField = !parentAcceptsBlocks;
const attrs = {
instruction: node.attributes?.instruction || '',
};
if (!inlineField) {
attrs.rightAlignPageNumbers = deriveRightAlignPageNumbers(processedContent);
}
const processedNode = {
type: 'tableOfContents',
attrs: {
instruction: node.attributes?.instruction || '',
rightAlignPageNumbers: deriveRightAlignPageNumbers(processedContent),
},
type: inlineField ? 'tableOfContentsInline' : 'tableOfContents',
attrs,
content: processedContent,
};

Expand All @@ -56,8 +70,14 @@ const encode = (params) => {
*/
const decode = (params) => {
const { node } = params;
const isInlineNode = node.type === 'tableOfContentsInline';
const tocContent = Array.isArray(node.content) ? node.content : [];
const contentNodes = tocContent.map((n) => exportSchemaToJson({ ...params, node: n }));
const inlineContentNodes = tocContent.flatMap((n) => {
const exported = exportSchemaToJson({ ...params, node: n });
if (!exported) return [];
return Array.isArray(exported) ? exported : [exported];
});
Comment thread
chittolinag marked this conversation as resolved.
Outdated
const blockContentNodes = isInlineNode ? [] : tocContent.map((n) => exportSchemaToJson({ ...params, node: n }));
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.

two things:

  1. block TOCs decode every child twice — once for inlineContentNodes (thrown away) and once for blockContentNodes (kept). some decoders modify data as they go, so the second pass sees corrupted input. hyperlinks can get dropped. fix:
Suggested change
const blockContentNodes = isInlineNode ? [] : tocContent.map((n) => exportSchemaToJson({ ...params, node: n }));
const inlineContentNodes = isInlineNode
? tocContent.flatMap((n) => {
const exported = exportSchemaToJson({ ...params, node: n });
if (!exported) return [];
return Array.isArray(exported) ? exported : [exported];
})
: [];
const blockContentNodes = isInlineNode ? [] : tocContent.map((n) => exportSchemaToJson({ ...params, node: n }));
  1. loaded the original Template_Test_Report.docx with debug logs — tableOfContentsInline was never produced. the paragraph wrapping at lines 42-49 is what prevents the crash. do you have a file that actually triggers the inline path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catches. just fixed the 1st issue. I am now checking the 2nd issue you mentioned, it seems like there's also a problem when exporting the file (the opening in Word). I'll fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok I've been trying to fix the export issue for a while now but no luck. I am pretty sure the export issue is unrelated though - after removing the TOC & re-exporting the document, the error persists.

If you agree, I'll file a separate ticket to handle it, but in the mean time I'll make sure to fix the issues you mentioned here in the review.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done ✅


// Inject the fldChar begin, instrText and fldChar separate into the first child (after any existing pPr)
const tocBeginElements = [
Expand All @@ -77,9 +97,16 @@ const decode = (params) => {
},
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'separate' }, elements: [] }] },
];
const tocEndElements = [
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'end' }, elements: [] }] },
];

if (isInlineNode) {
return [...tocBeginElements, ...inlineContentNodes, ...tocEndElements];
}

if (contentNodes.length > 0) {
const firstParagraph = contentNodes[0];
if (blockContentNodes.length > 0) {
const firstParagraph = blockContentNodes[0];
let insertIndex = 0;
if (firstParagraph.elements) {
const pPrIndex = firstParagraph.elements.findIndex((el) => el.name === 'w:pPr');
Expand All @@ -91,24 +118,20 @@ const decode = (params) => {
firstParagraph.elements.splice(insertIndex, 0, ...tocBeginElements);
} else {
// If there are no paragraphs, create one with the TOC begin elements
contentNodes.push({
blockContentNodes.push({
name: 'w:p',
elements: tocBeginElements,
});
}

// Inject the fldChar end into the last child
const tocEndElements = [
{ name: 'w:r', elements: [{ name: 'w:fldChar', attributes: { 'w:fldCharType': 'end' }, elements: [] }] },
];
const lastParagraph = contentNodes[contentNodes.length - 1];
const lastParagraph = blockContentNodes[blockContentNodes.length - 1];
if (lastParagraph.elements) {
lastParagraph.elements.push(...tocEndElements);
} else {
lastParagraph.elements = [...tocEndElements];
}

return contentNodes;
return blockContentNodes;
};

/** @type {import('@translator').NodeTranslatorConfig} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,53 @@ describe('sd:tableOfContents translator', () => {
const result = config.encode(params);
expect(result.attrs.rightAlignPageNumbers).toBe(false);
});

it('encodes inline tableOfContents when block parents are not allowed', () => {
const mockNodeListHandler = {
handler: vi.fn(() => [{ type: 'text', text: 'Inline content' }]),
};
const params = {
nodes: [
{
name: 'sd:tableOfContents',
attributes: { instruction: 'TOC \\h' },
elements: [{ name: 'w:r', elements: [] }],
},
],
nodeListHandler: mockNodeListHandler,
extraParams: { parentAcceptsBlocks: false },
};

const result = config.encode(params);
expect(result).toEqual({
type: 'tableOfContentsInline',
attrs: { instruction: 'TOC \\h' },
content: [{ type: 'text', text: 'Inline content' }],
});
});

it('wraps inline children into a paragraph when parent accepts blocks', () => {
const mockNodeListHandler = {
handler: vi.fn(() => [{ type: 'text', text: 'Inline content' }]),
};
const params = {
nodes: [
{
name: 'sd:tableOfContents',
attributes: { instruction: 'TOC \\h' },
elements: [{ name: 'w:r', elements: [] }],
},
],
nodeListHandler: mockNodeListHandler,
};

const result = config.encode(params);
expect(result).toEqual({
type: 'tableOfContents',
attrs: { instruction: 'TOC \\h', rightAlignPageNumbers: true },
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Inline content' }] }],
});
});
});

describe('decode', () => {
Expand Down Expand Up @@ -172,5 +219,42 @@ describe('sd:tableOfContents translator', () => {
expect(result[0].name).toBe('w:p');
expect(result[0].elements).toEqual([...expectedBeginElements, ...expectedEndElements]);
});

it('should decode inline TOC nodes into run content', () => {
const inlineParams = {
node: {
type: 'tableOfContentsInline',
attrs: { instruction: 'TOC \\h' },
content: [{ type: 'text', text: 'Inline result' }],
},
};
vi.mocked(exportSchemaToJson).mockReturnValue({
name: 'w:r',
elements: [{ name: 'w:t', elements: [{ text: 'Inline result' }] }],
});

const result = config.decode(inlineParams);

const inlineBegin = [
{ ...expectedBeginElements[0] },
{
name: 'w:r',
elements: [
{
name: 'w:instrText',
attributes: { 'xml:space': 'preserve' },
elements: [{ text: 'TOC \\h', type: 'text', name: '#text', elements: [] }],
},
],
},
{ ...expectedBeginElements[2] },
];

expect(result).toEqual([
...inlineBegin,
{ name: 'w:r', elements: [{ name: 'w:t', elements: [{ text: 'Inline result' }] }] },
...expectedEndElements,
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export const handleParagraphNode = (params) => {
paragraphProperties: resolvedParagraphProperties,
inlineParagraphProperties,
numberingDefinedInline: Boolean(inlineParagraphProperties.numberingProperties),
parentAcceptsBlocks: false,
},
path: [...(params.path || []), node],
};
Expand Down
6 changes: 5 additions & 1 deletion packages/super-editor/src/editors/v1/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { ShapeContainer } from './shape-container/index.js';
import { ShapeTextbox } from './shape-textbox/index.js';
import { ContentBlock } from './content-block/index.js';
import { BlockNode } from './block-node/index.js';
import { TableOfContents, TocPageNumber } from './table-of-contents/index.js';
import { TableOfContents, TableOfContentsInline, TocPageNumber } from './table-of-contents/index.js';
import { DocumentIndex } from './document-index/index.js';
import { VectorShape } from './vector-shape/index.js';
import { ShapeGroup } from './shape-group/index.js';
Expand Down Expand Up @@ -108,6 +108,7 @@ const getRichTextExtensions = () => {
Link,
Paragraph,
TableOfContents,
TableOfContentsInline,
DocumentIndex,
Strike,
Text,
Expand Down Expand Up @@ -165,6 +166,7 @@ const getStarterExtensions = () => {
Strike,
TabNode,
TableOfContents,
TableOfContentsInline,
TocPageNumber,
DocumentIndex,
Text,
Expand Down Expand Up @@ -274,6 +276,8 @@ export {
TableCell,
TableHeader,
DocumentIndex,
TableOfContents,
TableOfContentsInline,
IndexEntry,
TableOfContentsEntry,
TocPageNumber,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,41 @@ export const TableOfContents = Node.create({
};
},
});

export const TableOfContentsInline = Node.create({
name: 'tableOfContentsInline',

group: 'inline',

inline: true,

content: 'inline*',

selectable: true,

addOptions() {
return {
htmlAttributes: {
'data-id': 'table-of-contents-inline',
'aria-label': 'Inline Table of Contents',
},
};
},

parseDOM() {
return [{ tag: 'span[data-id="table-of-contents-inline"]' }];
},

renderDOM({ htmlAttributes }) {
return ['span', Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes), 0];
},

addAttributes() {
return {
instruction: {
default: null,
rendered: false,
},
};
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,12 @@ export interface TableOfContentsAttrs extends BlockNodeAttributes {
sdBlockId?: string | null;
}

/** Inline table of contents node attributes */
export interface TableOfContentsInlineAttrs extends InlineNodeAttributes {
/** Field instruction */
instruction?: string | null;
}

// ============================================
// DOCUMENT INDEX
// ============================================
Expand Down Expand Up @@ -1217,6 +1223,7 @@ declare module '../../core/types/NodeAttributesMap.js' {
// Content blocks
contentBlock: ContentBlockAttrs;
tableOfContents: TableOfContentsAttrs;
tableOfContentsInline: TableOfContentsInlineAttrs;
index: DocumentIndexAttrs;
indexEntry: IndexEntryAttrs;

Expand Down
1 change: 1 addition & 0 deletions packages/super-editor/src/editors/v1/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export type {
ContentBlockSize,
ContentBlockMarginOffset,
TableOfContentsAttrs,
TableOfContentsInlineAttrs,
StructuredContentBlockAttrs,
DocumentPartObjectAttrs,
} from './extensions/types/node-attributes.js';
Expand Down
Loading