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/dist/patch.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { getDocumentMap } from "./map.js";
2
2
  import * as marked from "marked";
3
- import { ContentType } from "./constants.js";
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["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";
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
- for (const row of instruction.content) {
70
- if (row.length !== table.token.header.length || typeof row === "string") {
71
- 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);
72
82
  }
73
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
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
- for (const row of instruction.content) {
90
- if (row.length !== table.token.header.length || typeof row === "string") {
91
- 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);
92
108
  }
93
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
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
- for (const row of instruction.content) {
111
- if (row.length !== table.token.header.length || typeof row === "string") {
112
- 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);
113
135
  }
114
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
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.RequestedBlockTypeBehaviorUnavailable, instruction, target);
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.tableRows:
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.tableRows:
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.tableRows:
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 (!target) {
171
- throw new PatchFailed(PatchFailureReason.InvalidTarget, instruction, null);
172
- }
173
- if ((!("applyIfContentPreexists" in instruction) ||
174
- !instruction.applyIfContentPreexists) &&
175
- typeof instruction.content === "string" &&
176
- document
177
- .slice(target.content.start, target.content.end)
178
- .includes(instruction.content.trim())) {
179
- throw new PatchFailed(PatchFailureReason.ContentAlreadyPreexistsInTarget, instruction, target);
180
- }
181
- switch (instruction.operation) {
182
- case "append":
183
- return append(document, instruction, target);
184
- case "prepend":
185
- return prepend(document, instruction, target);
186
- case "replace":
187
- 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;
188
340
  }
189
341
  };
@@ -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
  });