Skip to content

Commit 7f0cbc2

Browse files
authored
Add JsonObjectNode merge (#7)
1 parent 6f2b031 commit 7f0cbc2

File tree

3 files changed

+424
-0
lines changed

3 files changed

+424
-0
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,47 @@ node.update({
191191

192192
The `update` method reconciles the new content with the existing document, preserving comments, indentation, and spacing.
193193

194+
To merge two documents while preserving comments, use the `merge` method:
195+
196+
```ts
197+
198+
const destinationCode = `{
199+
// Destination pre-foo comment
200+
"foo": "value",
201+
// Destination post-foo comment
202+
"baz": [1, 2, 3]
203+
}
204+
`;
205+
206+
const sourceCode = `{
207+
/* Source pre-bar comment */
208+
"bar": 123, /* Inline comment */
209+
/* Source post-bar comment */
210+
"baz": true /* Another inline comment */
211+
}
212+
`;
213+
214+
const source = JsonParser.parse(sourceCode, JsonObjectNode);
215+
const destination = JsonParser.parse(destinationCode, JsonObjectNode);
216+
217+
console.log(JsonObjectNode.merge(source, destination).toString());
218+
```
219+
220+
Output:
221+
222+
```json5
223+
{
224+
// Destination pre-foo comment
225+
"foo": "value",
226+
/* Source pre-bar comment */
227+
"bar": 123, /* Inline comment */
228+
/* Source post-bar comment */
229+
"baz": true /* Another inline comment */
230+
}
231+
```
232+
233+
The `merge` method removes any existing destination properties that clash with source, along with their leading/trailing trivia, to avoid duplicate keys.
234+
194235
## Contributing
195236

196237
Contributions are welcome!

src/node/objectNode.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import {JsonPrimitiveNode, JsonStringNode} from './primitiveNode';
88
import {JsonValueFactory} from './factory';
99
import {JsonIdentifierNode} from './identifierNode';
1010
import {JsonError} from '../error';
11+
import {NodeMatcher} from '../manipulator';
12+
import {JsonTokenNode} from './tokenNode';
13+
import {JsonTokenType} from '../token';
14+
import INSIGNIFICANT = NodeMatcher.INSIGNIFICANT;
15+
import NEWLINE = NodeMatcher.NEWLINE;
16+
import SIGNIFICANT = NodeMatcher.SIGNIFICANT;
17+
import SPACE = NodeMatcher.SPACE;
1118

1219
export interface JsonObjectDefinition extends JsonCompositeDefinition {
1320
readonly properties: readonly JsonPropertyNode[];
@@ -33,6 +40,159 @@ export class JsonObjectNode extends JsonStructureNode implements JsonCompositeDe
3340
});
3441
}
3542

43+
public merge(source: JsonObjectNode): void {
44+
if (source.propertyNodes.length === 0) {
45+
return;
46+
}
47+
48+
if (this.propertyNodes.length === 0) {
49+
this.propertyNodes.push(...source.propertyNodes.map(property => property.clone()));
50+
this.children.splice(0, this.children.length, ...source.children.map(child => child.clone()));
51+
52+
return;
53+
}
54+
55+
for (const property of source.propertyNodes) {
56+
const key = property.key.toJSON();
57+
const sourceRange = source.findPropertyRange(key);
58+
59+
let sourceChildren: JsonNode[] = [property];
60+
61+
if (sourceRange !== null) {
62+
sourceChildren = source.children.slice(sourceRange[0], sourceRange[1] + 1);
63+
}
64+
65+
const newProperty = property.clone();
66+
67+
sourceChildren = sourceChildren.map(node => (node === property ? newProperty : node.clone()));
68+
69+
const range = this.findPropertyRange(key);
70+
71+
if (range === null) {
72+
this.propertyNodes.push(newProperty);
73+
this.insert(sourceChildren);
74+
75+
continue;
76+
}
77+
78+
const currentIndex = this.propertyNodes.findIndex(candidate => candidate.key.toJSON() === key);
79+
80+
this.propertyNodes.splice(currentIndex, 1, newProperty);
81+
this.children.splice(range[0], range[1] - range[0] + 1, ...sourceChildren);
82+
}
83+
}
84+
85+
private insert(nodes: JsonNode[]): void {
86+
let insertionIndex = this.children.length;
87+
88+
for (let index = this.children.length - 1; index >= 0; index--) {
89+
const child = this.children[index];
90+
91+
if (child instanceof JsonTokenNode) {
92+
if (child.isType(JsonTokenType.BRACE_RIGHT)) {
93+
insertionIndex = index;
94+
95+
continue;
96+
}
97+
98+
if (child.isType(JsonTokenType.COMMA)) {
99+
insertionIndex = index + 1;
100+
101+
break;
102+
}
103+
}
104+
105+
if (SIGNIFICANT(child)) {
106+
break;
107+
}
108+
109+
if (NEWLINE(child)) {
110+
while (index > 0 && SPACE(this.children[index - 1])) {
111+
index--;
112+
}
113+
114+
insertionIndex = index;
115+
116+
break;
117+
}
118+
119+
insertionIndex = index;
120+
}
121+
122+
let needsComma = false;
123+
124+
for (let index = insertionIndex - 1; index >= 0; index--) {
125+
const child = this.children[index];
126+
127+
if (child instanceof JsonTokenNode) {
128+
if (child.isType(JsonTokenType.COMMA)) {
129+
needsComma = false;
130+
131+
break;
132+
}
133+
}
134+
135+
if (SIGNIFICANT(child)) {
136+
needsComma = true;
137+
138+
break;
139+
}
140+
}
141+
142+
if (needsComma) {
143+
this.children.splice(insertionIndex, 0, new JsonTokenNode({
144+
type: JsonTokenType.COMMA,
145+
value: ',',
146+
}));
147+
148+
insertionIndex++;
149+
}
150+
151+
this.children.splice(insertionIndex, 0, ...nodes);
152+
}
153+
154+
private findPropertyRange(name: string): [number, number] | null {
155+
let startIndex = this.children.findIndex(
156+
node => node instanceof JsonPropertyNode && node.key.toJSON() === name,
157+
);
158+
159+
if (startIndex === -1) {
160+
return null;
161+
}
162+
163+
let endIndex = startIndex;
164+
165+
for (let lookBehind = startIndex - 1; lookBehind >= 0; lookBehind--) {
166+
const child = this.children[lookBehind];
167+
168+
if (!INSIGNIFICANT(child)) {
169+
break;
170+
}
171+
172+
if (NEWLINE(child)) {
173+
startIndex = lookBehind;
174+
}
175+
}
176+
177+
for (let lookAhead = endIndex + 1; lookAhead < this.children.length; lookAhead++) {
178+
const child = this.children[lookAhead];
179+
180+
if (!(child instanceof JsonTokenNode) || (SIGNIFICANT(child) && !child.isType(JsonTokenType.COMMA))) {
181+
break;
182+
}
183+
184+
if (NEWLINE(child)) {
185+
endIndex = lookAhead - 1;
186+
187+
break;
188+
}
189+
190+
endIndex = lookAhead;
191+
}
192+
193+
return [startIndex, endIndex];
194+
}
195+
36196
public update(other: JsonValueNode|JsonValue): JsonValueNode {
37197
if (!(other instanceof JsonValueNode)) {
38198
if (typeof other !== 'object' || other === null || Array.isArray(other)) {

0 commit comments

Comments
 (0)