@knpkv/confluence-to-markdown 0.6.0 → 0.7.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 (357) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +22 -13
  4. package/dist/AdfPlaceholders.d.ts +42 -0
  5. package/dist/AdfPlaceholders.d.ts.map +1 -0
  6. package/dist/AdfPlaceholders.js +547 -0
  7. package/dist/AdfPlaceholders.js.map +1 -0
  8. package/dist/AdfSchemaValidator.d.ts +37 -0
  9. package/dist/AdfSchemaValidator.d.ts.map +1 -0
  10. package/dist/AdfSchemaValidator.js +37 -0
  11. package/dist/AdfSchemaValidator.js.map +1 -0
  12. package/dist/AdfWalker.d.ts +39 -0
  13. package/dist/AdfWalker.d.ts.map +1 -0
  14. package/dist/AdfWalker.js +527 -0
  15. package/dist/AdfWalker.js.map +1 -0
  16. package/dist/AtlaskitTransformers.d.ts +35 -0
  17. package/dist/AtlaskitTransformers.d.ts.map +1 -0
  18. package/dist/AtlaskitTransformers.js +48 -0
  19. package/dist/AtlaskitTransformers.js.map +1 -0
  20. package/dist/Brand.d.ts +6 -6
  21. package/dist/Brand.d.ts.map +1 -1
  22. package/dist/Brand.js +8 -6
  23. package/dist/Brand.js.map +1 -1
  24. package/dist/ConfluenceAuth.d.ts +4 -4
  25. package/dist/ConfluenceAuth.d.ts.map +1 -1
  26. package/dist/ConfluenceAuth.js +37 -39
  27. package/dist/ConfluenceAuth.js.map +1 -1
  28. package/dist/ConfluenceClient.d.ts +7 -17
  29. package/dist/ConfluenceClient.d.ts.map +1 -1
  30. package/dist/ConfluenceClient.js +81 -38
  31. package/dist/ConfluenceClient.js.map +1 -1
  32. package/dist/ConfluenceConfig.d.ts +3 -3
  33. package/dist/ConfluenceConfig.d.ts.map +1 -1
  34. package/dist/ConfluenceConfig.js +13 -11
  35. package/dist/ConfluenceConfig.js.map +1 -1
  36. package/dist/ConfluenceError.d.ts +68 -16
  37. package/dist/ConfluenceError.d.ts.map +1 -1
  38. package/dist/ConfluenceError.js +30 -1
  39. package/dist/ConfluenceError.js.map +1 -1
  40. package/dist/GitError.d.ts +5 -5
  41. package/dist/GitService.d.ts +11 -3
  42. package/dist/GitService.d.ts.map +1 -1
  43. package/dist/GitService.js +22 -27
  44. package/dist/GitService.js.map +1 -1
  45. package/dist/LocalFileSystem.d.ts +3 -3
  46. package/dist/LocalFileSystem.d.ts.map +1 -1
  47. package/dist/LocalFileSystem.js +6 -6
  48. package/dist/LocalFileSystem.js.map +1 -1
  49. package/dist/MarkdownConverter.d.ts +16 -65
  50. package/dist/MarkdownConverter.d.ts.map +1 -1
  51. package/dist/MarkdownConverter.js +64 -85
  52. package/dist/MarkdownConverter.js.map +1 -1
  53. package/dist/Schemas.d.ts +128 -141
  54. package/dist/Schemas.d.ts.map +1 -1
  55. package/dist/Schemas.js +21 -23
  56. package/dist/Schemas.js.map +1 -1
  57. package/dist/SyncEngine.d.ts +8 -5
  58. package/dist/SyncEngine.d.ts.map +1 -1
  59. package/dist/SyncEngine.js +189 -113
  60. package/dist/SyncEngine.js.map +1 -1
  61. package/dist/bin.js +23 -35
  62. package/dist/bin.js.map +1 -1
  63. package/dist/commands/auth.d.ts +2 -14
  64. package/dist/commands/auth.d.ts.map +1 -1
  65. package/dist/commands/auth.js +11 -16
  66. package/dist/commands/auth.js.map +1 -1
  67. package/dist/commands/clone.d.ts +4 -6
  68. package/dist/commands/clone.d.ts.map +1 -1
  69. package/dist/commands/clone.js +34 -32
  70. package/dist/commands/clone.js.map +1 -1
  71. package/dist/commands/delete.d.ts +2 -10
  72. package/dist/commands/delete.d.ts.map +1 -1
  73. package/dist/commands/delete.js +5 -4
  74. package/dist/commands/delete.js.map +1 -1
  75. package/dist/commands/errorHandler.d.ts +2 -1
  76. package/dist/commands/errorHandler.d.ts.map +1 -1
  77. package/dist/commands/errorHandler.js +22 -15
  78. package/dist/commands/errorHandler.js.map +1 -1
  79. package/dist/commands/fetch.d.ts +27 -0
  80. package/dist/commands/fetch.d.ts.map +1 -0
  81. package/dist/commands/fetch.js +48 -0
  82. package/dist/commands/fetch.js.map +1 -0
  83. package/dist/commands/git.d.ts +7 -10
  84. package/dist/commands/git.d.ts.map +1 -1
  85. package/dist/commands/git.js +6 -6
  86. package/dist/commands/git.js.map +1 -1
  87. package/dist/commands/index.d.ts +1 -0
  88. package/dist/commands/index.d.ts.map +1 -1
  89. package/dist/commands/index.js +1 -0
  90. package/dist/commands/index.js.map +1 -1
  91. package/dist/commands/layers.d.ts +10 -9
  92. package/dist/commands/layers.d.ts.map +1 -1
  93. package/dist/commands/layers.js +41 -30
  94. package/dist/commands/layers.js.map +1 -1
  95. package/dist/commands/new.d.ts +2 -6
  96. package/dist/commands/new.d.ts.map +1 -1
  97. package/dist/commands/new.js +5 -4
  98. package/dist/commands/new.js.map +1 -1
  99. package/dist/commands/pageInput.d.ts +19 -0
  100. package/dist/commands/pageInput.d.ts.map +1 -0
  101. package/dist/commands/pageInput.js +68 -0
  102. package/dist/commands/pageInput.js.map +1 -0
  103. package/dist/commands/root.d.ts +8 -0
  104. package/dist/commands/root.d.ts.map +1 -0
  105. package/dist/commands/root.js +29 -0
  106. package/dist/commands/root.js.map +1 -0
  107. package/dist/commands/sync.d.ts +6 -9
  108. package/dist/commands/sync.d.ts.map +1 -1
  109. package/dist/commands/sync.js +5 -6
  110. package/dist/commands/sync.js.map +1 -1
  111. package/dist/index.d.ts +3 -8
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -13
  114. package/dist/index.js.map +1 -1
  115. package/dist/internal/NodeLayers.d.ts.map +1 -1
  116. package/dist/internal/NodeLayers.js +1 -2
  117. package/dist/internal/NodeLayers.js.map +1 -1
  118. package/dist/internal/adfMetadata.d.ts +30 -0
  119. package/dist/internal/adfMetadata.d.ts.map +1 -0
  120. package/dist/internal/adfMetadata.js +126 -0
  121. package/dist/internal/adfMetadata.js.map +1 -0
  122. package/dist/internal/cleanMarkdown.d.ts +5 -0
  123. package/dist/internal/cleanMarkdown.d.ts.map +1 -0
  124. package/dist/internal/cleanMarkdown.js +13 -0
  125. package/dist/internal/cleanMarkdown.js.map +1 -0
  126. package/dist/internal/frontmatter.d.ts.map +1 -1
  127. package/dist/internal/frontmatter.js +41 -8
  128. package/dist/internal/frontmatter.js.map +1 -1
  129. package/dist/internal/gitCommands.d.ts +9 -3
  130. package/dist/internal/gitCommands.d.ts.map +1 -1
  131. package/dist/internal/gitCommands.js +18 -9
  132. package/dist/internal/gitCommands.js.map +1 -1
  133. package/dist/internal/hashUtils.d.ts +1 -1
  134. package/dist/internal/hashUtils.d.ts.map +1 -1
  135. package/dist/internal/hashUtils.js +1 -1
  136. package/dist/internal/hashUtils.js.map +1 -1
  137. package/dist/internal/oauthServer.d.ts +10 -5
  138. package/dist/internal/oauthServer.d.ts.map +1 -1
  139. package/dist/internal/oauthServer.js +19 -40
  140. package/dist/internal/oauthServer.js.map +1 -1
  141. package/dist/internal/pathUtils.d.ts +1 -1
  142. package/dist/internal/pathUtils.d.ts.map +1 -1
  143. package/dist/internal/pathUtils.js +1 -1
  144. package/dist/internal/pathUtils.js.map +1 -1
  145. package/dist/internal/process.d.ts +15 -0
  146. package/dist/internal/process.d.ts.map +1 -0
  147. package/dist/internal/process.js +10 -0
  148. package/dist/internal/process.js.map +1 -0
  149. package/dist/internal/stdio.d.ts +6 -0
  150. package/dist/internal/stdio.d.ts.map +1 -0
  151. package/dist/internal/stdio.js +15 -0
  152. package/dist/internal/stdio.js.map +1 -0
  153. package/dist/internal/tokenStorage.d.ts +3 -13
  154. package/dist/internal/tokenStorage.d.ts.map +1 -1
  155. package/dist/internal/tokenStorage.js +26 -24
  156. package/dist/internal/tokenStorage.js.map +1 -1
  157. package/dist/internal/userCache.d.ts +1 -1
  158. package/dist/internal/userCache.d.ts.map +1 -1
  159. package/dist/internal/userCache.js +1 -1
  160. package/dist/internal/userCache.js.map +1 -1
  161. package/package.json +30 -30
  162. package/skills/confluence/SKILL.md +143 -0
  163. package/skills/confluence/agents/openai.yaml +4 -0
  164. package/src/AdfPlaceholders.ts +310 -13
  165. package/src/AdfSchemaValidator.ts +2 -4
  166. package/src/AdfWalker.ts +122 -42
  167. package/src/AtlaskitTransformers.ts +2 -4
  168. package/src/Brand.ts +11 -16
  169. package/src/ConfluenceAuth.ts +22 -30
  170. package/src/ConfluenceClient.ts +24 -20
  171. package/src/ConfluenceConfig.ts +14 -14
  172. package/src/GitService.ts +39 -49
  173. package/src/LocalFileSystem.ts +7 -9
  174. package/src/MarkdownConverter.ts +2 -4
  175. package/src/Schemas.ts +13 -12
  176. package/src/SyncEngine.ts +151 -53
  177. package/src/bin.ts +30 -56
  178. package/src/commands/auth.ts +21 -18
  179. package/src/commands/clone.ts +38 -37
  180. package/src/commands/delete.ts +5 -4
  181. package/src/commands/errorHandler.ts +25 -18
  182. package/src/commands/fetch.ts +90 -0
  183. package/src/commands/git.ts +6 -6
  184. package/src/commands/index.ts +1 -0
  185. package/src/commands/layers.ts +53 -33
  186. package/src/commands/new.ts +5 -4
  187. package/src/commands/pageInput.ts +103 -0
  188. package/src/commands/root.ts +59 -0
  189. package/src/commands/sync.ts +7 -6
  190. package/src/internal/NodeLayers.ts +1 -2
  191. package/src/internal/adfMetadata.ts +145 -0
  192. package/src/internal/cleanMarkdown.ts +15 -0
  193. package/src/internal/frontmatter.ts +45 -8
  194. package/src/internal/gitCommands.ts +23 -17
  195. package/src/internal/hashUtils.ts +2 -2
  196. package/src/internal/oauthServer.ts +84 -105
  197. package/src/internal/pathUtils.ts +1 -1
  198. package/src/internal/process.ts +15 -0
  199. package/src/internal/stdio.ts +22 -0
  200. package/src/internal/tokenStorage.ts +39 -29
  201. package/src/internal/userCache.ts +2 -2
  202. package/test/AdfPlaceholders.test.ts +213 -0
  203. package/test/AdfSchemaValidator.test.ts +6 -6
  204. package/test/AdfWalker.test.ts +167 -21
  205. package/test/AtlaskitTransformers.test.ts +4 -4
  206. package/test/Brand.test.ts +11 -11
  207. package/test/GitService.test.ts +6 -2
  208. package/test/MarkdownConverter.test.ts +12 -11
  209. package/test/RoundTrip.test.ts +258 -3
  210. package/test/Schemas.test.ts +40 -40
  211. package/test/adfMetadata.test.ts +110 -0
  212. package/test/cleanMarkdown.test.ts +36 -0
  213. package/test/commandHarness.test.ts +79 -0
  214. package/test/commandHarness.ts +147 -0
  215. package/test/fetch.test.ts +61 -0
  216. package/test/frontmatter.test.ts +41 -0
  217. package/test/integration.test.ts +569 -156
  218. package/test/layers.test.ts +12 -0
  219. package/test/oauthServer.test.ts +4 -5
  220. package/test/pageInput.test.ts +83 -0
  221. package/test/tokenStorage.test.ts +17 -17
  222. package/dist/SchemaConverterError.d.ts +0 -108
  223. package/dist/SchemaConverterError.d.ts.map +0 -1
  224. package/dist/SchemaConverterError.js +0 -84
  225. package/dist/SchemaConverterError.js.map +0 -1
  226. package/dist/ast/BlockNode.d.ts +0 -468
  227. package/dist/ast/BlockNode.d.ts.map +0 -1
  228. package/dist/ast/BlockNode.js +0 -319
  229. package/dist/ast/BlockNode.js.map +0 -1
  230. package/dist/ast/Document.d.ts +0 -244
  231. package/dist/ast/Document.d.ts.map +0 -1
  232. package/dist/ast/Document.js +0 -69
  233. package/dist/ast/Document.js.map +0 -1
  234. package/dist/ast/InlineNode.d.ts +0 -477
  235. package/dist/ast/InlineNode.d.ts.map +0 -1
  236. package/dist/ast/InlineNode.js +0 -263
  237. package/dist/ast/InlineNode.js.map +0 -1
  238. package/dist/ast/MacroNode.d.ts +0 -267
  239. package/dist/ast/MacroNode.d.ts.map +0 -1
  240. package/dist/ast/MacroNode.js +0 -164
  241. package/dist/ast/MacroNode.js.map +0 -1
  242. package/dist/ast/index.d.ts +0 -10
  243. package/dist/ast/index.d.ts.map +0 -1
  244. package/dist/ast/index.js +0 -14
  245. package/dist/ast/index.js.map +0 -1
  246. package/dist/parsers/ConfluenceParser.d.ts +0 -26
  247. package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
  248. package/dist/parsers/ConfluenceParser.js +0 -792
  249. package/dist/parsers/ConfluenceParser.js.map +0 -1
  250. package/dist/parsers/MarkdownParser.d.ts +0 -26
  251. package/dist/parsers/MarkdownParser.d.ts.map +0 -1
  252. package/dist/parsers/MarkdownParser.js +0 -873
  253. package/dist/parsers/MarkdownParser.js.map +0 -1
  254. package/dist/parsers/index.d.ts +0 -8
  255. package/dist/parsers/index.d.ts.map +0 -1
  256. package/dist/parsers/index.js +0 -8
  257. package/dist/parsers/index.js.map +0 -1
  258. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +0 -23
  259. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +0 -1
  260. package/dist/parsers/preprocessing/ConfluencePreprocessing.js +0 -323
  261. package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +0 -1
  262. package/dist/parsers/preprocessing/index.d.ts +0 -7
  263. package/dist/parsers/preprocessing/index.d.ts.map +0 -1
  264. package/dist/parsers/preprocessing/index.js +0 -7
  265. package/dist/parsers/preprocessing/index.js.map +0 -1
  266. package/dist/schemas/ConfluenceSchema.d.ts +0 -21
  267. package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
  268. package/dist/schemas/ConfluenceSchema.js +0 -38
  269. package/dist/schemas/ConfluenceSchema.js.map +0 -1
  270. package/dist/schemas/ConversionSchema.d.ts +0 -35
  271. package/dist/schemas/ConversionSchema.d.ts.map +0 -1
  272. package/dist/schemas/ConversionSchema.js +0 -208
  273. package/dist/schemas/ConversionSchema.js.map +0 -1
  274. package/dist/schemas/MarkdownSchema.d.ts +0 -21
  275. package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
  276. package/dist/schemas/MarkdownSchema.js +0 -38
  277. package/dist/schemas/MarkdownSchema.js.map +0 -1
  278. package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
  279. package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
  280. package/dist/schemas/hast/HastFromHtml.js +0 -107
  281. package/dist/schemas/hast/HastFromHtml.js.map +0 -1
  282. package/dist/schemas/hast/HastSchema.d.ts +0 -195
  283. package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
  284. package/dist/schemas/hast/HastSchema.js +0 -183
  285. package/dist/schemas/hast/HastSchema.js.map +0 -1
  286. package/dist/schemas/hast/index.d.ts +0 -9
  287. package/dist/schemas/hast/index.d.ts.map +0 -1
  288. package/dist/schemas/hast/index.js +0 -3
  289. package/dist/schemas/hast/index.js.map +0 -1
  290. package/dist/schemas/index.d.ts +0 -14
  291. package/dist/schemas/index.d.ts.map +0 -1
  292. package/dist/schemas/index.js +0 -16
  293. package/dist/schemas/index.js.map +0 -1
  294. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
  295. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
  296. package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
  297. package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
  298. package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
  299. package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
  300. package/dist/schemas/mdast/MdastSchema.js +0 -266
  301. package/dist/schemas/mdast/MdastSchema.js.map +0 -1
  302. package/dist/schemas/mdast/index.d.ts +0 -10
  303. package/dist/schemas/mdast/index.d.ts.map +0 -1
  304. package/dist/schemas/mdast/index.js +0 -4
  305. package/dist/schemas/mdast/index.js.map +0 -1
  306. package/dist/schemas/mdast/mdastToString.d.ts +0 -13
  307. package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
  308. package/dist/schemas/mdast/mdastToString.js +0 -85
  309. package/dist/schemas/mdast/mdastToString.js.map +0 -1
  310. package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
  311. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
  312. package/dist/schemas/nodes/block/BlockSchema.js +0 -634
  313. package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
  314. package/dist/schemas/nodes/block/index.d.ts +0 -7
  315. package/dist/schemas/nodes/block/index.d.ts.map +0 -1
  316. package/dist/schemas/nodes/block/index.js +0 -7
  317. package/dist/schemas/nodes/block/index.js.map +0 -1
  318. package/dist/schemas/nodes/index.d.ts +0 -9
  319. package/dist/schemas/nodes/index.d.ts.map +0 -1
  320. package/dist/schemas/nodes/index.js +0 -12
  321. package/dist/schemas/nodes/index.js.map +0 -1
  322. package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
  323. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
  324. package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
  325. package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
  326. package/dist/schemas/nodes/inline/index.d.ts +0 -7
  327. package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
  328. package/dist/schemas/nodes/inline/index.js +0 -7
  329. package/dist/schemas/nodes/inline/index.js.map +0 -1
  330. package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
  331. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
  332. package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
  333. package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
  334. package/dist/schemas/nodes/macro/index.d.ts +0 -7
  335. package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
  336. package/dist/schemas/nodes/macro/index.js +0 -7
  337. package/dist/schemas/nodes/macro/index.js.map +0 -1
  338. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -53
  339. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
  340. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -349
  341. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
  342. package/dist/schemas/preprocessing/index.d.ts +0 -8
  343. package/dist/schemas/preprocessing/index.d.ts.map +0 -1
  344. package/dist/schemas/preprocessing/index.js +0 -2
  345. package/dist/schemas/preprocessing/index.js.map +0 -1
  346. package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
  347. package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
  348. package/dist/serializers/ConfluenceSerializer.js +0 -551
  349. package/dist/serializers/ConfluenceSerializer.js.map +0 -1
  350. package/dist/serializers/MarkdownSerializer.d.ts +0 -34
  351. package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
  352. package/dist/serializers/MarkdownSerializer.js +0 -355
  353. package/dist/serializers/MarkdownSerializer.js.map +0 -1
  354. package/dist/serializers/index.d.ts +0 -8
  355. package/dist/serializers/index.d.ts.map +0 -1
  356. package/dist/serializers/index.js +0 -8
  357. package/dist/serializers/index.js.map +0 -1
