markdown-patch 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -107
- package/dist/cli.js +21 -0
- package/dist/constants.d.ts +0 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +0 -5
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +12 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +47 -29
- package/dist/patch.d.ts +12 -1
- package/dist/patch.d.ts.map +1 -1
- package/dist/patch.js +190 -38
- package/dist/tests/map.test.js +23 -0
- package/dist/tests/patch.test.js +271 -43
- package/dist/typeGuards.d.ts +7 -0
- package/dist/typeGuards.d.ts.map +1 -0
- package/dist/typeGuards.js +20 -0
- package/dist/types.d.ts +118 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -1
- package/package.json +21 -8
package/dist/patch.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { getDocumentMap } from "./map.js";
|
|
2
2
|
import * as marked from "marked";
|
|
3
|
-
import
|
|
3
|
+
import * as yaml from "yaml";
|
|
4
|
+
import { ContentType } from "./types.js";
|
|
5
|
+
import { isAppendableFrontmatterType, isDictionary, isList, isString, isStringArrayArray, } from "./typeGuards.js";
|
|
4
6
|
export var PatchFailureReason;
|
|
5
7
|
(function (PatchFailureReason) {
|
|
6
8
|
PatchFailureReason["InvalidTarget"] = "invalid-target";
|
|
7
9
|
PatchFailureReason["ContentAlreadyPreexistsInTarget"] = "content-already-preexists-in-target";
|
|
8
10
|
PatchFailureReason["TableContentIncorrectColumnCount"] = "table-content-incorrect-column-count";
|
|
9
|
-
PatchFailureReason["
|
|
11
|
+
PatchFailureReason["ContentTypeInvalid"] = "content-type-invalid";
|
|
12
|
+
PatchFailureReason["ContentTypeInvalidForTarget"] = "content-type-invalid-for-target";
|
|
13
|
+
PatchFailureReason["ContentNotMergeable"] = "content-not-mergeable";
|
|
10
14
|
})(PatchFailureReason || (PatchFailureReason = {}));
|
|
11
15
|
export class PatchFailed extends Error {
|
|
12
16
|
constructor(reason, instruction, targetMap) {
|
|
@@ -20,6 +24,8 @@ export class PatchFailed extends Error {
|
|
|
20
24
|
}
|
|
21
25
|
export class PatchError extends Error {
|
|
22
26
|
}
|
|
27
|
+
export class MergeNotPossible extends Error {
|
|
28
|
+
}
|
|
23
29
|
const replaceText = (document, instruction, target) => {
|
|
24
30
|
return [
|
|
25
31
|
document.slice(0, target.content.start),
|
|
@@ -66,11 +72,17 @@ const replaceTable = (document, instruction, target) => {
|
|
|
66
72
|
try {
|
|
67
73
|
const table = _getTableData(document, target);
|
|
68
74
|
const tableRows = [table.headerParts];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
if (isStringArrayArray(instruction.content)) {
|
|
76
|
+
for (const row of instruction.content) {
|
|
77
|
+
if (row.length !== table.token.header.length ||
|
|
78
|
+
typeof row === "string") {
|
|
79
|
+
throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
|
|
80
|
+
}
|
|
81
|
+
tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
74
86
|
}
|
|
75
87
|
return [
|
|
76
88
|
document.slice(0, target.content.start),
|
|
@@ -79,18 +91,24 @@ const replaceTable = (document, instruction, target) => {
|
|
|
79
91
|
].join("");
|
|
80
92
|
}
|
|
81
93
|
catch (TablePartsNotFound) {
|
|
82
|
-
throw new PatchFailed(PatchFailureReason.
|
|
94
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
83
95
|
}
|
|
84
96
|
};
|
|
85
97
|
const prependTable = (document, instruction, target) => {
|
|
86
98
|
try {
|
|
87
99
|
const table = _getTableData(document, target);
|
|
88
100
|
const tableRows = [table.headerParts];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
if (isStringArrayArray(instruction.content)) {
|
|
102
|
+
for (const row of instruction.content) {
|
|
103
|
+
if (row.length !== table.token.header.length ||
|
|
104
|
+
typeof row === "string") {
|
|
105
|
+
throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
|
|
106
|
+
}
|
|
107
|
+
tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
|
|
92
108
|
}
|
|
93
|
-
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
94
112
|
}
|
|
95
113
|
tableRows.push(table.contentParts);
|
|
96
114
|
return [
|
|
@@ -100,18 +118,24 @@ const prependTable = (document, instruction, target) => {
|
|
|
100
118
|
].join("");
|
|
101
119
|
}
|
|
102
120
|
catch (TablePartsNotFound) {
|
|
103
|
-
throw new PatchFailed(PatchFailureReason.
|
|
121
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
104
122
|
}
|
|
105
123
|
};
|
|
106
124
|
const appendTable = (document, instruction, target) => {
|
|
107
125
|
try {
|
|
108
126
|
const table = _getTableData(document, target);
|
|
109
127
|
const tableRows = [table.headerParts, table.contentParts];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
if (isStringArrayArray(instruction.content)) {
|
|
129
|
+
for (const row of instruction.content) {
|
|
130
|
+
if (row.length !== table.token.header.length ||
|
|
131
|
+
typeof row === "string") {
|
|
132
|
+
throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
|
|
133
|
+
}
|
|
134
|
+
tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
|
|
113
135
|
}
|
|
114
|
-
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
115
139
|
}
|
|
116
140
|
return [
|
|
117
141
|
document.slice(0, target.content.start),
|
|
@@ -120,7 +144,7 @@ const appendTable = (document, instruction, target) => {
|
|
|
120
144
|
].join("");
|
|
121
145
|
}
|
|
122
146
|
catch (TablePartsNotFound) {
|
|
123
|
-
throw new PatchFailed(PatchFailureReason.
|
|
147
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
124
148
|
}
|
|
125
149
|
};
|
|
126
150
|
const replace = (document, instruction, target) => {
|
|
@@ -130,7 +154,7 @@ const replace = (document, instruction, target) => {
|
|
|
130
154
|
switch (contentType) {
|
|
131
155
|
case ContentType.text:
|
|
132
156
|
return replaceText(document, instruction, target);
|
|
133
|
-
case ContentType.
|
|
157
|
+
case ContentType.json:
|
|
134
158
|
return replaceTable(document, instruction, target);
|
|
135
159
|
}
|
|
136
160
|
};
|
|
@@ -141,7 +165,7 @@ const prepend = (document, instruction, target) => {
|
|
|
141
165
|
switch (contentType) {
|
|
142
166
|
case ContentType.text:
|
|
143
167
|
return prependText(document, instruction, target);
|
|
144
|
-
case ContentType.
|
|
168
|
+
case ContentType.json:
|
|
145
169
|
return prependTable(document, instruction, target);
|
|
146
170
|
}
|
|
147
171
|
};
|
|
@@ -152,38 +176,166 @@ const append = (document, instruction, target) => {
|
|
|
152
176
|
switch (contentType) {
|
|
153
177
|
case ContentType.text:
|
|
154
178
|
return appendText(document, instruction, target);
|
|
155
|
-
case ContentType.
|
|
179
|
+
case ContentType.json:
|
|
156
180
|
return appendTable(document, instruction, target);
|
|
157
181
|
}
|
|
158
182
|
};
|
|
183
|
+
const addTargetHeading = (document, instruction, map) => {
|
|
184
|
+
const elements = [];
|
|
185
|
+
let bestTarget = map.heading[""];
|
|
186
|
+
for (const element of instruction.target ?? []) {
|
|
187
|
+
const possibleMatch = map.heading[[...elements, element].join("\u001f")];
|
|
188
|
+
if (possibleMatch) {
|
|
189
|
+
elements.push(element);
|
|
190
|
+
bestTarget = possibleMatch;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let finalContent = "";
|
|
197
|
+
let existingLevels = elements.length;
|
|
198
|
+
if (document.slice(bestTarget.content.end - map.lineEnding.length, bestTarget.content.end) !== map.lineEnding) {
|
|
199
|
+
finalContent += map.lineEnding;
|
|
200
|
+
}
|
|
201
|
+
for (const headingPart of (instruction.target ?? []).slice(existingLevels)) {
|
|
202
|
+
existingLevels += 1;
|
|
203
|
+
finalContent += `${"#".repeat(existingLevels)} ${headingPart}${map.lineEnding}`;
|
|
204
|
+
}
|
|
205
|
+
finalContent += instruction.content;
|
|
206
|
+
return [
|
|
207
|
+
document.slice(0, bestTarget.content.end),
|
|
208
|
+
finalContent,
|
|
209
|
+
document.slice(bestTarget.content.end),
|
|
210
|
+
].join("");
|
|
211
|
+
};
|
|
212
|
+
const addTargetBlock = (document, instruction, map) => {
|
|
213
|
+
return (document +
|
|
214
|
+
map.lineEnding +
|
|
215
|
+
instruction.content +
|
|
216
|
+
map.lineEnding +
|
|
217
|
+
map.lineEnding +
|
|
218
|
+
"^" +
|
|
219
|
+
instruction.target);
|
|
220
|
+
};
|
|
221
|
+
const addTarget = (document, instruction, map) => {
|
|
222
|
+
switch (instruction.targetType) {
|
|
223
|
+
case "heading":
|
|
224
|
+
return addTargetHeading(document, instruction, map);
|
|
225
|
+
case "block":
|
|
226
|
+
return addTargetBlock(document, instruction, map);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
159
229
|
const getTarget = (map, instruction) => {
|
|
160
230
|
switch (instruction.targetType) {
|
|
161
231
|
case "heading":
|
|
162
232
|
return map.heading[instruction.target ? instruction.target.join("\u001f") : ""];
|
|
163
233
|
case "block":
|
|
164
234
|
return map.block[instruction.target];
|
|
235
|
+
case "frontmatter":
|
|
236
|
+
return map.frontmatter[instruction.target];
|
|
165
237
|
}
|
|
166
238
|
};
|
|
239
|
+
function mergeFrontmatterValue(obj1, obj2) {
|
|
240
|
+
if (isList(obj1) && isList(obj2)) {
|
|
241
|
+
return [...obj1, ...obj2];
|
|
242
|
+
}
|
|
243
|
+
else if (isDictionary(obj1) && isDictionary(obj2)) {
|
|
244
|
+
return { ...obj1, ...obj2 };
|
|
245
|
+
}
|
|
246
|
+
else if (isString(obj1) && isString(obj2)) {
|
|
247
|
+
return obj1 + obj2;
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`Cannot merge objects of different types or unsupported types: ${typeof obj1} and ${typeof obj2}`);
|
|
250
|
+
}
|
|
251
|
+
function regenerateDocumentWithFrontmatter(frontmatter, document, map) {
|
|
252
|
+
const rawFrontmatterText = Object.values(frontmatter).some((value) => value !== undefined)
|
|
253
|
+
? `---\n${yaml.stringify(frontmatter).trimEnd()}\n---\n`
|
|
254
|
+
: "";
|
|
255
|
+
const frontmatterText = map.lineEnding !== "\n"
|
|
256
|
+
? rawFrontmatterText.replaceAll("\n", map.lineEnding)
|
|
257
|
+
: rawFrontmatterText;
|
|
258
|
+
const finalDocument = document.slice(map.contentOffset);
|
|
259
|
+
return frontmatterText + finalDocument;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Applies a patch to the specified document.
|
|
263
|
+
*
|
|
264
|
+
* @param document The document to apply the patch to.
|
|
265
|
+
* @param instruction The patch to apply.
|
|
266
|
+
* @returns The patched document
|
|
267
|
+
*/
|
|
167
268
|
export const applyPatch = (document, instruction) => {
|
|
168
269
|
const map = getDocumentMap(document);
|
|
169
270
|
const target = getTarget(map, instruction);
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
271
|
+
if (instruction.targetType === "block" ||
|
|
272
|
+
instruction.targetType === "heading") {
|
|
273
|
+
if (!target) {
|
|
274
|
+
if (instruction.createTargetIfMissing) {
|
|
275
|
+
return addTarget(document, instruction, map);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
throw new PatchFailed(PatchFailureReason.InvalidTarget, instruction, null);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if ((!("applyIfContentPreexists" in instruction) ||
|
|
282
|
+
!instruction.applyIfContentPreexists) &&
|
|
283
|
+
typeof instruction.content === "string" &&
|
|
284
|
+
document
|
|
285
|
+
.slice(target.content.start, target.content.end)
|
|
286
|
+
.includes(instruction.content.trim())) {
|
|
287
|
+
throw new PatchFailed(PatchFailureReason.ContentAlreadyPreexistsInTarget, instruction, target);
|
|
288
|
+
}
|
|
289
|
+
switch (instruction.operation) {
|
|
290
|
+
case "append":
|
|
291
|
+
return append(document, instruction, target);
|
|
292
|
+
case "prepend":
|
|
293
|
+
return prepend(document, instruction, target);
|
|
294
|
+
case "replace":
|
|
295
|
+
return replace(document, instruction, target);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const frontmatter = { ...map.frontmatter };
|
|
299
|
+
if (frontmatter[instruction.target] === undefined) {
|
|
300
|
+
if (instruction.createTargetIfMissing) {
|
|
301
|
+
if (isList(instruction.content)) {
|
|
302
|
+
frontmatter[instruction.target] = [];
|
|
303
|
+
}
|
|
304
|
+
else if (isString(instruction.content)) {
|
|
305
|
+
frontmatter[instruction.target] = "";
|
|
306
|
+
}
|
|
307
|
+
else if (isDictionary(instruction.content)) {
|
|
308
|
+
frontmatter[instruction.target] = {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
throw new PatchFailed(PatchFailureReason.InvalidTarget, instruction, null);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
switch (instruction.operation) {
|
|
317
|
+
case "append":
|
|
318
|
+
if (!isAppendableFrontmatterType(instruction.content)) {
|
|
319
|
+
throw new MergeNotPossible();
|
|
320
|
+
}
|
|
321
|
+
frontmatter[instruction.target] = mergeFrontmatterValue(frontmatter[instruction.target], instruction.content);
|
|
322
|
+
break;
|
|
323
|
+
case "prepend":
|
|
324
|
+
if (!isAppendableFrontmatterType(instruction.content)) {
|
|
325
|
+
throw new MergeNotPossible();
|
|
326
|
+
}
|
|
327
|
+
frontmatter[instruction.target] = mergeFrontmatterValue(instruction.content, frontmatter[instruction.target]);
|
|
328
|
+
break;
|
|
329
|
+
case "replace":
|
|
330
|
+
frontmatter[instruction.target] = instruction.content;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
return regenerateDocumentWithFrontmatter(frontmatter, document, map);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (error instanceof MergeNotPossible) {
|
|
337
|
+
throw new PatchFailed(PatchFailureReason.ContentNotMergeable, instruction, null);
|
|
338
|
+
}
|
|
339
|
+
throw error;
|
|
188
340
|
}
|
|
189
341
|
};
|
package/dist/tests/map.test.js
CHANGED
|
@@ -199,4 +199,27 @@ describe("map", () => {
|
|
|
199
199
|
//console.log(JSON.stringify(actualBlocks, undefined, 4));
|
|
200
200
|
expect(actualBlocks).toEqual(expectedBlocks);
|
|
201
201
|
});
|
|
202
|
+
describe("frontmatter", () => {
|
|
203
|
+
test("exists", () => {
|
|
204
|
+
const actualFrontmatter = getDocumentMap(sample).frontmatter;
|
|
205
|
+
const expectedFrontmatter = {
|
|
206
|
+
aliases: ["Structured Markdown Patch"],
|
|
207
|
+
"project-type": "Technical",
|
|
208
|
+
repository: "https://github.com/coddingtonbear/markdown-patch",
|
|
209
|
+
};
|
|
210
|
+
expect(expectedFrontmatter).toEqual(actualFrontmatter);
|
|
211
|
+
});
|
|
212
|
+
test("does not exist", () => {
|
|
213
|
+
const sample = fs.readFileSync(path.join(__dirname, "sample.frontmatter.none.md"), "utf-8");
|
|
214
|
+
const actualFrontmatter = getDocumentMap(sample).frontmatter;
|
|
215
|
+
const expectedFrontmatter = {};
|
|
216
|
+
expect(expectedFrontmatter).toEqual(actualFrontmatter);
|
|
217
|
+
});
|
|
218
|
+
test("does not exist, but starts with hr", () => {
|
|
219
|
+
const sample = fs.readFileSync(path.join(__dirname, "sample.frontmatter.nonfrontmatter-hr.md"), "utf-8");
|
|
220
|
+
const actualFrontmatter = getDocumentMap(sample).frontmatter;
|
|
221
|
+
const expectedFrontmatter = {};
|
|
222
|
+
expect(expectedFrontmatter).toEqual(actualFrontmatter);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
202
225
|
});
|