@knpkv/confluence-to-markdown 0.4.2 → 0.6.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +45 -10
  3. package/dist/ConfluenceAuth.d.ts.map +1 -1
  4. package/dist/ConfluenceAuth.js +12 -22
  5. package/dist/ConfluenceAuth.js.map +1 -1
  6. package/dist/ConfluenceClient.d.ts +13 -3
  7. package/dist/ConfluenceClient.d.ts.map +1 -1
  8. package/dist/ConfluenceClient.js +34 -70
  9. package/dist/ConfluenceClient.js.map +1 -1
  10. package/dist/ConfluenceError.d.ts +12 -12
  11. package/dist/GitError.d.ts +5 -5
  12. package/dist/GitService.d.ts.map +1 -1
  13. package/dist/GitService.js +0 -3
  14. package/dist/GitService.js.map +1 -1
  15. package/dist/SchemaConverterError.d.ts +3 -3
  16. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +23 -0
  17. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +1 -0
  18. package/dist/parsers/preprocessing/ConfluencePreprocessing.js +323 -0
  19. package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +1 -0
  20. package/dist/parsers/preprocessing/index.d.ts +7 -0
  21. package/dist/parsers/preprocessing/index.d.ts.map +1 -0
  22. package/dist/parsers/preprocessing/index.js +7 -0
  23. package/dist/parsers/preprocessing/index.js.map +1 -0
  24. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +29 -0
  25. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -1
  26. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +3 -5
  27. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -1
  28. package/package.json +35 -26
  29. package/src/AdfPlaceholders.ts +266 -0
  30. package/src/AdfSchemaValidator.ts +67 -0
  31. package/src/AdfWalker.ts +511 -0
  32. package/src/AtlaskitTransformers.ts +72 -0
  33. package/src/ConfluenceClient.ts +4 -4
  34. package/src/ConfluenceError.ts +65 -3
  35. package/src/MarkdownConverter.ts +106 -139
  36. package/src/Schemas.ts +4 -4
  37. package/src/SyncEngine.ts +130 -83
  38. package/src/atlaskit-adf-schema.d.ts +3 -0
  39. package/src/commands/clone.ts +8 -1
  40. package/src/commands/layers.ts +11 -4
  41. package/src/index.ts +3 -18
  42. package/test/AdfPlaceholders.test.ts +295 -0
  43. package/test/AdfSchemaValidator.test.ts +34 -0
  44. package/test/AdfWalker.test.ts +530 -0
  45. package/test/AtlaskitTransformers.test.ts +25 -0
  46. package/test/MarkdownConverter.test.ts +120 -105
  47. package/test/RoundTrip.test.ts +266 -0
  48. package/LICENSE +0 -21
  49. package/src/SchemaConverterError.ts +0 -108
  50. package/src/ast/BlockNode.ts +0 -425
  51. package/src/ast/Document.ts +0 -90
  52. package/src/ast/InlineNode.ts +0 -323
  53. package/src/ast/MacroNode.ts +0 -245
  54. package/src/ast/index.ts +0 -83
  55. package/src/parsers/ConfluenceParser.ts +0 -950
  56. package/src/parsers/MarkdownParser.ts +0 -1198
  57. package/src/parsers/index.ts +0 -8
  58. package/src/schemas/ConfluenceSchema.ts +0 -56
  59. package/src/schemas/ConversionSchema.ts +0 -318
  60. package/src/schemas/MarkdownSchema.ts +0 -56
  61. package/src/schemas/hast/HastFromHtml.ts +0 -153
  62. package/src/schemas/hast/HastSchema.ts +0 -274
  63. package/src/schemas/hast/index.ts +0 -35
  64. package/src/schemas/index.ts +0 -20
  65. package/src/schemas/mdast/MdastFromMarkdown.ts +0 -118
  66. package/src/schemas/mdast/MdastSchema.ts +0 -566
  67. package/src/schemas/mdast/index.ts +0 -59
  68. package/src/schemas/mdast/mdastToString.ts +0 -102
  69. package/src/schemas/nodes/block/BlockSchema.ts +0 -773
  70. package/src/schemas/nodes/block/index.ts +0 -13
  71. package/src/schemas/nodes/index.ts +0 -20
  72. package/src/schemas/nodes/inline/InlineSchema.ts +0 -523
  73. package/src/schemas/nodes/inline/index.ts +0 -14
  74. package/src/schemas/nodes/macro/MacroSchema.ts +0 -226
  75. package/src/schemas/nodes/macro/index.ts +0 -6
  76. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +0 -446
  77. package/src/schemas/preprocessing/index.ts +0 -8
  78. package/src/serializers/ConfluenceSerializer.ts +0 -717
  79. package/src/serializers/MarkdownSerializer.ts +0 -493
  80. package/src/serializers/index.ts +0 -8
  81. package/test/ast/BlockNode.test.ts +0 -265
  82. package/test/ast/Document.test.ts +0 -126
  83. package/test/ast/InlineNode.test.ts +0 -161
  84. package/test/fixtures/integration-test.html.fixture +0 -103
  85. package/test/fixtures/integration-test.md.expected +0 -257
  86. package/test/parsers/ConfluenceParser.test.ts +0 -283
  87. package/test/schemas/ConfluencePreprocessor.test.ts +0 -180
  88. package/test/schemas/ConversionSchema.test.ts +0 -159
  89. package/test/schemas/HastSchema.test.ts +0 -138
  90. package/test/schemas/MdastSchema.test.ts +0 -145
  91. package/test/schemas/nodes/block/BlockSchema.test.ts +0 -173
  92. package/test/schemas/nodes/inline/InlineSchema.test.ts +0 -198
  93. package/test/schemas/nodes/macro/MacroSchema.test.ts +0 -142