@@ -75,7 +75,7 @@ describe("MarkdownConverter", () => {
75
75
  expect(md).toContain("const x = 1")
76
76
  }).pipe(Effect.provide(TestLayer)))
77
77
 
78
- it.effect("converts a panel to a GitHub admonition", () =>
78
+ it.effect("converts a panel to a Confluence-preserving placeholder", () =>
79
79
  Effect.gen(function*() {
80
80
  const converter = yield* MarkdownConverter
81
81
  const md = yield* converter.adfToMarkdown(
@@ -85,18 +85,19 @@ describe("MarkdownConverter", () => {
85
85
  content: [{ type: "paragraph", content: [{ type: "text", text: "heads up" }] }]
86
86
  }])
87
87
  )
88
- expect(md).toContain("[!NOTE]")
88
+ expect(md).toContain("<!-- adf:panel type=info attrs=")
89
89
  expect(md).toContain("heads up")
90
+ expect(md).toContain("<!-- adf:/panel -->")
90
91
  }).pipe(Effect.provide(TestLayer)))
91
92
 
92
93
  it.effect("fails with ConversionError on invalid JSON", () =>
93
94
  Effect.gen(function*() {
94
95
  const converter = yield* MarkdownConverter
95
- const result = yield* Effect.either(converter.adfToMarkdown("not json"))
96
- expect(result._tag).toBe("Left")
97
- if (result._tag === "Left") {
98
- expect(result.left._tag).toBe("ConversionError")
99
- expect(result.left.direction).toBe("adfToMarkdown")
96
+ const result = yield* Effect.result(converter.adfToMarkdown("not json"))
97
+ expect(result._tag).toBe("Failure")
98
+ if (result._tag === "Failure") {
99
+ expect(result.failure._tag).toBe("ConversionError")
100
+ expect(result.failure.direction).toBe("adfToMarkdown")
100
101
  }
101
102
  }).pipe(Effect.provide(TestLayer)))
102
103
 
@@ -124,10 +125,10 @@ describe("MarkdownConverter", () => {
124
125
  // page that could overwrite a real local file.
125
126
  const converter = yield* MarkdownConverter
126
127
  for (const bad of ["null", "{}", "[1,2]", `{"type":"doc","content":"not an array"}`]) {
127
- const result = yield* Effect.either(converter.adfToMarkdown(bad))
128
- expect(result._tag).toBe("Left")
129
- if (result._tag === "Left") {
130
- expect(result.left._tag).toBe("ConversionError")
128
+ const result = yield* Effect.result(converter.adfToMarkdown(bad))
129
+ expect(result._tag).toBe("Failure")
130
+ if (result._tag === "Failure") {
131
+ expect(result.failure._tag).toBe("ConversionError")
131
132
  }
132
133
  }
133
134
  }).pipe(Effect.provide(TestLayer)))
@@ -131,12 +131,20 @@ describe("MarkdownConverter round-trip", () => {
131
131
  expect(md).toContain(`<span class="adf-status" data-color="blue">TESTING</span>`)
132
132
  }).pipe(Effect.provide(TestLayer)))
