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/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["RequestedBlockTypeBehaviorUnavailable"] = "requested-block-type-behavior-unavailable";
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
- for (const row of instruction.content) {
69
- if (row.length !== table.token.header.length || typeof row === "string") {
70
- throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
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
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
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
- for (const row of instruction.content) {
89
- if (row.length !== table.token.header.length || typeof row === "string") {
90
- throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
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
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
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
- for (const row of instruction.content) {
110
- if (row.length !== table.token.header.length || typeof row === "string") {
111
- throw new PatchFailed(PatchFailureReason.TableContentIncorrectColumnCount, instruction, target);
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
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
147
+ throw new PatchFailed(PatchFailureReason.ContentTypeInvalidForTarget, instruction, target);
123
148
  }
124
149
  };
125
150
  const replace = (document, instruction, target) => {
126
- const targetBlockTypeBehavior = "targetBlockTypeBehavior" in instruction &&
127
- instruction.targetBlockTypeBehavior
128
- ? instruction.targetBlockTypeBehavior
129
- : "text";
130
- switch (targetBlockTypeBehavior) {
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 "table":
157
+ case ContentType.json:
134
158
  return replaceTable(document, instruction, target);
135
159
  }
136
160
  };
137
161
  const prepend = (document, instruction, target) => {
138
- const targetBlockTypeBehavior = "targetBlockTypeBehavior" in instruction &&
139
- instruction.targetBlockTypeBehavior
140
- ? instruction.targetBlockTypeBehavior
141
- : "text";
142
- switch (targetBlockTypeBehavior) {
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 "table":
168
+ case ContentType.json:
146
169
  return prependTable(document, instruction, target);
147
170
  }
148
171
  };
149
172
  const append = (document, instruction, target) => {
150
- const targetBlockTypeBehavior = "targetBlockTypeBehavior" in instruction &&
151
- instruction.targetBlockTypeBehavior
152
- ? instruction.targetBlockTypeBehavior
153
- : "text";
154
- switch (targetBlockTypeBehavior) {
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 "table":
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 (!target) {
173
- throw new PatchFailed(PatchFailureReason.InvalidTarget, instruction, null);
174
- }
175
- if ((!("applyIfContentPreexists" in instruction) ||
176
- !instruction.applyIfContentPreexists) &&
177
- typeof instruction.content === "string" &&
178
- document
179
- .slice(target.content.start, target.content.end)
180
- .includes(instruction.content.trim())) {
181
- throw new PatchFailed(PatchFailureReason.ContentAlreadyPreexistsInTarget, instruction, target);
182
- }
183
- switch (instruction.operation) {
184
- case "append":
185
- return append(document, instruction, target);
186
- case "prepend":
187
- return prepend(document, instruction, target);
188
- case "replace":
189
- return replace(document, instruction, target);
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=map.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.test.d.ts","sourceRoot":"","sources":["../../src/tests/map.test.ts"],"names":[],"mappings":""}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=patch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch.test.d.ts","sourceRoot":"","sources":["../../src/tests/patch.test.ts"],"names":[],"mappings":""}