From 9ab5f76b96cd3d6f4ef8f0f9a2b3c640452afdc3 Mon Sep 17 00:00:00 2001 From: MarsLuay <70299537+MarsLuay@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:26:38 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20refactor=20parseStableLocation?= =?UTF-8?q?=20with=20declarative=20rule=20matcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors parseStableLocation in src/ai/docxStableIds.ts to use declarative PART_RULES and SUFFIX_RULES, decoupling part prefix matching from element suffix matching. This reduces over 200 lines of repetitive if blocks down to a clean, maintainable rule-driven loop while preserving exact location parsing behavior. Includes unit test suite in tests/docx-stable-ids.test.mjs. --- src/ai/docxStableIds.ts | 411 +++++++++------------------------ tests/docx-stable-ids.test.mjs | 271 ++++++++++++++++++++++ 2 files changed, 382 insertions(+), 300 deletions(-) create mode 100644 tests/docx-stable-ids.test.mjs diff --git a/src/ai/docxStableIds.ts b/src/ai/docxStableIds.ts index 017fb19..769ce6a 100644 --- a/src/ai/docxStableIds.ts +++ b/src/ai/docxStableIds.ts @@ -13,325 +13,136 @@ export interface DocxStableLocation { colIndex: number | null; } -const BODY_PARAGRAPH_ID = /^body\/p\[(\d+)\]$/; -const BODY_RUN_ID = /^body\/p\[(\d+)\]\/r\[(\d+)\]$/; -const BODY_TABLE_ID = /^body\/tbl\[(\d+)\]$/; -const BODY_CELL_ID = /^body\/tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/; - -const HEADER_PARAGRAPH_ID = /^header\/(\d+)\/p\[(\d+)\]$/; -const HEADER_RUN_ID = /^header\/(\d+)\/p\[(\d+)\]\/r\[(\d+)\]$/; -const HEADER_TABLE_ID = /^header\/(\d+)\/tbl\[(\d+)\]$/; -const HEADER_CELL_ID = /^header\/(\d+)\/tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/; - -const FOOTER_PARAGRAPH_ID = /^footer\/(\d+)\/p\[(\d+)\]$/; -const FOOTER_RUN_ID = /^footer\/(\d+)\/p\[(\d+)\]\/r\[(\d+)\]$/; -const FOOTER_TABLE_ID = /^footer\/(\d+)\/tbl\[(\d+)\]$/; -const FOOTER_CELL_ID = /^footer\/(\d+)\/tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/; - -const FOOTNOTE_PARAGRAPH_ID = /^footnotes\/fn\[(\d+)\]\/p\[(\d+)\]$/; -const FOOTNOTE_RUN_ID = /^footnotes\/fn\[(\d+)\]\/p\[(\d+)\]\/r\[(\d+)\]$/; -const FOOTNOTE_TABLE_ID = /^footnotes\/fn\[(\d+)\]\/tbl\[(\d+)\]$/; -const FOOTNOTE_CELL_ID = /^footnotes\/fn\[(\d+)\]\/tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/; - -const ENDNOTE_PARAGRAPH_ID = /^endnotes\/en\[(\d+)\]\/p\[(\d+)\]$/; -const ENDNOTE_RUN_ID = /^endnotes\/en\[(\d+)\]\/p\[(\d+)\]\/r\[(\d+)\]$/; -const ENDNOTE_TABLE_ID = /^endnotes\/en\[(\d+)\]\/tbl\[(\d+)\]$/; -const ENDNOTE_CELL_ID = /^endnotes\/en\[(\d+)\]\/tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/; - -export function docxIdPrefix(part: DocxPartKind, partNumber: number | null): string { - switch (part) { - case 'body': - return 'body'; - case 'header': - return `header/${partNumber ?? 1}`; - case 'footer': - return `footer/${partNumber ?? 1}`; - case 'footnotes': - return `footnotes/fn[${partNumber ?? 0}]`; - case 'endnotes': - return `endnotes/en[${partNumber ?? 0}]`; - } +interface PartRule { + pattern: RegExp; + part: DocxPartKind; + getPartNumber: (match: RegExpExecArray) => number | null; } -export function parseStableLocation(id: string): DocxStableLocation | null { - const bodyParagraph = BODY_PARAGRAPH_ID.exec(id); - if (bodyParagraph) { - return { - part: 'body', - partNumber: null, - kind: 'paragraph', - paragraphIndex: Number(bodyParagraph[1]), - runIndex: null, - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const bodyRun = BODY_RUN_ID.exec(id); - if (bodyRun) { - return { - part: 'body', - partNumber: null, - kind: 'run', - paragraphIndex: Number(bodyRun[1]), - runIndex: Number(bodyRun[2]), - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const bodyTable = BODY_TABLE_ID.exec(id); - if (bodyTable) { - return { - part: 'body', - partNumber: null, - kind: 'table', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(bodyTable[1]), - rowIndex: null, - colIndex: null, - }; - } - - const bodyCell = BODY_CELL_ID.exec(id); - if (bodyCell) { - return { - part: 'body', - partNumber: null, - kind: 'cell', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(bodyCell[1]), - rowIndex: Number(bodyCell[2]), - colIndex: Number(bodyCell[3]), - }; - } - - const headerParagraph = HEADER_PARAGRAPH_ID.exec(id); - if (headerParagraph) { - return { - part: 'header', - partNumber: Number(headerParagraph[1]), - kind: 'paragraph', - paragraphIndex: Number(headerParagraph[2]), - runIndex: null, - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const headerRun = HEADER_RUN_ID.exec(id); - if (headerRun) { - return { - part: 'header', - partNumber: Number(headerRun[1]), - kind: 'run', - paragraphIndex: Number(headerRun[2]), - runIndex: Number(headerRun[3]), - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const headerTable = HEADER_TABLE_ID.exec(id); - if (headerTable) { - return { - part: 'header', - partNumber: Number(headerTable[1]), - kind: 'table', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(headerTable[2]), - rowIndex: null, - colIndex: null, - }; - } - - const headerCell = HEADER_CELL_ID.exec(id); - if (headerCell) { - return { - part: 'header', - partNumber: Number(headerCell[1]), - kind: 'cell', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(headerCell[2]), - rowIndex: Number(headerCell[3]), - colIndex: Number(headerCell[4]), - }; - } - - const footerParagraph = FOOTER_PARAGRAPH_ID.exec(id); - if (footerParagraph) { - return { - part: 'footer', - partNumber: Number(footerParagraph[1]), - kind: 'paragraph', - paragraphIndex: Number(footerParagraph[2]), - runIndex: null, - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const footerRun = FOOTER_RUN_ID.exec(id); - if (footerRun) { - return { - part: 'footer', - partNumber: Number(footerRun[1]), - kind: 'run', - paragraphIndex: Number(footerRun[2]), - runIndex: Number(footerRun[3]), - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const footerTable = FOOTER_TABLE_ID.exec(id); - if (footerTable) { - return { - part: 'footer', - partNumber: Number(footerTable[1]), - kind: 'table', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(footerTable[2]), - rowIndex: null, - colIndex: null, - }; - } - - const footerCell = FOOTER_CELL_ID.exec(id); - if (footerCell) { - return { - part: 'footer', - partNumber: Number(footerCell[1]), - kind: 'cell', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(footerCell[2]), - rowIndex: Number(footerCell[3]), - colIndex: Number(footerCell[4]), - }; - } +interface SuffixRule { + pattern: RegExp; + kind: DocxStableBlockKind; + getIndices: (match: RegExpExecArray) => { + paragraphIndex: number; + runIndex: number | null; + tableIndex: number | null; + rowIndex: number | null; + colIndex: number | null; + }; +} - const footnoteParagraph = FOOTNOTE_PARAGRAPH_ID.exec(id); - if (footnoteParagraph) { - return { - part: 'footnotes', - partNumber: Number(footnoteParagraph[1]), - kind: 'paragraph', - paragraphIndex: Number(footnoteParagraph[2]), +const PART_RULES: PartRule[] = [ + { + pattern: /^body\/(.+)$/, + part: 'body', + getPartNumber: () => null, + }, + { + pattern: /^header\/(\d+)\/(.+)$/, + part: 'header', + getPartNumber: (m) => Number(m[1]), + }, + { + pattern: /^footer\/(\d+)\/(.+)$/, + part: 'footer', + getPartNumber: (m) => Number(m[1]), + }, + { + pattern: /^footnotes\/fn\[(\d+)\]\/(.+)$/, + part: 'footnotes', + getPartNumber: (m) => Number(m[1]), + }, + { + pattern: /^endnotes\/en\[(\d+)\]\/(.+)$/, + part: 'endnotes', + getPartNumber: (m) => Number(m[1]), + }, +]; + +const SUFFIX_RULES: SuffixRule[] = [ + { + pattern: /^p\[(\d+)\]$/, + kind: 'paragraph', + getIndices: (m) => ({ + paragraphIndex: Number(m[1]), runIndex: null, tableIndex: null, rowIndex: null, colIndex: null, - }; - } - - const footnoteRun = FOOTNOTE_RUN_ID.exec(id); - if (footnoteRun) { - return { - part: 'footnotes', - partNumber: Number(footnoteRun[1]), - kind: 'run', - paragraphIndex: Number(footnoteRun[2]), - runIndex: Number(footnoteRun[3]), + }), + }, + { + pattern: /^p\[(\d+)\]\/r\[(\d+)\]$/, + kind: 'run', + getIndices: (m) => ({ + paragraphIndex: Number(m[1]), + runIndex: Number(m[2]), tableIndex: null, rowIndex: null, colIndex: null, - }; - } - - const footnoteTable = FOOTNOTE_TABLE_ID.exec(id); - if (footnoteTable) { - return { - part: 'footnotes', - partNumber: Number(footnoteTable[1]), - kind: 'table', + }), + }, + { + pattern: /^tbl\[(\d+)\]$/, + kind: 'table', + getIndices: (m) => ({ paragraphIndex: 0, runIndex: null, - tableIndex: Number(footnoteTable[2]), + tableIndex: Number(m[1]), rowIndex: null, colIndex: null, - }; - } - - const footnoteCell = FOOTNOTE_CELL_ID.exec(id); - if (footnoteCell) { - return { - part: 'footnotes', - partNumber: Number(footnoteCell[1]), - kind: 'cell', + }), + }, + { + pattern: /^tbl\[(\d+)\]\/tr\[(\d+)\]\/tc\[(\d+)\]$/, + kind: 'cell', + getIndices: (m) => ({ paragraphIndex: 0, runIndex: null, - tableIndex: Number(footnoteCell[2]), - rowIndex: Number(footnoteCell[3]), - colIndex: Number(footnoteCell[4]), - }; - } + tableIndex: Number(m[1]), + rowIndex: Number(m[2]), + colIndex: Number(m[3]), + }), + }, +]; - const endnoteParagraph = ENDNOTE_PARAGRAPH_ID.exec(id); - if (endnoteParagraph) { - return { - part: 'endnotes', - partNumber: Number(endnoteParagraph[1]), - kind: 'paragraph', - paragraphIndex: Number(endnoteParagraph[2]), - runIndex: null, - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const endnoteRun = ENDNOTE_RUN_ID.exec(id); - if (endnoteRun) { - return { - part: 'endnotes', - partNumber: Number(endnoteRun[1]), - kind: 'run', - paragraphIndex: Number(endnoteRun[2]), - runIndex: Number(endnoteRun[3]), - tableIndex: null, - rowIndex: null, - colIndex: null, - }; - } - - const endnoteTable = ENDNOTE_TABLE_ID.exec(id); - if (endnoteTable) { - return { - part: 'endnotes', - partNumber: Number(endnoteTable[1]), - kind: 'table', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(endnoteTable[2]), - rowIndex: null, - colIndex: null, - }; +export function docxIdPrefix(part: DocxPartKind, partNumber: number | null): string { + switch (part) { + case 'body': + return 'body'; + case 'header': + return `header/${partNumber ?? 1}`; + case 'footer': + return `footer/${partNumber ?? 1}`; + case 'footnotes': + return `footnotes/fn[${partNumber ?? 0}]`; + case 'endnotes': + return `endnotes/en[${partNumber ?? 0}]`; } +} - const endnoteCell = ENDNOTE_CELL_ID.exec(id); - if (endnoteCell) { - return { - part: 'endnotes', - partNumber: Number(endnoteCell[1]), - kind: 'cell', - paragraphIndex: 0, - runIndex: null, - tableIndex: Number(endnoteCell[2]), - rowIndex: Number(endnoteCell[3]), - colIndex: Number(endnoteCell[4]), - }; +export function parseStableLocation(id: string): DocxStableLocation | null { + for (const partRule of PART_RULES) { + const partMatch = partRule.pattern.exec(id); + if (!partMatch) { + continue; + } + + const rest = partMatch[partMatch.length - 1] ?? ''; + for (const suffixRule of SUFFIX_RULES) { + const suffixMatch = suffixRule.pattern.exec(rest); + if (!suffixMatch) { + continue; + } + + return { + part: partRule.part, + partNumber: partRule.getPartNumber(partMatch), + kind: suffixRule.kind, + ...suffixRule.getIndices(suffixMatch), + }; + } + return null; } return null; diff --git a/tests/docx-stable-ids.test.mjs b/tests/docx-stable-ids.test.mjs new file mode 100644 index 0000000..43adc06 --- /dev/null +++ b/tests/docx-stable-ids.test.mjs @@ -0,0 +1,271 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { bundleSource } from './helpers/load-plugin-modules.mjs'; + +async function loadDocxStableIdsModule() { + const outfile = await bundleSource('src/ai/docxStableIds.ts', 'docx-stable-ids.cjs'); + return import(`file://${outfile}`); +} + +test('parseStableLocation parses body locations correctly', async () => { + const { parseStableLocation } = await loadDocxStableIdsModule(); + + assert.deepEqual(parseStableLocation('body/p[0]'), { + part: 'body', + partNumber: null, + kind: 'paragraph', + paragraphIndex: 0, + runIndex: null, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('body/p[12]/r[3]'), { + part: 'body', + partNumber: null, + kind: 'run', + paragraphIndex: 12, + runIndex: 3, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('body/tbl[2]'), { + part: 'body', + partNumber: null, + kind: 'table', + paragraphIndex: 0, + runIndex: null, + tableIndex: 2, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('body/tbl[1]/tr[4]/tc[5]'), { + part: 'body', + partNumber: null, + kind: 'cell', + paragraphIndex: 0, + runIndex: null, + tableIndex: 1, + rowIndex: 4, + colIndex: 5, + }); +}); + +test('parseStableLocation parses header locations correctly', async () => { + const { parseStableLocation } = await loadDocxStableIdsModule(); + + assert.deepEqual(parseStableLocation('header/1/p[2]'), { + part: 'header', + partNumber: 1, + kind: 'paragraph', + paragraphIndex: 2, + runIndex: null, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('header/2/p[0]/r[5]'), { + part: 'header', + partNumber: 2, + kind: 'run', + paragraphIndex: 0, + runIndex: 5, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('header/3/tbl[0]'), { + part: 'header', + partNumber: 3, + kind: 'table', + paragraphIndex: 0, + runIndex: null, + tableIndex: 0, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('header/1/tbl[2]/tr[3]/tc[4]'), { + part: 'header', + partNumber: 1, + kind: 'cell', + paragraphIndex: 0, + runIndex: null, + tableIndex: 2, + rowIndex: 3, + colIndex: 4, + }); +}); + +test('parseStableLocation parses footer locations correctly', async () => { + const { parseStableLocation } = await loadDocxStableIdsModule(); + + assert.deepEqual(parseStableLocation('footer/1/p[0]'), { + part: 'footer', + partNumber: 1, + kind: 'paragraph', + paragraphIndex: 0, + runIndex: null, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('footer/2/p[1]/r[2]'), { + part: 'footer', + partNumber: 2, + kind: 'run', + paragraphIndex: 1, + runIndex: 2, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('footer/1/tbl[0]'), { + part: 'footer', + partNumber: 1, + kind: 'table', + paragraphIndex: 0, + runIndex: null, + tableIndex: 0, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('footer/1/tbl[0]/tr[1]/tc[2]'), { + part: 'footer', + partNumber: 1, + kind: 'cell', + paragraphIndex: 0, + runIndex: null, + tableIndex: 0, + rowIndex: 1, + colIndex: 2, + }); +}); + +test('parseStableLocation parses footnote and endnote locations correctly', async () => { + const { parseStableLocation } = await loadDocxStableIdsModule(); + + assert.deepEqual(parseStableLocation('footnotes/fn[0]/p[1]'), { + part: 'footnotes', + partNumber: 0, + kind: 'paragraph', + paragraphIndex: 1, + runIndex: null, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('footnotes/fn[1]/p[2]/r[3]'), { + part: 'footnotes', + partNumber: 1, + kind: 'run', + paragraphIndex: 2, + runIndex: 3, + tableIndex: null, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('endnotes/en[0]/tbl[1]'), { + part: 'endnotes', + partNumber: 0, + kind: 'table', + paragraphIndex: 0, + runIndex: null, + tableIndex: 1, + rowIndex: null, + colIndex: null, + }); + + assert.deepEqual(parseStableLocation('endnotes/en[2]/tbl[0]/tr[1]/tc[2]'), { + part: 'endnotes', + partNumber: 2, + kind: 'cell', + paragraphIndex: 0, + runIndex: null, + tableIndex: 0, + rowIndex: 1, + colIndex: 2, + }); +}); + +test('parseStableLocation returns null for invalid IDs', async () => { + const { parseStableLocation } = await loadDocxStableIdsModule(); + + assert.equal(parseStableLocation(''), null); + assert.equal(parseStableLocation('body'), null); + assert.equal(parseStableLocation('body/'), null); + assert.equal(parseStableLocation('body/p'), null); + assert.equal(parseStableLocation('body/p[abc]'), null); + assert.equal(parseStableLocation('header/p[0]'), null); + assert.equal(parseStableLocation('footer/1'), null); + assert.equal(parseStableLocation('footnotes/p[0]'), null); + assert.equal(parseStableLocation('invalid/p[0]'), null); +}); + +test('location builders construct correct IDs', async () => { + const { + paragraphIdForLocation, + runIdForLocation, + tableIdForLocation, + cellIdForLocation, + } = await loadDocxStableIdsModule(); + + const locBodyPara = { + part: 'body', + partNumber: null, + kind: 'paragraph', + paragraphIndex: 5, + runIndex: null, + tableIndex: null, + rowIndex: null, + colIndex: null, + }; + assert.equal(paragraphIdForLocation(locBodyPara), 'body/p[5]'); + + const locHeaderRun = { + part: 'header', + partNumber: 2, + kind: 'run', + paragraphIndex: 1, + runIndex: 3, + tableIndex: null, + rowIndex: null, + colIndex: null, + }; + assert.equal(runIdForLocation(locHeaderRun), 'header/2/p[1]/r[3]'); + + const locFooterTable = { + part: 'footer', + partNumber: 1, + kind: 'table', + paragraphIndex: 0, + runIndex: null, + tableIndex: 4, + rowIndex: null, + colIndex: null, + }; + assert.equal(tableIdForLocation(locFooterTable), 'footer/1/tbl[4]'); + + const locEndnoteCell = { + part: 'endnotes', + partNumber: 0, + kind: 'cell', + paragraphIndex: 0, + runIndex: null, + tableIndex: 1, + rowIndex: 2, + colIndex: 3, + }; + assert.equal(cellIdForLocation(locEndnoteCell), 'endnotes/en[0]/tbl[1]/tr[2]/tc[3]'); +});