133
133
 
134
- it.effect("upgrades a legacy block extension placeholder to the attrs form, then stays fixed", () =>
134
+ it.effect("upgrades a legacy generic block extension placeholder to the attrs form, then stays fixed", () =>
135
135
  Effect.gen(function*() {
136
136
  const md = yield* roundTrip(
137
- `<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core -->\n`
137
+ `<!-- adf:extension key=anchor type=com.atlassian.confluence.macro.core -->\n`
138
138
  )
139
- expect(md).toContain("<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=")
139
+ expect(md).toContain("<!-- adf:extension key=anchor type=com.atlassian.confluence.macro.core attrs=")
140
+ const again = yield* roundTrip(md)
141
+ expect(again).toBe(md)
142
+ }).pipe(Effect.provide(TestLayer)))
143
+
144
+ it.effect("round-trips native TOC syntax as a fixed point", () =>
145
+ Effect.gen(function*() {
146
+ const md = yield* roundTrip("[[toc:min=2,max=4]]\n")
147
+ expect(md).toContain("[[toc:min=2,max=4]]")
140
148
  const again = yield* roundTrip(md)
141
149
  expect(again).toBe(md)
142
150
  }).pipe(Effect.provide(TestLayer)))
@@ -162,6 +170,34 @@ describe("MarkdownConverter round-trip", () => {
162
170
  expect(adfOut.content[0]).toEqual({ type: "extension", attrs })
163
171
  }).pipe(Effect.provide(TestLayer)))
