markdown-patch 0.1.1 → 0.1.2

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.
Files changed (39) hide show
  1. package/README.md +111 -0
  2. package/dist/cli.js +108 -0
  3. package/{src/constants.ts → dist/constants.js} +7 -8
  4. package/dist/debug.js +50 -0
  5. package/dist/index.js +1 -0
  6. package/dist/map.js +144 -0
  7. package/dist/patch.js +191 -0
  8. package/dist/tests/map.test.js +202 -0
  9. package/dist/tests/patch.test.js +222 -0
  10. package/dist/types.js +1 -0
  11. package/package.json +7 -2
  12. package/.tool-versions +0 -1
  13. package/.vscode/launch.json +0 -21
  14. package/document.md +0 -11
  15. package/document.mdpatch.json +0 -8
  16. package/jest.config.ts +0 -9
  17. package/src/cli.ts +0 -88
  18. package/src/debug.ts +0 -75
  19. package/src/index.ts +0 -9
  20. package/src/map.ts +0 -200
  21. package/src/patch.ts +0 -326
  22. package/src/tests/map.test.ts +0 -212
  23. package/src/tests/patch.test.ts +0 -297
  24. package/src/tests/sample.md +0 -81
  25. package/src/tests/sample.patch.block.append.md +0 -82
  26. package/src/tests/sample.patch.block.prepend.md +0 -82
  27. package/src/tests/sample.patch.block.replace.md +0 -81
  28. package/src/tests/sample.patch.block.targetBlockTypeBehavior.table.append.md +0 -82
  29. package/src/tests/sample.patch.block.targetBlockTypeBehavior.table.prepend.md +0 -82
  30. package/src/tests/sample.patch.block.targetBlockTypeBehavior.table.replace.md +0 -77
  31. package/src/tests/sample.patch.heading.append.md +0 -82
  32. package/src/tests/sample.patch.heading.document.append.md +0 -82
  33. package/src/tests/sample.patch.heading.document.prepend.md +0 -82
  34. package/src/tests/sample.patch.heading.prepend.md +0 -82
  35. package/src/tests/sample.patch.heading.replace.md +0 -81
  36. package/src/tests/sample.patch.heading.trimTargetWhitespace.append.md +0 -80
  37. package/src/tests/sample.patch.heading.trimTargetWhitespace.prepend.md +0 -80
  38. package/src/types.ts +0 -155
  39. package/tsconfig.json +0 -18
