markdown-patch 0.1.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.
@@ -0,0 +1,81 @@
1
+ ---
2
+ aliases:
3
+ - Structured Markdown Patch
4
+ project-type: Technical
5
+ repository: https://github.com/coddingtonbear/markdown-patch
6
+ ---
7
+
8
+ # Overview
9
+ Beep Boop
10
+ # Problems
11
+ - ~~It would be nice for the mechanism to be able to handle something like `upsert` for frontmatter fields. See [[#^e6068e]] in addition to what we already support [[#^259a73]].~~
12
+ - This was solved by making every content block directly-addressable. All interactions treat the document as a key-value mapping.
13
+ - ~~You can't use our earlier header delimiter `::` in an HTTP header; how did I not notice that? I've had a [conversation with ChatGPT](https://chatgpt.com/share/117b262a-f534-40e6-bc05-287758706f34) to land on a choice of `@#@` instead, but there aren't obvious good options. See [[#^1d6271]]~~
14
+ - I changed my mind in the end and went with the more-likely-to-collide-but-at-least-not-bizarre `///`.
15
+ - Should we allow partial matches? The pros are that it would make the usual, garden path of just wanting to push content into a section very easy. The cons are that it makes it kind of unclear what's going to happen when you do an `upsert` or `insert` for a particular value. (See [[#^bfec1f]] for more).
16
+
17
+ # Actions
18
+ | Name | Description | Heading? | Frontmatter? | Block |
19
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------- |
20
+ | `update` | Find the referenced `target` and replace the content at that region. | ✅ | ✅ | ✅ |
21
+ | `append` | Find the referenced `target` and add content to the end of its region. | ✅ | ✅ | ✅ |
22
+ | `prepend` | Find the referenced `target` and add content to the beginning of its region. | ✅ | ✅ | ✅ |
23
+ | `insert` | Find the path leading to the referenced `target` and add the specified content under a the specified name. This will create a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
24
+ | `upsert` | Find the path leading to the referenced `target` and either replace the content under the specified name, or add new content with a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
25
+
26
+ ^2c67a6
27
+
28
+ # Headers
29
+ | Header | Explanation |
30
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | `Target-Type` | `heading`, `block`, `frontmatter` |
32
+ | `Target` | Name of the target:<br>- `heading`: The `///`-delimited path to the heading to replace the content of. E.g. `Page Targets///Block///Use Cases`. This value should be URL-encoded. |
33
+ | `Target-Delimiter` | By default, we use `///` to delimit a `Target`, but it's remotely possible that this value might be present in a header. If it is, you can specify a different delimiter to use for `Target`. |
34
+
35
+ ^1d6271
36
+
37
+ # Page Targets
38
+
39
+ ## Heading
40
+
41
+ | Heading | Value |
42
+ | ------------- | ----------------------------------------------------- |
43
+ | `Target-Type` | `heading` |
44
+ | `Target` | The path to the heading you would like to append to. |
45
+
46
+ ^bfec1f
47
+
48
+ | Position | Where |
49
+ | -------- | -------------------------------------------------------------- |
50
+ | Start | Beginning of line immediately following line named by heading |
51
+ | End | Last newline before heading of same or higher priority or EOF. |
52
+ | | |
53
+ Unlike with [[#Heading]], this targets the *content* of the heading and does not include the heading iself.
54
+ - ✅: ...replacing the content specified by a particular heading.
55
+ - ✅: ...appending content to a block specified by a particular heading.
56
+ ## Block
57
+ | Position | Where |
58
+ | -------- | ------------------------------------------------------ |
59
+ | Start | Beginning of line for specified block. |
60
+ | End | Last character (including newline) of specified block. |
61
+ A "Block" in Obsidian can be any *block*-type element. This might mean a paragraph, but it could also mean a table, but how block references are marked differs in Obsidian depending upon what kind of block is being marked.
62
+ ### Use Cases
63
+ - ✅: ...replacing the content specified by a particular block ID.
64
+ - I want to be able to replace a whole table or whole paragraph with new content.
65
+ - ✅: ...appending content to a block specified by a particular block ID.
66
+ - I want to be able to add new rows to an existing table.
67
+ - I want to be able to add new content to the end of a line.
68
+
69
+ ## Frontmatter Field
70
+
71
+ | Position | Where |
72
+ | -------- | ------------------------------------------------------------------------ |
73
+ | Start | First character of content referenced by a particular frontmatter field. |
74
+ | End | Last character of content referenced by a particular frontmatter field. |
75
+ ### Use Cases
76
+ - ✅: ...appending content to an existing frontmatter field.
77
+ - ✅: ...replacing the content of an existing frontmatter field. ^259a73
78
+ - ✅: ...adding a new frontmatter field. ^e6068e
79
+ ## Document Properties (Exploratory)
80
+
81
+ [^block-ambiguity]: There is currently no obvious place to plop a block were we to create a new one. So, I might implement this such that these actions *work*, but will just add the block to the end of a file. This isn't great, but it's at least consistent?
@@ -0,0 +1,80 @@
1
+ ---
2
+ aliases:
3
+ - Structured Markdown Patch
4
+ project-type: Technical
5
+ repository: https://github.com/coddingtonbear/markdown-patch
6
+ ---
7
+
8
+ # Overview
9
+ I came up with this project for supporting [[Obsidian Local Rest API]] (and via that [[Obsidian Web]]) because I often need to shove data into Markdown documents for posterity, but want to be able to do that programmatically with more care than just shoving content to the end of a file.
10
+ # Problems
11
+ - ~~It would be nice for the mechanism to be able to handle something like `upsert` for frontmatter fields. See [[#^e6068e]] in addition to what we already support [[#^259a73]].~~
12
+ - This was solved by making every content block directly-addressable. All interactions treat the document as a key-value mapping.
13
+ - ~~You can't use our earlier header delimiter `::` in an HTTP header; how did I not notice that? I've had a [conversation with ChatGPT](https://chatgpt.com/share/117b262a-f534-40e6-bc05-287758706f34) to land on a choice of `@#@` instead, but there aren't obvious good options. See [[#^1d6271]]~~
14
+ - I changed my mind in the end and went with the more-likely-to-collide-but-at-least-not-bizarre `///`.
15
+ - Should we allow partial matches? The pros are that it would make the usual, garden path of just wanting to push content into a section very easy. The cons are that it makes it kind of unclear what's going to happen when you do an `upsert` or `insert` for a particular value. (See [[#^bfec1f]] for more).Beep Boop
16
+ # Actions
17
+ | Name | Description | Heading? | Frontmatter? | Block |
18
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------- |
19
+ | `update` | Find the referenced `target` and replace the content at that region. | ✅ | ✅ | ✅ |
20
+ | `append` | Find the referenced `target` and add content to the end of its region. | ✅ | ✅ | ✅ |
21
+ | `prepend` | Find the referenced `target` and add content to the beginning of its region. | ✅ | ✅ | ✅ |
22
+ | `insert` | Find the path leading to the referenced `target` and add the specified content under a the specified name. This will create a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
23
+ | `upsert` | Find the path leading to the referenced `target` and either replace the content under the specified name, or add new content with a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
24
+
25
+ ^2c67a6
26
+
27
+ # Headers
28
+ | Header | Explanation |
29
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `Target-Type` | `heading`, `block`, `frontmatter` |
31
+ | `Target` | Name of the target:<br>- `heading`: The `///`-delimited path to the heading to replace the content of. E.g. `Page Targets///Block///Use Cases`. This value should be URL-encoded. |
32
+ | `Target-Delimiter` | By default, we use `///` to delimit a `Target`, but it's remotely possible that this value might be present in a header. If it is, you can specify a different delimiter to use for `Target`. |
33
+
34
+ ^1d6271
35
+
36
+ # Page Targets
37
+
38
+ ## Heading
39
+
40
+ | Heading | Value |
41
+ | ------------- | ----------------------------------------------------- |
42
+ | `Target-Type` | `heading` |
43
+ | `Target` | The path to the heading you would like to append to. |
44
+
45
+ ^bfec1f
46
+
47
+ | Position | Where |
48
+ | -------- | -------------------------------------------------------------- |
49
+ | Start | Beginning of line immediately following line named by heading |
50
+ | End | Last newline before heading of same or higher priority or EOF. |
51
+ | | |
52
+ Unlike with [[#Heading]], this targets the *content* of the heading and does not include the heading iself.
53
+ - ✅: ...replacing the content specified by a particular heading.
54
+ - ✅: ...appending content to a block specified by a particular heading.
55
+ ## Block
56
+ | Position | Where |
57
+ | -------- | ------------------------------------------------------ |
58
+ | Start | Beginning of line for specified block. |
59
+ | End | Last character (including newline) of specified block. |
60
+ A "Block" in Obsidian can be any *block*-type element. This might mean a paragraph, but it could also mean a table, but how block references are marked differs in Obsidian depending upon what kind of block is being marked.
61
+ ### Use Cases
62
+ - ✅: ...replacing the content specified by a particular block ID.
63
+ - I want to be able to replace a whole table or whole paragraph with new content.
64
+ - ✅: ...appending content to a block specified by a particular block ID.
65
+ - I want to be able to add new rows to an existing table.
66
+ - I want to be able to add new content to the end of a line.
67
+
68
+ ## Frontmatter Field
69
+
70
+ | Position | Where |
71
+ | -------- | ------------------------------------------------------------------------ |
72
+ | Start | First character of content referenced by a particular frontmatter field. |
73
+ | End | Last character of content referenced by a particular frontmatter field. |
74
+ ### Use Cases
75
+ - ✅: ...appending content to an existing frontmatter field.
76
+ - ✅: ...replacing the content of an existing frontmatter field. ^259a73
77
+ - ✅: ...adding a new frontmatter field. ^e6068e
78
+ ## Document Properties (Exploratory)
79
+
80
+ [^block-ambiguity]: There is currently no obvious place to plop a block were we to create a new one. So, I might implement this such that these actions *work*, but will just add the block to the end of a file. This isn't great, but it's at least consistent?
@@ -0,0 +1,80 @@
1
+ ---
2
+ aliases:
3
+ - Structured Markdown Patch
4
+ project-type: Technical
5
+ repository: https://github.com/coddingtonbear/markdown-patch
6
+ ---
7
+
8
+ # Overview
9
+ I came up with this project for supporting [[Obsidian Local Rest API]] (and via that [[Obsidian Web]]) because I often need to shove data into Markdown documents for posterity, but want to be able to do that programmatically with more care than just shoving content to the end of a file.
10
+ # Problems
11
+ - ~~It would be nice for the mechanism to be able to handle something like `upsert` for frontmatter fields. See [[#^e6068e]] in addition to what we already support [[#^259a73]].~~
12
+ - This was solved by making every content block directly-addressable. All interactions treat the document as a key-value mapping.
13
+ - ~~You can't use our earlier header delimiter `::` in an HTTP header; how did I not notice that? I've had a [conversation with ChatGPT](https://chatgpt.com/share/117b262a-f534-40e6-bc05-287758706f34) to land on a choice of `@#@` instead, but there aren't obvious good options. See [[#^1d6271]]~~
14
+ - I changed my mind in the end and went with the more-likely-to-collide-but-at-least-not-bizarre `///`.
15
+ - Should we allow partial matches? The pros are that it would make the usual, garden path of just wanting to push content into a section very easy. The cons are that it makes it kind of unclear what's going to happen when you do an `upsert` or `insert` for a particular value. (See [[#^bfec1f]] for more).
16
+
17
+ # Actions
18
+ | Name | Description | Heading? | Frontmatter? | Block |
19
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------- |
20
+ | `update` | Find the referenced `target` and replace the content at that region. | ✅ | ✅ | ✅ |
21
+ | `append` | Find the referenced `target` and add content to the end of its region. | ✅ | ✅ | ✅ |
22
+ | `prepend` | Find the referenced `target` and add content to the beginning of its region. | ✅ | ✅ | ✅ |
23
+ | `insert` | Find the path leading to the referenced `target` and add the specified content under a the specified name. This will create a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
24
+ | `upsert` | Find the path leading to the referenced `target` and either replace the content under the specified name, or add new content with a new header or frontmatter field if necessary. | ✅ | ✅ | ❌[^block-ambiguity] |
25
+
26
+ ^2c67a6
27
+
28
+ # Headers
29
+ | Header | Explanation |
30
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | `Target-Type` | `heading`, `block`, `frontmatter` |
32
+ | `Target` | Name of the target:<br>- `heading`: The `///`-delimited path to the heading to replace the content of. E.g. `Page Targets///Block///Use Cases`. This value should be URL-encoded. |
33
+ | `Target-Delimiter` | By default, we use `///` to delimit a `Target`, but it's remotely possible that this value might be present in a header. If it is, you can specify a different delimiter to use for `Target`. |
34
+
35
+ ^1d6271
36
+
37
+ # Page Targets
38
+
39
+ ## Heading
40
+
41
+ | Heading | Value |
42
+ | ------------- | ----------------------------------------------------- |
43
+ | `Target-Type` | `heading` |
44
+ | `Target` | The path to the heading you would like to append to. |
45
+
46
+ ^bfec1f
47
+
48
+ | Position | Where |
49
+ | -------- | -------------------------------------------------------------- |
50
+ | Start | Beginning of line immediately following line named by heading |
51
+ | End | Last newline before heading of same or higher priority or EOF. |
52
+ | | |
53
+ Unlike with [[#Heading]], this targets the *content* of the heading and does not include the heading iself.
54
+ - ✅: ...replacing the content specified by a particular heading.
55
+ - ✅: ...appending content to a block specified by a particular heading.
56
+ ## Block
57
+ | Position | Where |
58
+ | -------- | ------------------------------------------------------ |
59
+ | Start | Beginning of line for specified block. |
60
+ | End | Last character (including newline) of specified block. |
61
+ A "Block" in Obsidian can be any *block*-type element. This might mean a paragraph, but it could also mean a table, but how block references are marked differs in Obsidian depending upon what kind of block is being marked.
62
+ ### Use Cases
63
+ - ✅: ...replacing the content specified by a particular block ID.
64
+ - I want to be able to replace a whole table or whole paragraph with new content.
65
+ - ✅: ...appending content to a block specified by a particular block ID.
66
+ - I want to be able to add new rows to an existing table.
67
+ - I want to be able to add new content to the end of a line.
68
+
69
+ ## Frontmatter Field
70
+
71
+ | Position | Where |
72
+ | -------- | ------------------------------------------------------------------------ |
73
+ | Start | First character of content referenced by a particular frontmatter field. |
74
+ | End | Last character of content referenced by a particular frontmatter field. |
75
+ ### Use Cases
76
+ - ✅: ...appending content to an existing frontmatter field.
77
+ - ✅: ...replacing the content of an existing frontmatter field. ^259a73
78
+ - ✅: ...adding a new frontmatter field. ^e6068e
79
+ ## Document Properties (Exploratory)
80
+ Beep Boop[^block-ambiguity]: There is currently no obvious place to plop a block were we to create a new one. So, I might implement this such that these actions *work*, but will just add the block to the end of a file. This isn't great, but it's at least consistent?
package/src/types.ts ADDED
@@ -0,0 +1,155 @@
1
+ export interface DocumentRange {
2
+ start: number;
3
+ end: number;
4
+ }
5
+
6
+ export interface DocumentMapMarkerContentPair {
7
+ marker: DocumentRange;
8
+ content: DocumentRange;
9
+ }
10
+
11
+ export interface HeadingMarkerContentPair extends DocumentMapMarkerContentPair {
12
+ level: number;
13
+ }
14
+
15
+ export interface DocumentMap {
16
+ heading: Record<string, HeadingMarkerContentPair>;
17
+ block: Record<string, DocumentMapMarkerContentPair>;
18
+ }
19
+
20
+ export type PatchTargetType = "heading" | "block";
21
+
22
+ export type PatchOperation = "replace" | "prepend" | "append";
23
+
24
+ export interface BasePatchInstructionTarget {
25
+ targetType: PatchTargetType;
26
+ target: any;
27
+ }
28
+
29
+ export interface BasePatchInstructionOperation {
30
+ operation: string;
31
+ }
32
+
33
+ export interface BaseHeadingPatchInstruction
34
+ extends BasePatchInstructionTarget {
35
+ targetType: "heading";
36
+ target: string[] | null;
37
+ }
38
+
39
+ export interface BaseBlockPatchInstruction extends BasePatchInstructionTarget {
40
+ targetType: "block";
41
+ target: string;
42
+ }
43
+
44
+ export interface NonExtendingPatchInstruction
45
+ extends BasePatchInstructionOperation {}
46
+
47
+ export interface ExtendingPatchInstruction
48
+ extends BasePatchInstructionOperation {
49
+ /** Trim whitepsace from target before joining with content
50
+ *
51
+ * - For `prepend`: Trims content from the beginning of
52
+ * the target content.
53
+ * - For `append`: Trims content from the end of the target
54
+ * content. Your content should probably end in a newline
55
+ * in this case, or the trailing heading will no longer
56
+ * be the start of its own line
57
+ */
58
+ trimTargetWhitespace?: boolean;
59
+ /** Apply patch even if content already exists at target
60
+ *
61
+ * By default, we will fail to apply a patch if the supplied
62
+ * content is found anywhere in your target content. If you
63
+ * would instead like the patch to occur, regardless of whether
64
+ * it appears the content is already there, you can set
65
+ * `applyIfContentPreexists` to `true`.
66
+ */
67
+ applyIfContentPreexists?: boolean;
68
+ }
69
+
70
+ export interface StringContent {
71
+ content: string;
72
+ }
73
+
74
+ export interface TableRowsContent {
75
+ targetBlockTypeBehavior: "table";
76
+ content: string[][];
77
+ }
78
+
79
+ export interface PrependHeadingPatchInstruction
80
+ extends ExtendingPatchInstruction,
81
+ BaseHeadingPatchInstruction,
82
+ StringContent {
83
+ operation: "prepend";
84
+ }
85
+
86
+ export interface AppendHeadingPatchInstruction
87
+ extends ExtendingPatchInstruction,
88
+ BaseHeadingPatchInstruction,
89
+ StringContent {
90
+ operation: "append";
91
+ }
92
+
93
+ export interface ReplaceHeadingPatchInstruction
94
+ extends NonExtendingPatchInstruction,
95
+ BaseHeadingPatchInstruction,
96
+ StringContent {
97
+ operation: "replace";
98
+ }
99
+
100
+ export interface PrependBlockPatchInstruction
101
+ extends ExtendingPatchInstruction,
102
+ BaseBlockPatchInstruction,
103
+ StringContent {
104
+ operation: "prepend";
105
+ }
106
+
107
+ export interface AppendBlockPatchInstruction
108
+ extends ExtendingPatchInstruction,
109
+ BaseBlockPatchInstruction,
110
+ StringContent {
111
+ operation: "append";
112
+ }
113
+
114
+ export interface ReplaceBlockPatchInstruction
115
+ extends NonExtendingPatchInstruction,
116
+ BaseBlockPatchInstruction,
117
+ StringContent {
118
+ operation: "replace";
119
+ }
120
+
121
+ export interface PrependTableRowsBlockPatchInstruction
122
+ extends ExtendingPatchInstruction,
123
+ BaseBlockPatchInstruction,
124
+ TableRowsContent {
125
+ operation: "prepend";
126
+ }
127
+
128
+ export interface AppendTableRowsBlockPatchInstruction
129
+ extends ExtendingPatchInstruction,
130
+ BaseBlockPatchInstruction,
131
+ TableRowsContent {
132
+ operation: "append";
133
+ }
134
+
135
+ export interface ReplaceTableRowsBlockPatchInstruction
136
+ extends NonExtendingPatchInstruction,
137
+ BaseBlockPatchInstruction,
138
+ TableRowsContent {
139
+ operation: "replace";
140
+ }
141
+
142
+ export type HeadingPatchInstruction =
143
+ | PrependHeadingPatchInstruction
144
+ | AppendHeadingPatchInstruction
145
+ | ReplaceHeadingPatchInstruction;
146
+
147
+ export type BlockPatchInstruction =
148
+ | PrependBlockPatchInstruction
149
+ | AppendBlockPatchInstruction
150
+ | ReplaceBlockPatchInstruction
151
+ | PrependTableRowsBlockPatchInstruction
152
+ | AppendTableRowsBlockPatchInstruction
153
+ | ReplaceTableRowsBlockPatchInstruction;
154
+
155
+ export type PatchInstruction = HeadingPatchInstruction | BlockPatchInstruction;
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "@tsconfig/node16/tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext", /* Specify what module code is generated. */
5
+ "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6
+ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
7
+ "outDir": "./dist", /* Specify an output folder for all emitted files. */
8
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
9
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
10
+ "strict": true, /* Enable all strict type-checking options. */
11
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "ts-node": {
16
+ "transpileOnly": true
17
+ }
18
+ }