164
172
 
173
+ it.effect("round-trips Confluence TOC macroMetadata through the placeholder attrs blob", () =>
174
+ Effect.gen(function*() {
175
+ const converter = yield* MarkdownConverter
176
+ const attrs = {
177
+ extensionKey: "toc",
178
+ extensionType: "com.atlassian.confluence.macro.core",
179
+ layout: "default",
180
+ parameters: {
181
+ macroMetadata: {
182
+ schemaVersion: { value: "1" },
183
+ title: "Table of Contents"
184
+ },
185
+ macroParams: {}
186
+ }
187
+ }
188
+ const md = yield* converter.adfToMarkdown(JSON.stringify({
189
+ version: 1,
190
+ type: "doc",
191
+ content: [{ type: "extension", attrs }]
192
+ }))
193
+ expect(md).toContain("<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=")
194
+ expect(md).not.toContain("[[toc")
195
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as {
196
+ content: Array<{ type: string; attrs: Record<string, unknown> }>
197
+ }
198
+ expect(adfOut.content[0]).toEqual({ type: "extension", attrs })
199
+ }).pipe(Effect.provide(TestLayer)))
200
+
165
201
  it.effect("round-trips a bodiedExtension with its body re-attached", () =>
166
202
  Effect.gen(function*() {
167
203
  const converter = yield* MarkdownConverter
@@ -189,6 +225,225 @@ describe("MarkdownConverter round-trip", () => {
189
225
  ])
190
226
  }).pipe(Effect.provide(TestLayer)))