package/src/map.ts DELETED
@@ -1,200 +0,0 @@
1
- import * as marked from "marked";
2
-
3
- import {
4
- DocumentMap,
5
- DocumentMapMarkerContentPair,
6
- HeadingMarkerContentPair,
7
- } from "./types.js";
8
-
9
- import {
10
- CAN_INCLUDE_BLOCK_REFERENCE,
11
- TARGETABLE_BY_ISOLATED_BLOCK_REFERENCE,
12
- } from "./constants.js";
13
-
14
- function getHeadingPositions(
15
- document: string,
16
- tokens: marked.TokensList
17
- ): Record<string, HeadingMarkerContentPair> {
18
- // If the document starts with frontmatter, figure out where
19
- // the frontmatter ends so we can know where the text of the
20
- // document begins
21
- let documentStart = 0;
22
- if (tokens[0].type === "hr") {
23
- documentStart = tokens[0].raw.length + 1;
24
- for (const token of tokens.slice(1)) {
25
- documentStart += token.raw.length;
26
- if (token.type === "hr") {
27
- break;
28
- }
29
- }
30
- }
31
-
32
- const positions: Record<string, HeadingMarkerContentPair> = {
33
- "": {
34
- content: {
35
- start: documentStart,
36
- end: document.length,
37
- },
38
- marker: {
39
- start: 0,
40
- end: 0,
41
- },
42
- level: 0,
43
- },
44
- };
45
- const stack: Array<{ heading: string; position: HeadingMarkerContentPair }> =
46
- [];
47
-
48
- let currentPosition = 0;
49
-
50
- tokens.forEach((token, index) => {
51
- if (token.type === "heading") {
52
- const headingToken = token as marked.Tokens.Heading;
53
-
54
- const startHeading = document.indexOf(
55
- headingToken.raw.trim(),
56
- currentPosition
57
- );
58
- const endHeading = startHeading + headingToken.raw.trim().length + 1;
59
- const headingLevel = headingToken.depth;
60
-
61
- // Determine the start of the content after this heading
62
- const startContent = endHeading;
63
-
64
- // Determine the end of the content before the next heading of the same or higher level, or end of document
65
- let endContent: number | undefined = undefined;
66
- for (let i = index + 1; i < tokens.length; i++) {
67
- if (
68
- tokens[i].type === "heading" &&
69
- (tokens[i] as marked.Tokens.Heading).depth <= headingLevel
70
- ) {
71
- endContent = document.indexOf(tokens[i].raw.trim(), startContent);
72
- break;
73
- }
74
- }
75
- if (endContent === undefined) {
76
- endContent = document.length;
77
- }
78
-
79
- const currentHeading: HeadingMarkerContentPair = {
80
- content: {
81
- start: startContent,
82
- end: endContent,
83
- },
84
- marker: {
85
- start: startHeading,
86
- end: endHeading,
87
- },
88
- level: headingLevel,
89
- };
90
-
91
- // Build the full heading path with parent headings separated by '\t'
92
- let fullHeadingPath = headingToken.text.trim();
93
- while (
94
- stack.length &&
95
- stack[stack.length - 1].position.level >= headingLevel
96
- ) {
97
- stack.pop();
98
- }
99
-
100
- if (stack.length) {
101
- const parent = stack[stack.length - 1];
102
- parent.position.content.end = endContent;
103
- fullHeadingPath = `${parent.heading}\u001f${fullHeadingPath}`;
104
- }
105
-
106
- positions[fullHeadingPath] = currentHeading;
107
- stack.push({ heading: fullHeadingPath, position: currentHeading });
108
-
109
- currentPosition = endHeading;
110
- }
111
- });
112
-
113
- return positions;
114
- }
115
-
116
- function getBlockPositions(
117
- document: string,
118
- tokens: marked.TokensList
119
- ): Record<string, DocumentMapMarkerContentPair> {
120
- const positions: Record<string, DocumentMapMarkerContentPair> = {};
121
-
122
- let lastBlockDetails:
123
- | {
124
- token: marked.Token;
125
- start: number;
126
- end: number;
127
- }
128
- | undefined = undefined;
129
- let startContent = 0;
130
- let endContent = 0;
131
- let endMarker = 0;
132
- marked.walkTokens(tokens, (token) => {
133
- const blockReferenceRegex = /(?:\s+|^)\^([a-zA-Z0-9_-]+)\s*$/;
134
- startContent = document.indexOf(token.raw, startContent);
135
- const match = blockReferenceRegex.exec(token.raw);
136
- endContent = startContent + (match ? match.index : token.raw.length);
137
- const startMarker = match ? startContent + match.index : -1;
138
- endMarker = startContent + token.raw.length;
139
- // The end of a list item token sometimes doesn't include the trailing
140
- // newline -- i'm honestly not sure why, but treating it as
141
- // included here would simplify my implementation
142
- if (
143
- document.slice(endMarker - 1, endMarker) !== "\n" &&
144
- document.slice(endMarker, endMarker + 1) === "\n"
145
- ) {
146
- endMarker += 1;
147
- } else if (
148
- document.slice(endMarker - 2, endMarker) !== "\r\n" &&
149
- document.slice(endMarker, endMarker + 2) === "\r\n"
150
- ) {
151
- endMarker += 2;
152
- }
153
- if (CAN_INCLUDE_BLOCK_REFERENCE.includes(token.type) && match) {
154
- const name = match[1];
155
- if (!name || match.index === undefined) {
156
- return;
157
- }
158
-
159
- const finalStartContent = {
160
- start: startContent,
161
- end: endContent,
162
- };
163
- if (
164
- finalStartContent.start === finalStartContent.end &&
165
- lastBlockDetails
166
- ) {
167
- finalStartContent.start = lastBlockDetails.start;
168
- finalStartContent.end = lastBlockDetails.end;
169
- }
170
-
171
- positions[name] = {
172
- content: finalStartContent,
173
- marker: {
174
- start: startMarker,
175
- end: endMarker,
176
- },
177
- };
178
- }
179
-
180
- if (TARGETABLE_BY_ISOLATED_BLOCK_REFERENCE.includes(token.type)) {
181
- lastBlockDetails = {
182
- token: token,
183
- start: startContent,
184
- end: endContent - 1,
185
- };
186
- }
187
- });
188
-
189
- return positions;
190
- }
191
-
192
- export const getDocumentMap = (document: string): DocumentMap => {
193
- const lexer = new marked.Lexer();
194
- const tokens = lexer.lex(document);
195
-
196
- return {
197
- heading: getHeadingPositions(document, tokens),
198
- block: getBlockPositions(document, tokens),
199
- };
200
- };
package/src/patch.ts DELETED
@@ -1,326 +0,0 @@
1
- import { getDocumentMap } from "./map.js";
2
- import * as marked from "marked";
3
- import {
4
- AppendTableRowsBlockPatchInstruction,
5
- PrependTableRowsBlockPatchInstruction,
6
- DocumentMap,
7
- DocumentMapMarkerContentPair,
8
- ExtendingPatchInstruction,
9
- PatchInstruction,
10
- ReplaceTableRowsBlockPatchInstruction,
11
- } from "./types.js";
12
-
13
- export enum PatchFailureReason {
14
- InvalidTarget = "invalid-target",
15
- ContentAlreadyPreexistsInTarget = "content-already-preexists-in-target",
16
- TableContentIncorrectColumnCount = "table-content-incorrect-column-count",
17
- RequestedBlockTypeBehaviorUnavailable = "requested-block-type-behavior-unavailable",
18
- }
19
-
20
- export class PatchFailed extends Error {
21
- public reason: PatchFailureReason;
22
- public instruction: PatchInstruction;
23
- public targetMap: DocumentMapMarkerContentPair | null;
24
-
25
- constructor(
26
- reason: PatchFailureReason,
27
- instruction: PatchInstruction,
28
- targetMap: DocumentMapMarkerContentPair | null
29
- ) {
30
- super();
31
- this.reason = reason;
32
- this.instruction = instruction;
33
- this.targetMap = targetMap;
34
- this.name = "PatchFailed";
35
-
36
- Object.setPrototypeOf(this, new.target.prototype);
37
- }
38
- }
39
-
40
- export class PatchError extends Error {}
41
-
42
- const replaceText = (
43
- document: string,
44
- instruction: PatchInstruction,
45
- target: DocumentMapMarkerContentPair
46
- ): string => {
47
- return [
48
- document.slice(0, target.content.start),
49
- instruction.content,
50
- document.slice(target.content.end),
51
- ].join("");
52
- };
53
-
54
- const prependText = (
55
- document: string,
56
- instruction: ExtendingPatchInstruction & PatchInstruction,
57
- target: DocumentMapMarkerContentPair
58
- ): string => {
59
- return [
60
- document.slice(0, target.content.start),
61
- instruction.content,
62
- instruction.trimTargetWhitespace
63
- ? document.slice(target.content.start).trimStart()
64
- : document.slice(target.content.start),
65
- ].join("");
66
- };
67
-
68
- const appendText = (
69
- document: string,
70
- instruction: ExtendingPatchInstruction & PatchInstruction,
71
- target: DocumentMapMarkerContentPair
72
- ): string => {
73
- return [
74
- instruction.trimTargetWhitespace
75
- ? document.slice(0, target.content.end).trimEnd()
76
- : document.slice(0, target.content.end),
77
- instruction.content,
78
- document.slice(target.content.end),
79
- ].join("");
80
- };
81
-
82
- export class TablePartsNotFound extends Error {}
83
-
84
- const _getTableData = (
85
- document: string,
86
- target: DocumentMapMarkerContentPair
87
- ): {
88
- token: marked.Tokens.Table;
89
- lineEnding: string;
90
- headerParts: string;
91
- contentParts: string;
92
- } => {
93
- const targetTable = document.slice(target.content.start, target.content.end);
94
- const tableToken = marked.lexer(targetTable)[0];
95
- const match = /^(.*?)(?:\r?\n)(.*?)(\r?\n)/.exec(targetTable);
96
- if (!(tableToken.type === "table") || !match) {
97
- throw new TablePartsNotFound();
98
- }
99
-
100
- const lineEnding = match[3];
101
- return {
102
- token: tableToken as marked.Tokens.Table,
103
- lineEnding: match[3],
104
- headerParts: match[1] + lineEnding + match[2] + lineEnding,
105
- contentParts: targetTable.slice(match[0].length),
106
- };
107
- };
108
-
109
- const replaceTable = (
110
- document: string,
111
- instruction: ReplaceTableRowsBlockPatchInstruction,
112
- target: DocumentMapMarkerContentPair
113
- ): string => {
114
- try {
115
- const table = _getTableData(document, target);
116
- const tableRows: string[] = [table.headerParts];
117
- for (const row of instruction.content) {
118
- if (row.length !== table.token.header.length || typeof row === "string") {
119
- throw new PatchFailed(
120
- PatchFailureReason.TableContentIncorrectColumnCount,
121
- instruction,
122
- target
123
- );
124
- }
125
-
126
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
127
- }
128
-
129
- return [
130
- document.slice(0, target.content.start),
131
- tableRows.join(""),
132
- document.slice(target.content.end),
133
- ].join("");
134
- } catch (TablePartsNotFound) {
135
- throw new PatchFailed(
136
- PatchFailureReason.RequestedBlockTypeBehaviorUnavailable,
137
- instruction,
138
- target
139
- );
140
- }
141
- };
142
-
143
- const prependTable = (
144
- document: string,
145
- instruction: PrependTableRowsBlockPatchInstruction,
146
- target: DocumentMapMarkerContentPair
147
- ): string => {
148
- try {
149
- const table = _getTableData(document, target);
150
- const tableRows: string[] = [table.headerParts];
151
- for (const row of instruction.content) {
152
- if (row.length !== table.token.header.length || typeof row === "string") {
153
- throw new PatchFailed(
154
- PatchFailureReason.TableContentIncorrectColumnCount,
155
- instruction,
156
- target
157
- );
158
- }
159
-
160
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
161
- }
162
-
163
- tableRows.push(table.contentParts);
164
-
165
- return [
166
- document.slice(0, target.content.start),
167
- tableRows.join(""),
168
- document.slice(target.content.end),
169
- ].join("");
170
- } catch (TablePartsNotFound) {
171
- throw new PatchFailed(
172
- PatchFailureReason.RequestedBlockTypeBehaviorUnavailable,
173
- instruction,
174
- target
175
- );
176
- }
177
- };
178
-
179
- const appendTable = (
180
- document: string,
181
- instruction: AppendTableRowsBlockPatchInstruction,
182
- target: DocumentMapMarkerContentPair
183
- ): string => {
184
- try {
185
- const table = _getTableData(document, target);
186
- const tableRows: string[] = [table.headerParts, table.contentParts];
187
- for (const row of instruction.content) {
188
- if (row.length !== table.token.header.length || typeof row === "string") {
189
- throw new PatchFailed(
190
- PatchFailureReason.TableContentIncorrectColumnCount,
191
- instruction,
192
- target
193
- );
194
- }
195
-
196
- tableRows.push("| " + row.join(" | ") + " |" + table.lineEnding);
197
- }
198
-
199
- return [
200
- document.slice(0, target.content.start),
201
- tableRows.join(""),
202
- document.slice(target.content.end),
203
- ].join("");
204
- } catch (TablePartsNotFound) {
205
- throw new PatchFailed(
206
- PatchFailureReason.RequestedBlockTypeBehaviorUnavailable,
207
- instruction,
208
- target
209
- );
210
- }
211
- };
212
-
213
- const replace = (
214
- document: string,
215
- instruction: PatchInstruction,
216
- target: DocumentMapMarkerContentPair
217
- ): string => {
218
- const targetBlockTypeBehavior =
219
- "targetBlockTypeBehavior" in instruction
220
- ? instruction.targetBlockTypeBehavior
221
- : "text";
222
-
223
- switch (targetBlockTypeBehavior) {
224
- case "text":
225
- return replaceText(document, instruction, target);
226
- case "table":
227
- return replaceTable(
228
- document,
229
- instruction as ReplaceTableRowsBlockPatchInstruction,
230
- target
231
- );
232
- }
233
- };
234
-
235
- const prepend = (
236
- document: string,
237
- instruction: ExtendingPatchInstruction & PatchInstruction,
238
- target: DocumentMapMarkerContentPair
239
- ): string => {
240
- const targetBlockTypeBehavior =
241
- "targetBlockTypeBehavior" in instruction
242
- ? instruction.targetBlockTypeBehavior
243
- : "text";
244
-
245
- switch (targetBlockTypeBehavior) {
246
- case "text":
247
- return prependText(document, instruction, target);
248
- case "table":
249
- return prependTable(
250
- document,
251
- instruction as PrependTableRowsBlockPatchInstruction,
252
- target
253
- );
254
- }
255
- };
256
-
257
- const append = (
258
- document: string,
259
- instruction: ExtendingPatchInstruction & PatchInstruction,
260
- target: DocumentMapMarkerContentPair
261
- ): string => {
262
- const targetBlockTypeBehavior =
263
- "targetBlockTypeBehavior" in instruction
264
- ? instruction.targetBlockTypeBehavior
265
- : "text";
266
-
267
- switch (targetBlockTypeBehavior) {
268
- case "text":
269
- return appendText(document, instruction, target);
270
- case "table":
271
- return appendTable(
272
- document,
273
- instruction as AppendTableRowsBlockPatchInstruction,
274
- target
275
- );
276
- }
277
- };
278
-
279
- const getTarget = (
280
- map: DocumentMap,
281
- instruction: PatchInstruction
282
- ): DocumentMapMarkerContentPair | undefined => {
283
- switch (instruction.targetType) {
284
- case "heading":
285
- return map.heading[
286
- instruction.target ? instruction.target.join("\u001f") : ""
287
- ];
288
- case "block":
289
- return map.block[instruction.target];
290
- }
291
- };
292
-
293
- export const applyPatch = (
294
- document: string,
295
- instruction: PatchInstruction
296
- ): string => {
297
- const map = getDocumentMap(document);
298
- const target = getTarget(map, instruction);
299
-
300
- if (!target) {
301
- throw new PatchFailed(PatchFailureReason.InvalidTarget, instruction, null);
302
- }
303
-
304
- if (
305
- (!("applyIfContentPreexists" in instruction) ||
306
- !instruction.applyIfContentPreexists) &&
307
- typeof instruction.content === "string" &&
308
- document
309
- .slice(target.content.start, target.content.end)
310
- .includes(instruction.content.trim())
311
- ) {
312
- throw new PatchFailed(
313
- PatchFailureReason.ContentAlreadyPreexistsInTarget,
314
- instruction,
315
- target
316
- );
317
- }
318
- switch (instruction.operation) {
319
- case "append":
320
- return append(document, instruction, target);
321
- case "prepend":
322
- return prepend(document, instruction, target);
323
- case "replace":
324
- return replace(document, instruction, target);
325
- }
326
- };