markdown-patch 0.1.2 → 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.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/debug.d.ts +3 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +12 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/map.d.ts +3 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +47 -29
- package/dist/patch.d.ts +30 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +205 -55
- package/dist/tests/map.test.d.ts +2 -0
- package/dist/tests/map.test.d.ts.map +1 -0
- package/dist/tests/map.test.js +23 -0
- package/dist/tests/patch.test.d.ts +2 -0
- package/dist/tests/patch.test.d.ts.map +1 -0
- package/dist/tests/patch.test.js +271 -42
- 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 +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -1
- package/package.json +33 -5
package/dist/patch.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { getDocumentMap } from "./map.js";
|
|
2
2
|
import * as marked from "marked";
|
|
3
|
+
import * as yaml from "yaml";
|
|
4
|
+
import { ContentType } from "./types.js";
|
|
5
|
+
import { isAppendableFrontmatterType, isDictionary, isList, isString, isStringArrayArray, } from "./typeGuards.js";
|
|
3
6
|
export var PatchFailureReason;
|
|
4
7
|
(function (PatchFailureReason) {
|
|
5
8
|
PatchFailureReason["InvalidTarget"] = "invalid-target";
|
|
6
9
|
PatchFailureReason["ContentAlreadyPreexistsInTarget"] = "content-already-preexists-in-target";
|
|
7
10
|
PatchFailureReason["TableContentIncorrectColumnCount"] = "table-content-incorrect-column-count";
|
|
8
|
-
PatchFailureReason["
|
|
11
|
+
PatchFailureReason["ContentTypeInvalid"] = "content-type-invalid";
|
|
12
|
+
PatchFailureReason["ContentTypeInvalidForTarget"] = "content-type-invalid-for-target";
|
|
13
|
+
PatchFailureReason["ContentNotMergeable"] = "content-not-mergeable";
|
|
9
14
|
})(PatchFailureReason || (PatchFailureReason = {}));
|
|
10
15
|
export class PatchFailed extends Error {
|
|
11
16
|
constructor(reason, instruction, targetMap) {
|
|
@@ -19,6 +24,8 @@ export class PatchFailed extends Error {
|
|
|
19
24
|
}
|
|
20
25
|
export class PatchError extends Error {
|
|
21
26
|
}
|
|
27
|
+
export class MergeNotPossible extends Error {
|
|
28
|
+
}
|
|
22
29
|
const replaceText = (document, instruction, target) => {
|
|
23
30
|
return [
|
|
24
31
|
document.slice(0, target.content.start),
|
|
@@ -65,11 +72,17 @@ const replaceTable = (document, instruction, target) => {
|
|
|
65
72
|
try {
|
|
66
73
|
const table = _getTableData(document, target);
|
|
67
74
|
const tableRows = [table.headerParts];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
71
82
|
}
|
|
72
|
-
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
73
86
|
}
|
|
74
87
|
return [
|
|
75
88
|
document.slice(0, target.content.start),
|
|
@@ -78,18 +91,24 @@ const replaceTable = (document, instruction, target) => {
|
|
|
78
91
|
].join("");
|
|
79
92
|
}
|
|
80
93
|
catch (TablePartsNotFound) {
|
|
81
|
-
throw new PatchFailed(PatchFailureReason.
|
|
94
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
82
95
|
}
|
|
83
96
|
};
|
|
84
97
|
const prependTable = (document, instruction, target) => {
|
|
85
98
|
try {
|
|
86
99
|
const table = _getTableData(document, target);
|
|
87
100
|
const tableRows = [table.headerParts];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
91
108
|
}
|
|
92
|
-
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
93
112
|
}
|
|
94
113
|
tableRows.push(table.contentParts);
|
|
95
114
|
return [
|
|
@@ -99,18 +118,24 @@ const prependTable = (document, instruction, target) => {
|
|
|
99
118
|
].join("");
|
|
100
119
|
}
|
|
101
120
|
catch (TablePartsNotFound) {
|
|
102
|
-
throw new PatchFailed(PatchFailureReason.
|
|
121
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
103
122
|
}
|
|
104
123
|
};
|
|
105
124
|
const appendTable = (document, instruction, target) => {
|
|
106
125
|
try {
|
|
107
126
|
const table = _getTableData(document, target);
|
|
108
127
|
const tableRows = [table.headerParts, table.contentParts];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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);
|
|
112
135
|
}
|
|
113
|
-
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalid, instruction, target);
|
|
114
139
|
}
|
|
115
140
|
return [
|
|
116
141
|
document.slice(0, target.content.start),
|
|
@@ -119,73 +144,198 @@ const appendTable = (document, instruction, target) => {
|
|
|
119
144
|
].join("");
|
|
120
145
|
}
|
|
121
146
|
catch (TablePartsNotFound) {
|
|
122
|
-
throw new PatchFailed(PatchFailureReason.
|
|
147
|
+
throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
|
|
123
148
|
}
|
|
124
149
|
};
|
|
125
150
|
const replace = (document, instruction, target) => {
|
|
126
|
-
const
|
|
127
|
-
instruction.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
case "text":
|
|
151
|
+
const contentType = "contentType" in instruction && instruction.contentType
|
|
152
|
+
? instruction.contentType
|
|
153
|
+
: ContentType.text;
|
|
154
|
+
switch (contentType) {
|
|
155
|
+
case ContentType.text:
|
|
132
156
|
return replaceText(document, instruction, target);
|
|
133
|
-
case
|
|
157
|
+
case ContentType.json:
|
|
134
158
|
return replaceTable(document, instruction, target);
|
|
135
159
|
}
|
|
136
160
|
};
|
|
137
161
|
const prepend = (document, instruction, target) => {
|
|
138
|
-
const
|
|
139
|
-
instruction.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
case "text":
|
|
162
|
+
const contentType = "contentType" in instruction && instruction.contentType
|
|
163
|
+
? instruction.contentType
|
|
164
|
+
: ContentType.text;
|
|
165
|
+
switch (contentType) {
|
|
166
|
+
case ContentType.text:
|
|
144
167
|
return prependText(document, instruction, target);
|
|
145
|
-
case
|
|
168
|
+
case ContentType.json:
|
|
146
169
|
return prependTable(document, instruction, target);
|
|
147
170
|
}
|
|
148
171
|
};
|
|
149
172
|
const append = (document, instruction, target) => {
|
|
150
|
-
const
|
|
151
|
-
instruction.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
case "text":
|
|
173
|
+
const contentType = "contentType" in instruction && instruction.contentType
|
|
174
|
+
? instruction.contentType
|
|
175
|
+
: ContentType.text;
|
|
176
|
+
switch (contentType) {
|
|
177
|
+
case ContentType.text:
|
|
156
178
|
return appendText(document, instruction, target);
|
|
157
|
-
case
|
|
179
|
+
case ContentType.json:
|
|
158
180
|
return appendTable(document, instruction, target);
|
|
159
181
|
}
|
|
160
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
|
+
};
|
|
161
229
|
const getTarget = (map, instruction) => {
|
|
162
230
|
switch (instruction.targetType) {
|
|
163
231
|
case "heading":
|
|
164
232
|
return map.heading[instruction.target ? instruction.target.join("\u001f") : ""];
|
|
165
233
|
case "block":
|
|
166
234
|
return map.block[instruction.target];
|
|
235
|
+
case "frontmatter":
|
|
236
|
+
return map.frontmatter[instruction.target];
|
|
167
237
|
}
|
|
168
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
|
+
*/
|
|
169
268
|
export const applyPatch = (document, instruction) => {
|
|
170
269
|
const map = getDocumentMap(document);
|
|
171
270
|
const target = getTarget(map, instruction);
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
190
340
|
}
|
|
191
341
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.test.d.ts","sourceRoot":"","sources":["../../src/tests/map.test.ts"],"names":[],"mappings":""}
|
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
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch.test.d.ts","sourceRoot":"","sources":["../../src/tests/patch.test.ts"],"names":[],"mappings":""}
|