191
227
 
228
+ it.effect("round-trips a Confluence panel as a panel node", () =>
229
+ Effect.gen(function*() {
230
+ const converter = yield* MarkdownConverter
231
+ const attrs = { panelType: "warning" }
232
+ const md = yield* converter.adfToMarkdown(JSON.stringify({
233
+ version: 1,
234
+ type: "doc",
235
+ content: [{
236
+ type: "panel",
237
+ attrs,
238
+ content: [{ type: "paragraph", content: [{ type: "text", text: "watch this" }] }]
239
+ }]
240
+ }))
241
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as {
242
+ content: Array<{ type: string; attrs: Record<string, unknown>; content: Array<unknown> }>
243
+ }
244
+ expect(adfOut.content[0]).toEqual({
245
+ type: "panel",
246
+ attrs,
247
+ content: [{ type: "paragraph", content: [{ type: "text", text: "watch this" }] }]
248
+ })
249
+ }).pipe(Effect.provide(TestLayer)))
250
+
251
+ it.effect("round-trips Confluence-only inline marks as native marks", () =>
252
+ Effect.gen(function*() {
253
+ const converter = yield* MarkdownConverter
254
+ const source = {
255
+ version: 1,
256
+ type: "doc",
257
+ content: [{
258
+ type: "paragraph",
259
+ content: [
260
+ { type: "text", text: "underline", marks: [{ type: "underline" }] },
261
+ { type: "text", text: "2", marks: [{ type: "subsup", attrs: { type: "sub" } }] },
262
+ { type: "text", text: "2", marks: [{ type: "subsup", attrs: { type: "sup" } }] },
263
+ { type: "text", text: "Colored", marks: [{ type: "textColor", attrs: { color: "#ff5630" } }] },
264
+ { type: "text", text: "highlighted", marks: [{ type: "backgroundColor", attrs: { color: "#f8e6a0" } }] }
265
+ ]
266
+ }]
267
+ }
268
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
269
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
270
+ expect(adfOut.content[0]).toEqual(source.content[0])
271
+ }).pipe(Effect.provide(TestLayer)))
272
+
273
+ it.effect("round-trips paragraph alignment and indentation marks", () =>
274
+ Effect.gen(function*() {
275
+ const converter = yield* MarkdownConverter
276
+ const source = {
277
+ version: 1,
278
+ type: "doc",
279
+ content: [
280
+ {
281
+ type: "paragraph",
282
+ marks: [{ type: "alignment", attrs: { align: "center" } }],
283
+ content: [{ type: "text", text: "centered" }]
284
+ },
285
+ {
286
+ type: "paragraph",
287
+ marks: [{ type: "alignment", attrs: { align: "end" } }],
288
+ content: [{ type: "text", text: "right" }]
289
+ },
290
+ {
291
+ type: "paragraph",
292
+ marks: [{ type: "indentation", attrs: { level: 2 } }],
293
+ content: [{ type: "text", text: "indented" }]
294
+ }
295
+ ]
296
+ }
297
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
298
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
299
+ expect(adfOut.content).toEqual(source.content)
300
+ }).pipe(Effect.provide(TestLayer)))
301
+
302
+ it.effect("round-trips inline smart links as native inlineCard nodes", () =>
303
+ Effect.gen(function*() {
304
+ const converter = yield* MarkdownConverter
305
+ const source = {
306
+ version: 1,
307
+ type: "doc",
308
+ content: [{
309
+ type: "paragraph",
310
+ content: [
311
+ { type: "text", text: "Inline smart link: " },
312
+ { type: "inlineCard", attrs: { url: "https://www.atlassian.com" } },
313
+ { type: "text", text: "." }
314
+ ]
315
+ }]
316
+ }
317
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
318
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
319
+ expect(adfOut.content[0]).toEqual(source.content[0])
320
+ }).pipe(Effect.provide(TestLayer)))
321
+
322
+ it.effect("round-trips task lists as native task nodes", () =>
323
+ Effect.gen(function*() {
324
+ const converter = yield* MarkdownConverter
325
+ const source = {
326
+ version: 1,
327
+ type: "doc",
328
+ content: [{
329
+ type: "taskList",
330
+ attrs: { localId: "tasks-1" },
331
+ content: [
332
+ {
333
+ type: "taskItem",
334
+ attrs: { localId: "task-1", state: "DONE" },
335
+ content: [{ type: "text", text: "Existing primitive coverage reviewed" }]
336
+ },
337
+ {
338
+ type: "taskItem",
339
+ attrs: { localId: "task-2", state: "TODO" },
340
+ content: [{ type: "text", text: "Insert real @mention in editor" }]
341
+ }
342
+ ]
343
+ }]
344
+ }
345
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
346
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
347
+ expect(adfOut.content[0]).toEqual(source.content[0])
348
+ }).pipe(Effect.provide(TestLayer)))
349
+
350
+ it.effect("round-trips decision lists as native decision nodes", () =>
351
+ Effect.gen(function*() {
352
+ const converter = yield* MarkdownConverter
353
+ const source = {
354
+ version: 1,
355
+ type: "doc",
356
+ content: [{
357
+ type: "decisionList",
358
+ attrs: { localId: "decisions-1" },
359
+ content: [{
360
+ type: "decisionItem",
361
+ attrs: { localId: "decision-1", state: "DECIDED" },
362
+ content: [{ type: "text", text: "Decide whether to maintain a separate asset for advanced macros." }]
363
+ }]
364
+ }]
365
+ }
366
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
367
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
368
+ expect(adfOut.content[0]).toEqual(source.content[0])
369
+ }).pipe(Effect.provide(TestLayer)))
370
+
371
+ it.effect("round-trips expand, table, layout, cards, date, and emoji as native nodes", () =>
372
+ Effect.gen(function*() {
373
+ const converter = yield* MarkdownConverter
374
+ const source = {
375
+ version: 1,
376
+ type: "doc",
377
+ content: [
378
+ {
379
+ type: "expand",
380
+ attrs: { title: "Expandable supplementary content" },
381
+ content: [{ type: "paragraph", content: [{ type: "text", text: "This section can be expanded." }] }]
382
+ },
383
+ {
384
+ type: "table",
385
+ content: [{
386
+ type: "tableRow",
387
+ content: [
388
+ {
389
+ type: "tableHeader",
390
+ content: [{ type: "paragraph", content: [{ type: "text", text: "Primitive" }] }]
391
+ },
392
+ {
393
+ type: "tableHeader",
394
+ content: [{ type: "paragraph", content: [{ type: "text", text: "Example" }] }]
395
+ }
396
+ ]
397
+ }, {
398
+ type: "tableRow",
399
+ content: [
400
+ { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "Date" }] }] },
401
+ {
402
+ type: "tableCell",
403
+ content: [{
404
+ type: "paragraph",
405
+ content: [{ type: "date", attrs: { timestamp: "1782259200000" } }]
406
+ }]
407
+ }
408
+ ]
409
+ }, {
410
+ type: "tableRow",
411
+ content: [
412
+ { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "Emoji" }] }] },
413
+ {
414
+ type: "tableCell",
415
+ content: [{
416
+ type: "paragraph",
417
+ content: [{ type: "emoji", attrs: { shortName: ":white_check_mark:", text: "✅" } }]
418
+ }]
419
+ }
420
+ ]
421
+ }]
422
+ },
423
+ {
424
+ type: "layoutSection",
425
+ content: [
426
+ {
427
+ type: "layoutColumn",
428
+ attrs: { width: 50 },
429
+ content: [{ type: "paragraph", content: [{ type: "text", text: "Left column" }] }]
430
+ },
431
+ {
432
+ type: "layoutColumn",
433
+ attrs: { width: 50 },
434
+ content: [{ type: "paragraph", content: [{ type: "text", text: "Right column" }] }]
435
+ }
436
+ ]
437
+ },
438
+ { type: "blockCard", attrs: { url: "https://www.atlassian.com/software/confluence" } },
439
+ { type: "embedCard", attrs: { url: "https://www.atlassian.com/software/confluence", layout: "center" } }
440
+ ]
441
+ }
442
+ const md = yield* converter.adfToMarkdown(JSON.stringify(source))
443
+ const adfOut = JSON.parse(yield* converter.markdownToAdf(md)) as typeof source
444
+ expect(adfOut.content).toEqual(source.content)
445
+ }).pipe(Effect.provide(TestLayer)))
446
+
192
447
  // Regression: `|` in a cell was escaped twice (escapeText + the table-cell