@@ -0,0 +1,295 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { revertPlaceholders } from "../src/AdfPlaceholders.js"
3
+
4
+ const docOf = (content: ReadonlyArray<unknown>) => ({ version: 1, type: "doc", content })
5
+ const para = (text: string) => ({ type: "paragraph", content: [{ type: "text", text }] })
6
+
7
+ describe("revertPlaceholders", () => {
8
+ it("rewrites a status span placeholder into a status node", () => {
9
+ const out = revertPlaceholders(
10
+ docOf([para(`Some <span class="adf-status" data-color="blue">TESTING</span> here`)])
11
+ ) as { content: Array<{ content: Array<{ type: string; attrs?: Record<string, unknown> }> }> }
12
+
13
+ const cellContent = out.content[0]!.content
14
+ expect(cellContent).toHaveLength(3)
15
+ expect(cellContent[0]).toMatchObject({ type: "text", text: "Some " })
16
+ expect(cellContent[1]).toMatchObject({
17
+ type: "status",
18
+ attrs: { text: "TESTING", color: "blue" }
19
+ })
20
+ expect(cellContent[2]).toMatchObject({ type: "text", text: " here" })
21
+ })
22
+
23
+ it("replaces a single-comment paragraph with a block extension node", () => {
24
+ const out = revertPlaceholders(
25
+ docOf([para(`<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core -->`)])
26
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown> }> }
27
+
28
+ expect(out.content[0]).toMatchObject({
29
+ type: "extension",
30
+ attrs: { extensionKey: "toc", extensionType: "com.atlassian.confluence.macro.core" }
31
+ })
32
+ })
33
+
34
+ it("rewrites status and extension placeholders inside table cells", () => {
35
+ const cell = (text: string) => ({
36
+ type: "tableCell",
37
+ attrs: {},
38
+ content: [{ type: "paragraph", content: [{ type: "text", text }] }]
39
+ })
40
+ const out = revertPlaceholders(
41
+ docOf([{
42
+ type: "table",
43
+ attrs: { isNumberColumnEnabled: false, layout: "default" },
44
+ content: [{
45
+ type: "tableRow",
46
+ content: [
47
+ cell(`<span class="adf-status" data-color="green">OK</span>`),
48
+ cell(`<!-- adf:extension key=toc type=t -->`)
49
+ ]
50
+ }]
51
+ }])
52
+ ) as {
53
+ content: Array<{
54
+ content: Array<{
55
+ content: Array<{
56
+ content: Array<{ type: string; attrs?: Record<string, unknown>; content?: ReadonlyArray<unknown> }>
57
+ }>
58
+ }>
59
+ }>
60
+ }
61
+
62
+ const cells = out.content[0]!.content[0]!.content
63
+ // First cell: paragraph wrapping a status node
64
+ expect(cells[0]!.content[0]).toMatchObject({
65
+ type: "paragraph",
66
+ content: [{ type: "status", attrs: { text: "OK", color: "green" } }]
67
+ })
68
+ // Second cell: extension replaces the paragraph entirely
69
+ expect(cells[1]!.content[0]).toMatchObject({
70
+ type: "extension",
71
+ attrs: { extensionKey: "toc", extensionType: "t" }
72
+ })
73
+ })
74
+
75
+ it("rewrites inlineExtension placeholders inline", () => {
76
+ const out = revertPlaceholders(
77
+ docOf([para(`before <!-- adf:inlineExtension key=jira type=t --> after`)])
78
+ ) as { content: Array<{ content: Array<{ type: string; attrs?: Record<string, unknown> }> }> }
79
+
80
+ const inlineContent = out.content[0]!.content
81
+ expect(inlineContent).toHaveLength(3)
82
+ expect(inlineContent[1]).toMatchObject({
83
+ type: "inlineExtension",
84
+ attrs: { extensionKey: "jira", extensionType: "t" }
85
+ })
86
+ })
87
+
88
+ it("restores the full attrs (parameters included) from an attrs blob", () => {
89
+ const attrs = {
90
+ extensionKey: "toc",
91
+ extensionType: "com.atlassian.confluence.macro.core",
92
+ layout: "default",
93
+ localId: "abc-123",
94
+ parameters: { macroParams: { maxLevel: { value: "3" } } }
95
+ }
96
+ const blob = Buffer.from(JSON.stringify(attrs)).toString("base64")
97
+ const out = revertPlaceholders(
98
+ docOf([para(`<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=${blob} -->`)])
99
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown> }> }
100
+
101
+ expect(out.content[0]).toEqual({ type: "extension", attrs })
102
+ })
103
+
104
+ it("falls back to key/type when the attrs blob does not decode to JSON", () => {
105
+ // "aGVsbG8=" is valid base64 but decodes to "hello", which is not JSON.
106
+ const out = revertPlaceholders(
107
+ docOf([para(`<!-- adf:extension key=toc type=t attrs=aGVsbG8= -->`)])
108
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown> }> }
109
+
110
+ expect(out.content[0]).toEqual({
111
+ type: "extension",
112
+ attrs: { extensionKey: "toc", extensionType: "t" }
113
+ })
114
+ })
115
+
116
+ it("re-attaches the blocks between bodiedExtension markers as its body", () => {
117
+ const attrs = { extensionKey: "details", extensionType: "com.example" }
118
+ const blob = Buffer.from(JSON.stringify(attrs)).toString("base64")
119
+ const out = revertPlaceholders(
120
+ docOf([
121
+ para(`<!-- adf:bodiedExtension key=details type=com.example attrs=${blob} -->`),
122
+ para("first body paragraph"),
123
+ para("second body paragraph"),
124
+ para(`<!-- adf:/bodiedExtension -->`),
125
+ para("after")
126
+ ])
127
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown>; content?: ReadonlyArray<unknown> }> }
128
+
129
+ expect(out.content).toHaveLength(2)
130
+ expect(out.content[0]).toEqual({
131
+ type: "bodiedExtension",
132
+ attrs,
133
+ content: [para("first body paragraph"), para("second body paragraph")]
134
+ })
135
+ expect(out.content[1]).toEqual(para("after"))
136
+ })
137
+
138
+ it("reverts an extension marker nested inside a bodiedExtension body", () => {
139
+ const out = revertPlaceholders(
140
+ docOf([
141
+ para(`<!-- adf:bodiedExtension key=outer type=com.example -->`),
142
+ para(`<!-- adf:extension key=inner type=com.example -->`),
143
+ para(`<!-- adf:/bodiedExtension -->`)
144
+ ])
145
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown>; content?: ReadonlyArray<unknown> }> }
146
+
147
+ expect(out.content[0]).toEqual({
148
+ type: "bodiedExtension",
149
+ attrs: { extensionKey: "outer", extensionType: "com.example" },
150
+ content: [{ type: "extension", attrs: { extensionKey: "inner", extensionType: "com.example" } }]
151
+ })
152
+ })
153
+
154
+ it("downgrades a bodiedExtension marker without an end marker to a plain extension", () => {
155
+ const out = revertPlaceholders(
156
+ docOf([
157
+ para(`<!-- adf:bodiedExtension key=details type=com.example -->`),
158
+ para("just a paragraph, no end marker")
159
+ ])
160
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown> }> }
161
+
162
+ expect(out.content[0]).toEqual({
163
+ type: "extension",
164
+ attrs: { extensionKey: "details", extensionType: "com.example" }
165
+ })
166
+ expect(out.content[1]).toEqual(para("just a paragraph, no end marker"))
167
+ })
168
+
169
+ it("keeps the bodied kind for an empty-body open/end pair via a stub paragraph", () => {
170
+ const out = revertPlaceholders(
171
+ docOf([
172
+ para(`<!-- adf:bodiedExtension key=excerpt type=com.example -->`),
173
+ para(`<!-- adf:/bodiedExtension -->`)
174
+ ])
175
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown>; content?: ReadonlyArray<unknown> }> }
176
+
177
+ expect(out.content[0]).toEqual({
178
+ type: "bodiedExtension",
179
+ attrs: { extensionKey: "excerpt", extensionType: "com.example" },
180
+ content: [{ type: "paragraph", content: [] }]
181
+ })
182
+ })
183
+
184
+ it("does not let an unpaired open marker steal a later macro's end marker", () => {
185
+ const out = revertPlaceholders(
186
+ docOf([
187
+ para(`<!-- adf:bodiedExtension key=legacy type=com.example -->`),
188
+ para("unrelated paragraph"),
189
+ para(`<!-- adf:bodiedExtension key=modern type=com.example -->`),
190
+ para("modern body"),
191
+ para(`<!-- adf:/bodiedExtension -->`)
192
+ ])
193
+ ) as { content: Array<{ type: string; attrs?: Record<string, unknown>; content?: ReadonlyArray<unknown> }> }
194
+
195
+ expect(out.content).toEqual([
196
+ { type: "extension", attrs: { extensionKey: "legacy", extensionType: "com.example" } },
197
+ para("unrelated paragraph"),
198
+ {
199
+ type: "bodiedExtension",
200
+ attrs: { extensionKey: "modern", extensionType: "com.example" },
201
+ content: [para("modern body")]
202
+ }
203
+ ])
204
+ })
205
+
206
+ it("downgrades a bodied marker to extension where the schema forbids bodiedExtension", () => {
207
+ // blockquote content allows extension but not bodiedExtension — emitting
208
+ // one would fail outgoing validation and the push would error out.
209
+ const out = revertPlaceholders(
210
+ docOf([{
211
+ type: "blockquote",
212
+ content: [
213
+ para(`<!-- adf:bodiedExtension key=k type=t -->`),
214
+ para("body text"),
215
+ para(`<!-- adf:/bodiedExtension -->`)
216
+ ]
217
+ }])
218
+ ) as { content: Array<{ type: string; content?: ReadonlyArray<unknown> }> }
219
+
220
+ expect(out.content[0]!.content).toEqual([
221
+ { type: "extension", attrs: { extensionKey: "k", extensionType: "t" } },
222
+ para("body text")
223
+ ])
224
+ })
225
+
226
+ it("drops a stray end marker", () => {
227
+ const out = revertPlaceholders(
228
+ docOf([para("before"), para(`<!-- adf:/bodiedExtension -->`), para("after")])
229
+ ) as { content: Array<unknown> }
230
+
231
+ expect(out.content).toEqual([para("before"), para("after")])
232
+ })
233
+
234
+ it("leaves code-marked text that quotes placeholder syntax untouched", () => {
235
+ const codeText = (text: string) => ({
236
+ type: "paragraph",
237
+ content: [{ type: "text", text, marks: [{ type: "code" }] }]
238
+ })
239
+ const input = docOf([
240
+ codeText(`<span class="adf-status" data-color="green">DONE</span>`),
241
+ codeText(`<!-- adf:extension key=k type=t -->`),
242
+ codeText(`<!-- adf:/bodiedExtension -->`)
243
+ ])
244
+ expect(revertPlaceholders(input)).toEqual(input)
245
+ })
246
+
247
+ it("leaves placeholder-looking text inside a codeBlock untouched", () => {
248
+ // A code sample *quoting* the placeholder syntax must not get structured
249
+ // nodes injected — codeBlock only permits text children, so the document
250
+ // would fail outgoing schema validation and the push would error out.
251
+ const code = `<span class="adf-status" data-color="blue">X</span>\n<!-- adf:inlineExtension key=k type=t -->`
252
+ const input = docOf([{
253
+ type: "codeBlock",
254
+ attrs: { language: "html" },
255
+ content: [{ type: "text", text: code }]
256
+ }])
257
+ expect(revertPlaceholders(input)).toEqual(input)
258
+ })
259
+
260
+ it("leaves text without placeholders untouched", () => {
261
+ const input = docOf([para("plain content")])
262
+ const out = revertPlaceholders(input)
263
+ expect(out).toEqual(input)
264
+ })
265
+
266
+ it("rewrites a confluence-mention link into a mention node", () => {
267
+ const out = revertPlaceholders(
268
+ docOf([{
269
+ type: "paragraph",
270
+ content: [{
271
+ type: "text",
272
+ text: "@Andrey Konopkov",
273
+ marks: [{ type: "link", attrs: { href: "confluence-mention://557057%3Aabc-123" } }]
274
+ }]
275
+ }])
276
+ ) as { content: Array<{ content: Array<{ type: string; attrs?: Record<string, unknown> }> }> }
277
+
278
+ expect(out.content[0]!.content[0]).toMatchObject({
279
+ type: "mention",
280
+ attrs: { id: "557057:abc-123", text: "@Andrey Konopkov" }
281
+ })
282
+ })
283
+
284
+ it("leaves ordinary links alone", () => {
285
+ const input = docOf([{
286
+ type: "paragraph",
287
+ content: [{
288
+ type: "text",
289
+ text: "click",
290
+ marks: [{ type: "link", attrs: { href: "https://example.com" } }]
291
+ }]
292
+ }])
293
+ expect(revertPlaceholders(input)).toEqual(input)
294
+ })
295
+ })
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import * as Effect from "effect/Effect"
3
+ import { AdfSchemaValidator, layer as AdfSchemaValidatorLayer } from "../src/AdfSchemaValidator.js"
4
+
5
+ describe("AdfSchemaValidator", () => {
6
+ it.effect("accepts a valid minimal doc and narrows the type", () =>
7
+ Effect.gen(function*() {
8
+ const v = yield* AdfSchemaValidator
9
+ const doc = {
10
+ version: 1,
11
+ type: "doc",
12
+ content: [{ type: "paragraph", content: [{ type: "text", text: "hi" }] }]
13
+ }
14
+ const result = yield* v.check(doc, "incoming")
15
+ expect(result.type).toBe("doc")
16
+ }).pipe(Effect.provide(AdfSchemaValidatorLayer)))
17
+
18
+ it.effect("fails with structured issues on a structurally invalid doc", () =>
19
+ Effect.gen(function*() {
20
+ const v = yield* AdfSchemaValidator
21
+ const doc = {
22
+ version: 1,
23
+ type: "doc",
24
+ content: [{ type: "paragraph", content: [{ type: "text", text: 42 }] }]
25
+ }
26
+ const result = yield* Effect.either(v.check(doc, "incoming"))
27
+ expect(result._tag).toBe("Left")
28
+ if (result._tag === "Left") {
29
+ expect(result.left._tag).toBe("AdfSchemaError")
30
+ expect(result.left.direction).toBe("incoming")
31
+ expect(result.left.issues.length).toBeGreaterThan(0)
32
+ }
33
+ }).pipe(Effect.provide(AdfSchemaValidatorLayer)))
34
+ })