Skip to content
Merged
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
36 changes: 24 additions & 12 deletions src/docxCommentLogging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ export interface DocxCommentsLogSummary {

const MAX_COMMENT_TEXT_CHARS = 500;

type ParagraphNode = NonNullable<DocxCommentLogSource['content']>[number];
type ParagraphChildNode = NonNullable<ParagraphNode['content']>[number];
type RunChildNode = NonNullable<ParagraphChildNode['content']>[number];

function extractTextFromRunChild(runChild: RunChildNode): string {
return runChild.type === 'text' && typeof runChild.text === 'string' ? runChild.text : '';
}

function extractTextFromParagraphChild(child: ParagraphChildNode): string {
if (child.type !== 'run' || !child.content) {
return '';
}
return child.content.map(extractTextFromRunChild).join('');
}

function extractTextFromParagraph(paragraph: ParagraphNode): string {
if (!paragraph.content) {
return '';
}
return paragraph.content.map(extractTextFromParagraphChild).join('');
}

export function extractDocxCommentPlainText(comment: DocxCommentLogSource): string {
if (typeof comment.text === 'string' && comment.text.length > 0) {
return truncateCommentText(comment.text);
Expand All @@ -49,18 +71,8 @@ export function extractDocxCommentPlainText(comment: DocxCommentLogSource): stri
return '';
}

const parts: string[] = [];
for (const paragraph of paragraphs) {
for (const child of paragraph.content ?? []) {
if (child.type !== 'run') continue;
for (const runChild of child.content ?? []) {
if (runChild.type === 'text' && typeof runChild.text === 'string') {
parts.push(runChild.text);
}
}
}
}
return truncateCommentText(parts.join(''));
const rawText = paragraphs.map(extractTextFromParagraph).join('');
return truncateCommentText(rawText);
}

export function truncateCommentText(text: string, maxChars = MAX_COMMENT_TEXT_CHARS): string {
Expand Down
50 changes: 50 additions & 0 deletions tests/docx-comment-logging.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,53 @@ test("summarizeDocxComments includes replies with parentId", async () => {
assert.equal(summary.comments[1].parentId, 1);
assert.equal(summary.comments[1].text, "add more polish");
});

test("extractDocxCommentPlainText handles explicit text, nested runs, edge cases, and truncation", async () => {
const { extractDocxCommentPlainText } = await loadCommentLoggingModule();

// Explicit text takes precedence
assert.equal(
extractDocxCommentPlainText({ id: 10, text: "Explicit comment text" }),
"Explicit comment text"
);

// Empty comment / no content
assert.equal(extractDocxCommentPlainText({ id: 11 }), "");
assert.equal(extractDocxCommentPlainText({ id: 12, content: [] }), "");
assert.equal(extractDocxCommentPlainText({ id: 13, content: [{ content: [] }] }), "");

// Non-run and non-text child nodes ignored gracefully
const mixedContent = [
{
content: [
{ type: "image", content: [] },
{
type: "run",
content: [
{ type: "bold_flag", text: undefined },
{ type: "text", text: "Hello " },
{ type: "text", text: "world!" },
],
},
],
},
{
content: [
{
type: "run",
content: [{ type: "text", text: " Next line." }],
},
],
},
];
assert.equal(
extractDocxCommentPlainText({ id: 14, content: mixedContent }),
"Hello world! Next line."
);

// Text truncation over 500 chars
const longStr = "A".repeat(600);
const truncated = extractDocxCommentPlainText({ id: 15, text: longStr });
assert.equal(truncated.length, 501); // 500 chars + '…'
assert.ok(truncated.endsWith("…"));
});