193
448
  // pass), emitting `\\|` — GFM reads that as literal backslash + bare pipe,
194
449
  // which opens a phantom column and breaks the row.
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
- import * as Either from "effect/Either"
2
+ import * as Result from "effect/Result"
3
3
  import * as Schema from "effect/Schema"
4
4
  import type { ContentHash, PageId } from "../src/Brand.js"
5
5
  import {
@@ -17,11 +17,11 @@ describe("Schemas", () => {
17
17
  rootPageId: "123456",
18
18
  baseUrl: "https://mysite.atlassian.net"
19
19
  }
20
- const result = Schema.decodeUnknownEither(ConfluenceConfigFileSchema)(config)
21
- expect(Either.isRight(result)).toBe(true)
22
- if (Either.isRight(result)) {
23
- expect(result.right.docsPath).toBe(".confluence/docs")
24
- expect(result.right.excludePatterns).toEqual([])
20
+ const result = Schema.decodeUnknownResult(ConfluenceConfigFileSchema)(config)
21
+ expect(Result.isSuccess(result)).toBe(true)
22
+ if (Result.isSuccess(result)) {
23
+ expect(result.success.docsPath).toBe(".confluence/docs")
24
+ expect(result.success.excludePatterns).toEqual([])
25
25
  }
26
26
  })
27
27
 
@@ -33,11 +33,11 @@ describe("Schemas", () => {
33
33
  docsPath: "docs",
34
34
  excludePatterns: ["*.tmp"]
35
35
  }
36
- const result = Schema.decodeUnknownEither(ConfluenceConfigFileSchema)(config)
37
- expect(Either.isRight(result)).toBe(true)
38
- if (Either.isRight(result)) {
39
- expect(result.right.spaceKey).toBe("DEV")
40
- expect(result.right.docsPath).toBe("docs")
36
+ const result = Schema.decodeUnknownResult(ConfluenceConfigFileSchema)(config)
37
+ expect(Result.isSuccess(result)).toBe(true)
38
+ if (Result.isSuccess(result)) {
39
+ expect(result.success.spaceKey).toBe("DEV")
40
+ expect(result.success.docsPath).toBe("docs")
41
41
  }
42
42
  })
43
43
 
@@ -46,14 +46,14 @@ describe("Schemas", () => {
46
46
  rootPageId: "123456",
47
47
  baseUrl: "http://invalid.com"
48
48
  }
49
- const result = Schema.decodeUnknownEither(ConfluenceConfigFileSchema)(config)
50
- expect(Either.isLeft(result)).toBe(true)
49
+ const result = Schema.decodeUnknownResult(ConfluenceConfigFileSchema)(config)
50
+ expect(Result.isFailure(result)).toBe(true)
51
51
  })
