From 00af73605d190b49e12dfcaab0b584b28671e141 Mon Sep 17 00:00:00 2001 From: Damon Blais Date: Sat, 17 Oct 2026 09:50:53 -0700 Subject: [PATCH 1/4] fix: keep other table array entries' sub-tables when removing one entry --- CHANGELOG.md | 4 ++++ src/__tests__/patch.test.ts | 26 ++++++++++++++++++++++++++ src/patch.ts | 17 ++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e9793b..59a65834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Patching: removing one `[[table]]` entry no longer deletes the sub-tables of the other entries. Dropping the second `[[route]]` from a document whose entries each own a `[route.args]` section used to strip `[route.args]` from every remaining entry; only the removed entry's own sub-tables, those between its header and the next `[[route]]`, are removed now. + ## [3.3.0] - 2026-09-27 ### Added diff --git a/src/__tests__/patch.test.ts b/src/__tests__/patch.test.ts index 6a374f67..a8eb449d 100644 --- a/src/__tests__/patch.test.ts +++ b/src/__tests__/patch.test.ts @@ -2098,6 +2098,32 @@ test('should remove table array and preserve leading KV', () => { expect(patched).not.toContain('name'); }); +test('should remove one table array entry and keep the sub-tables of the others', () => { + const existing = dedent` + [[route]] + id = "a" + [route.args] + x = 1 + + [[route]] + id = "b" + [route.args] + y = 2 + + [[route]] + id = "c" + [route.args] + z = 3 + ` + '\n'; + + for (const removed of [0, 1, 2]) { + const updated = parse(existing) as { route: object[] }; + updated.route.splice(removed, 1); + + expect(parse(patch(existing, updated))).toEqual(updated); + } +}); + test('should remove leading KV and preserve table array', () => { const existing = dedent` title = "Project" diff --git a/src/patch.ts b/src/patch.ts index 2faf1dfb..3a461b87 100644 --- a/src/patch.ts +++ b/src/patch.ts @@ -3200,8 +3200,23 @@ function applyChanges( const key = (node as Table | TableArray).key.item.value; const siblings = (parent as { items: TreeNode[] }).items; const extending: TreeNode[] = []; - for (const sibling of siblings) { + // A sub-table belongs to the nearest [[x]] entry above it, so an + // entry owns only the siblings between it and the next [[x]]. + const aotEntry = isTableArray(node); + const ownStart = siblings.indexOf(node); + let ownEnd = siblings.length; + if (aotEntry) { + for (let i = ownStart + 1; i < siblings.length; i++) { + const s = siblings[i]; + if (isTableArray(s) && arraysEqual(s.key.item.value, key)) { + ownEnd = i; + break; + } + } + } + for (const [index, sibling] of siblings.entries()) { if (sibling === node) continue; + if (aotEntry && (index < ownStart || index >= ownEnd)) continue; const siblingKey = isKeyValue(sibling) ? sibling.key.value : isTable(sibling) || isTableArray(sibling) From 1df8616fde68897ee27b684fd08b20f7e543d2dc Mon Sep 17 00:00:00 2001 From: Damon Blais Date: Sat, 17 Oct 2026 10:01:49 -0700 Subject: [PATCH 2/4] fix: end an entry's sub-tables at the next entry of an ancestor array --- CHANGELOG.md | 2 +- src/__tests__/patch.test.ts | 21 +++++++++++++++++++++ src/patch.ts | 7 +++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a65834..b6dbd238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Patching: removing one `[[table]]` entry no longer deletes the sub-tables of the other entries. Dropping the second `[[route]]` from a document whose entries each own a `[route.args]` section used to strip `[route.args]` from every remaining entry; only the removed entry's own sub-tables, those between its header and the next `[[route]]`, are removed now. +- Patching: removing one `[[table]]` entry no longer deletes the sub-tables of the other entries. Dropping the second `[[route]]` from a document whose entries each own a `[route.args]` section used to strip `[route.args]` from every remaining entry; only the removed entry's own sub-tables are removed now: those between its header and the next entry of the same array or of an ancestor array, so removing a `[[group.route]]` entry also leaves the next `[[group]]`'s `[group.route.args]` alone. ## [3.3.0] - 2026-09-27 diff --git a/src/__tests__/patch.test.ts b/src/__tests__/patch.test.ts index a8eb449d..2b9593d8 100644 --- a/src/__tests__/patch.test.ts +++ b/src/__tests__/patch.test.ts @@ -2124,6 +2124,27 @@ test('should remove one table array entry and keep the sub-tables of the others' } }); +test('should remove a nested table array entry and keep the next parent entry\'s sub-tables', () => { + const existing = dedent` + [[group]] + id = "g0" + [[group.route]] + id = "a" + [[group.route]] + id = "b" + + [[group]] + id = "g1" + [group.route.args] + x = 1 + ` + '\n'; + + const updated = parse(existing) as { group: { route: object[] }[] }; + updated.group[0].route.splice(1, 1); + + expect(parse(patch(existing, updated))).toEqual(updated); +}); + test('should remove leading KV and preserve table array', () => { const existing = dedent` title = "Project" diff --git a/src/patch.ts b/src/patch.ts index 3a461b87..08ef6812 100644 --- a/src/patch.ts +++ b/src/patch.ts @@ -3201,14 +3201,17 @@ function applyChanges( const siblings = (parent as { items: TreeNode[] }).items; const extending: TreeNode[] = []; // A sub-table belongs to the nearest [[x]] entry above it, so an - // entry owns only the siblings between it and the next [[x]]. + // entry owns only the siblings up to the next entry of its own + // array or of an ancestor array ([[x]] ends an [[x.y]] entry too). const aotEntry = isTableArray(node); const ownStart = siblings.indexOf(node); let ownEnd = siblings.length; if (aotEntry) { for (let i = ownStart + 1; i < siblings.length; i++) { const s = siblings[i]; - if (isTableArray(s) && arraysEqual(s.key.item.value, key)) { + if (!isTableArray(s)) continue; + const sKey = s.key.item.value; + if (sKey.length <= key.length && arraysEqual(sKey, key.slice(0, sKey.length))) { ownEnd = i; break; } From 5e91a18263820d1dc51883791b2f52136030e847 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:46:38 -0400 Subject: [PATCH 3/4] test: assert full AOT removal output --- src/__tests__/patch.test.ts | 55 +++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/__tests__/patch.test.ts b/src/__tests__/patch.test.ts index 2b9593d8..ecd7a79f 100644 --- a/src/__tests__/patch.test.ts +++ b/src/__tests__/patch.test.ts @@ -2120,7 +2120,45 @@ test('should remove one table array entry and keep the sub-tables of the others' const updated = parse(existing) as { route: object[] }; updated.route.splice(removed, 1); - expect(parse(patch(existing, updated))).toEqual(updated); + const patched = patch(existing, updated); + const expected = [ + dedent` + [[route]] + id = "b" + [route.args] + y = 2 + + [[route]] + id = "c" + [route.args] + z = 3 + ` + '\n', + dedent` + [[route]] + id = "a" + [route.args] + x = 1 + + [[route]] + id = "c" + [route.args] + z = 3 + ` + '\n', + dedent` + [[route]] + id = "a" + [route.args] + x = 1 + + [[route]] + id = "b" + [route.args] + y = 2 + ` + '\n', + ]; + + expect(patched).toBe(expected[removed]); + expect(parse(patched)).toEqual(updated); } }); @@ -2141,8 +2179,21 @@ test('should remove a nested table array entry and keep the next parent entry\'s const updated = parse(existing) as { group: { route: object[] }[] }; updated.group[0].route.splice(1, 1); + const patched = patch(existing, updated); + const expected = dedent` + [[group]] + id = "g0" + [[group.route]] + id = "a" + + [[group]] + id = "g1" + [group.route.args] + x = 1 + ` + '\n'; - expect(parse(patch(existing, updated))).toEqual(updated); + expect(patched).toBe(expected); + expect(parse(patched)).toEqual(updated); }); test('should remove leading KV and preserve table array', () => { From bf10e5c430bd5bfff45d34010ad07ce04d947a7f Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:49:34 -0400 Subject: [PATCH 4/4] docs: simplify changelog entry and include ref to PR. --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6dbd238..f91db1e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Patching: removing one `[[table]]` entry no longer deletes the sub-tables of the other entries. Dropping the second `[[route]]` from a document whose entries each own a `[route.args]` section used to strip `[route.args]` from every remaining entry; only the removed entry's own sub-tables are removed now: those between its header and the next entry of the same array or of an ancestor array, so removing a `[[group.route]]` entry also leaves the next `[[group]]`'s `[group.route.args]` alone. +- Patching: removing one `[[table]]` entry no longer deletes the sub-tables of the other entries. ([#321]) ## [3.3.0] - 2026-09-27 @@ -505,5 +505,6 @@ This first forked version from [timhall/toml-patch](https://github.com/timhall/t [#313]: https://github.com/DecimalTurn/toml-patch/pull/313 [#314]: https://github.com/DecimalTurn/toml-patch/pull/314 [#316]: https://github.com/DecimalTurn/toml-patch/pull/316 +[#321]: https://github.com/DecimalTurn/toml-patch/pull/321 [0e66e68]: https://github.com/DecimalTurn/toml-patch/commit/0e66e68cbf42a07bc44445e46c3ea7bea97f95c1 [f97d571]: https://github.com/DecimalTurn/toml-patch/commit/f97d57194952b6603f0fcd686c32d3ae52a2c903