52
52
 
53
53
  it("rejects missing required fields", () => {
54
54
  const config = { baseUrl: "https://mysite.atlassian.net" }
55
- const result = Schema.decodeUnknownEither(ConfluenceConfigFileSchema)(config)
56
- expect(Either.isLeft(result)).toBe(true)
55
+ const result = Schema.decodeUnknownResult(ConfluenceConfigFileSchema)(config)
56
+ expect(Result.isFailure(result)).toBe(true)
57
57
  })
58
58
  })
59
59
 
@@ -68,8 +68,8 @@ describe("Schemas", () => {
68
68
  updated: new Date().toISOString(),
69
69
  contentHash: validHash
70
70
  }
71
- const result = Schema.decodeUnknownEither(PageFrontMatterSchema)(fm)
72
- expect(Either.isRight(result)).toBe(true)
71
+ const result = Schema.decodeUnknownResult(PageFrontMatterSchema)(fm)
72
+ expect(Result.isSuccess(result)).toBe(true)
73
73
  })
74
74
 
75
75
  it("decodes front matter with optional fields", () => {
@@ -82,11 +82,11 @@ describe("Schemas", () => {
82
82
  position: 0,
83
83
  contentHash: validHash
84
84
  }
85
- const result = Schema.decodeUnknownEither(PageFrontMatterSchema)(fm)
86
- expect(Either.isRight(result)).toBe(true)
87
- if (Either.isRight(result)) {
88
- expect(result.right.parentId).toBe("456")
89
- expect(result.right.position).toBe(0)
85
+ const result = Schema.decodeUnknownResult(PageFrontMatterSchema)(fm)
86
+ expect(Result.isSuccess(result)).toBe(true)
87
+ if (Result.isSuccess(result)) {
88
+ expect(result.success.parentId).toBe("456")
89
+ expect(result.success.position).toBe(0)
90
90
  }
91
91
  })
92
92
 
@@ -98,8 +98,8 @@ describe("Schemas", () => {
98
98
  updated: new Date().toISOString(),
99
99
  contentHash: validHash
100
100
  }
101
- const result = Schema.decodeUnknownEither(PageFrontMatterSchema)(fm)
102
- expect(Either.isLeft(result)).toBe(true)
101
+ const result = Schema.decodeUnknownResult(PageFrontMatterSchema)(fm)
102
+ expect(Result.isFailure(result)).toBe(true)
103
103
  })
104
104
  })
105
105
 
@@ -113,8 +113,8 @@ describe("Schemas", () => {
113
113
  cloud_id: "abc123",
114
114
  site_url: "https://mysite.atlassian.net"
115
115
  }
116
- const result = Schema.decodeUnknownEither(OAuthTokenSchema)(token)
117
- expect(Either.isRight(result)).toBe(true)
116
+ const result = Schema.decodeUnknownResult(OAuthTokenSchema)(token)
117
+ expect(Result.isSuccess(result)).toBe(true)
118
118
  })
119
119
 
120
120
  it("decodes token with user info", () => {
@@ -131,10 +131,10 @@ describe("Schemas", () => {
131
131
  email: "test@example.com"
132
132
  }
133
133
  }
134
- const result = Schema.decodeUnknownEither(OAuthTokenSchema)(token)
135
- expect(Either.isRight(result)).toBe(true)
136
- if (Either.isRight(result)) {
137
- expect(result.right.user?.name).toBe("Test User")
134
+ const result = Schema.decodeUnknownResult(OAuthTokenSchema)(token)
135
+ expect(Result.isSuccess(result)).toBe(true)
136
+ if (Result.isSuccess(result)) {
137
+ expect(result.success.user?.name).toBe("Test User")
138
138
  }
139
139
  })
140
140
 
@@ -147,8 +147,8 @@ describe("Schemas", () => {
147
147
  cloud_id: "abc123",
148
148
  site_url: "https://mysite.atlassian.net"
149
149
  }
150
- const result = Schema.decodeUnknownEither(OAuthTokenSchema)(token)
151
- expect(Either.isLeft(result)).toBe(true)
150
+ const result = Schema.decodeUnknownResult(OAuthTokenSchema)(token)
151
+ expect(Result.isFailure(result)).toBe(true)
152
152
  })
153
153
  })
154
154
 
@@ -158,16 +158,16 @@ describe("Schemas", () => {
158
158
  clientId: "client_id_value",
159
159
  clientSecret: "client_secret_value"
160
160
  }
161
- const result = Schema.decodeUnknownEither(OAuthConfigSchema)(config)
162
- expect(Either.isRight(result)).toBe(true)
161
+ const result = Schema.decodeUnknownResult(OAuthConfigSchema)(config)
162
+ expect(Result.isSuccess(result)).toBe(true)
163
163
  })
164
164
 
165
165
  it("rejects missing clientSecret", () => {
166
166
  const config = {
167
167
  clientId: "client_id_value"
168
168
  }
169
- const result = Schema.decodeUnknownEither(OAuthConfigSchema)(config)
170
- expect(Either.isLeft(result)).toBe(true)
169
+ const result = Schema.decodeUnknownResult(OAuthConfigSchema)(config)
170
+ expect(Result.isFailure(result)).toBe(true)
171
171
  })
172
172
  })
173
173
 
@@ -178,8 +178,8 @@ describe("Schemas", () => {
178
178
  name: "Test User",
179
179
  email: "test@example.com"
180
180
  }
181
- const result = Schema.decodeUnknownEither(OAuthUserSchema)(user)
182
- expect(Either.isRight(result)).toBe(true)
181
+ const result = Schema.decodeUnknownResult(OAuthUserSchema)(user)
182
+ expect(Result.isSuccess(result)).toBe(true)
183
183
  })
184
184
 
185
185
  it("rejects missing email", () => {
@@ -187,8 +187,8 @@ describe("Schemas", () => {
187
187
  account_id: "user123",
188
188
  name: "Test User"
189
189
  }
190
- const result = Schema.decodeUnknownEither(OAuthUserSchema)(user)
191
- expect(Either.isLeft(result)).toBe(true)
190
+ const result = Schema.decodeUnknownResult(OAuthUserSchema)(user)
191
+ expect(Result.isFailure(result)).toBe(true)
192
192
  })
193
193
  })
194
194
  })
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { collectAdfMetadataHrefs, externalizeAdfMetadata, hydrateAdfMetadata } from "../src/internal/adfMetadata.js"
3
+
4
+ const toBase64 = (text: string): string => {
5
+ const bytes = new TextEncoder().encode(text)
6
+ return btoa(String.fromCharCode(...bytes))
7
+ }
8
+
9
+ describe("ADF metadata sidecars", () => {
10
+ it("moves decoded placeholder metadata into a linked sidecar", () => {
11
+ const markdown = [
12
+ `<!-- adf:panel type=info attrs={"panelType":"info"} -->`,
13
+ "",
14
+ "Body",
15
+ "",
16
+ "<!-- adf:/panel -->",
17
+ "",
18
+ `Regular link: <!-- adf:inlineCard attrs={"url":"https://www.atlassian.com"} -->.`
19
+ ].join("\n")
20
+
21
+ const prepared = externalizeAdfMetadata(markdown, "./123.adf.json")
22
+
23
+ expect(prepared.markdown).toContain("<!-- adf:panel type=info ref=./123.adf.json#panel-1 -->")
24
+ expect(prepared.markdown).toContain("<!-- adf:inlineCard ref=./123.adf.json#inlineCard-2 -->")
25
+ expect(prepared.markdown).not.toContain("attrs={")
26
+ expect(prepared.sidecar).toEqual({
27
+ version: 1,
28
+ entries: {
29
+ "panel-1": {
30
+ kind: "attrs",
31
+ value: { panelType: "info" }
32
+ },
33
+ "inlineCard-2": {
34
+ kind: "attrs",
35
+ value: { url: "https://www.atlassian.com" }
36
+ }
37
+ }
38
+ })
39
+ })
40
+
41
+ it("moves base64 placeholder metadata into a linked sidecar", () => {
42
+ const attrs = {
43
+ extensionKey: "toc",
44
+ extensionType: "com.atlassian.confluence.macro.core",
45
+ parameters: {
46
+ macroMetadata: {
47
+ title: "Table of Contents"
48
+ }
49
+ }
50
+ }
51
+ const encodedAttrs = toBase64(JSON.stringify(attrs))
52
+ const markdown =
53
+ `| **On this page** | <!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=${encodedAttrs} --> |`
54
+
55
+ const prepared = externalizeAdfMetadata(markdown, "./2731114497.adf.json")
56
+
57
+ expect(prepared.markdown).toBe(
58
+ "| **On this page** | <!-- adf:extension key=toc type=com.atlassian.confluence.macro.core ref=./2731114497.adf.json#extension-1 --> |"
59
+ )
60
+ expect(prepared.markdown).not.toContain(encodedAttrs)
61
+ expect(prepared.sidecar).toEqual({
62
+ version: 1,
63
+ entries: {
64
+ "extension-1": {
65
+ kind: "attrs",
66
+ value: attrs
67
+ }
68
+ }
69
+ })
70
+ })
71
+
72
+ it("hydrates linked sidecar metadata back into placeholders for push", () => {
73
+ const markdown = `<!-- adf:taskList ref=./123.adf.json#taskList-1 -->`
74
+ const hydrated = hydrateAdfMetadata(
75
+ markdown,
76
+ new Map([
77
+ [
78
+ "./123.adf.json",
79
+ {
80
+ version: 1,
81
+ entries: {
82
+ "taskList-1": {
83
+ kind: "node",
84
+ value: {
85
+ type: "taskList",
86
+ attrs: { localId: "task-list" },
87
+ content: [{ type: "taskItem", attrs: { state: "DONE" } }]
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ]
93
+ ])
94
+ )
95
+
96
+ expect(hydrated).toBe(
97
+ `<!-- adf:taskList node={"attrs":{"localId":"task-list"},"content":[{"attrs":{"state":"DONE"},"type":"taskItem"}],"type":"taskList"} -->`
98
+ )
99
+ })
100
+
101
+ it("collects each referenced sidecar href once", () => {
102
+ const hrefs = collectAdfMetadataHrefs([
103
+ "<!-- adf:panel ref=./123.adf.json#panel-1 -->",
104
+ "<!-- adf:taskList ref=./123.adf.json#taskList-2 -->",
105
+ "<!-- adf:panel ref=./456.adf.json#panel-1 -->"
106
+ ].join("\n"))
107
+
108
+ expect([...hrefs].sort()).toEqual(["./123.adf.json", "./456.adf.json"])
109
+ })
110
+ })
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import { cleanMarkdown } from "../src/internal/cleanMarkdown.js"
3
+
4
+ describe("cleanMarkdown", () => {
5
+ it("removes full-line ADF metadata comments", () => {
6
+ const markdown = [
7
+ "# Title",
8
+ "",
9
+ "<!-- adf:panel type=info attrs={} -->",
10
+ "",
11
+ "Body",
12
+ "",
13
+ "<!-- adf:/panel -->"
14
+ ].join("\n")
15
+
16
+ expect(cleanMarkdown(markdown)).toBe("# Title\n\nBody\n")
17
+ })
18
+
19
+ it("removes inline ADF metadata comments", () => {
20
+ expect(cleanMarkdown("Card <!-- adf:inlineCard attrs={} --> text")).toBe("Card text\n")
21
+ })
22
+
23
+ it("keeps regular markdown, native macros, status spans, and non-ADF comments", () => {
24
+ const markdown = [
25
+ "[[toc]]",
26
+ "",
27
+ "<span data-adf-status=\"todo\" data-adf-color=\"blue\">TODO</span>",
28
+ "",
29
+ "<!-- keep this comment -->",
30
+ "",
31
+ "- item"
32
+ ].join("\n")
33
+
34
+ expect(cleanMarkdown(markdown)).toBe(`${markdown}\n`)
35
+ })
36
+ })