@knpkv/confluence-to-markdown 0.5.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 (395) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +58 -14
  3. package/dist/AdfPlaceholders.d.ts +42 -0
  4. package/dist/AdfPlaceholders.d.ts.map +1 -0
  5. package/dist/AdfPlaceholders.js +547 -0
  6. package/dist/AdfPlaceholders.js.map +1 -0
  7. package/dist/AdfSchemaValidator.d.ts +37 -0
  8. package/dist/AdfSchemaValidator.d.ts.map +1 -0
  9. package/dist/AdfSchemaValidator.js +37 -0
  10. package/dist/AdfSchemaValidator.js.map +1 -0
  11. package/dist/AdfWalker.d.ts +39 -0
  12. package/dist/AdfWalker.d.ts.map +1 -0
  13. package/dist/AdfWalker.js +527 -0
  14. package/dist/AdfWalker.js.map +1 -0
  15. package/dist/AtlaskitTransformers.d.ts +35 -0
  16. package/dist/AtlaskitTransformers.d.ts.map +1 -0
  17. package/dist/AtlaskitTransformers.js +48 -0
  18. package/dist/AtlaskitTransformers.js.map +1 -0
  19. package/dist/Brand.d.ts +6 -6
  20. package/dist/Brand.d.ts.map +1 -1
  21. package/dist/Brand.js +8 -6
  22. package/dist/Brand.js.map +1 -1
  23. package/dist/ConfluenceAuth.d.ts +4 -4
  24. package/dist/ConfluenceAuth.d.ts.map +1 -1
  25. package/dist/ConfluenceAuth.js +15 -27
  26. package/dist/ConfluenceAuth.js.map +1 -1
  27. package/dist/ConfluenceClient.d.ts +4 -4
  28. package/dist/ConfluenceClient.d.ts.map +1 -1
  29. package/dist/ConfluenceClient.js +21 -14
  30. package/dist/ConfluenceClient.js.map +1 -1
  31. package/dist/ConfluenceConfig.d.ts +3 -3
  32. package/dist/ConfluenceConfig.d.ts.map +1 -1
  33. package/dist/ConfluenceConfig.js +13 -11
  34. package/dist/ConfluenceConfig.js.map +1 -1
  35. package/dist/ConfluenceError.d.ts +56 -4
  36. package/dist/ConfluenceError.d.ts.map +1 -1
  37. package/dist/ConfluenceError.js +30 -1
  38. package/dist/ConfluenceError.js.map +1 -1
  39. package/dist/GitService.d.ts +11 -3
  40. package/dist/GitService.d.ts.map +1 -1
  41. package/dist/GitService.js +19 -27
  42. package/dist/GitService.js.map +1 -1
  43. package/dist/LocalFileSystem.d.ts +3 -3
  44. package/dist/LocalFileSystem.d.ts.map +1 -1
  45. package/dist/LocalFileSystem.js +6 -6
  46. package/dist/LocalFileSystem.js.map +1 -1
  47. package/dist/MarkdownConverter.d.ts +16 -65
  48. package/dist/MarkdownConverter.d.ts.map +1 -1
  49. package/dist/MarkdownConverter.js +64 -85
  50. package/dist/MarkdownConverter.js.map +1 -1
  51. package/dist/Schemas.d.ts +128 -141
  52. package/dist/Schemas.d.ts.map +1 -1
  53. package/dist/Schemas.js +21 -23
  54. package/dist/Schemas.js.map +1 -1
  55. package/dist/SyncEngine.d.ts +8 -5
  56. package/dist/SyncEngine.d.ts.map +1 -1
  57. package/dist/SyncEngine.js +189 -113
  58. package/dist/SyncEngine.js.map +1 -1
  59. package/dist/bin.js +23 -35
  60. package/dist/bin.js.map +1 -1
  61. package/dist/commands/auth.d.ts +2 -14
  62. package/dist/commands/auth.d.ts.map +1 -1
  63. package/dist/commands/auth.js +11 -16
  64. package/dist/commands/auth.js.map +1 -1
  65. package/dist/commands/clone.d.ts +4 -6
  66. package/dist/commands/clone.d.ts.map +1 -1
  67. package/dist/commands/clone.js +34 -32
  68. package/dist/commands/clone.js.map +1 -1
  69. package/dist/commands/delete.d.ts +2 -10
  70. package/dist/commands/delete.d.ts.map +1 -1
  71. package/dist/commands/delete.js +5 -4
  72. package/dist/commands/delete.js.map +1 -1
  73. package/dist/commands/errorHandler.d.ts +2 -1
  74. package/dist/commands/errorHandler.d.ts.map +1 -1
  75. package/dist/commands/errorHandler.js +22 -15
  76. package/dist/commands/errorHandler.js.map +1 -1
  77. package/dist/commands/fetch.d.ts +27 -0
  78. package/dist/commands/fetch.d.ts.map +1 -0
  79. package/dist/commands/fetch.js +48 -0
  80. package/dist/commands/fetch.js.map +1 -0
  81. package/dist/commands/git.d.ts +7 -10
  82. package/dist/commands/git.d.ts.map +1 -1
  83. package/dist/commands/git.js +6 -6
  84. package/dist/commands/git.js.map +1 -1
  85. package/dist/commands/index.d.ts +1 -0
  86. package/dist/commands/index.d.ts.map +1 -1
  87. package/dist/commands/index.js +1 -0
  88. package/dist/commands/index.js.map +1 -1
  89. package/dist/commands/layers.d.ts +10 -9
  90. package/dist/commands/layers.d.ts.map +1 -1
  91. package/dist/commands/layers.js +41 -30
  92. package/dist/commands/layers.js.map +1 -1
  93. package/dist/commands/new.d.ts +2 -6
  94. package/dist/commands/new.d.ts.map +1 -1
  95. package/dist/commands/new.js +5 -4
  96. package/dist/commands/new.js.map +1 -1
  97. package/dist/commands/pageInput.d.ts +19 -0
  98. package/dist/commands/pageInput.d.ts.map +1 -0
  99. package/dist/commands/pageInput.js +68 -0
  100. package/dist/commands/pageInput.js.map +1 -0
  101. package/dist/commands/root.d.ts +8 -0
  102. package/dist/commands/root.d.ts.map +1 -0
  103. package/dist/commands/root.js +29 -0
  104. package/dist/commands/root.js.map +1 -0
  105. package/dist/commands/sync.d.ts +6 -9
  106. package/dist/commands/sync.d.ts.map +1 -1
  107. package/dist/commands/sync.js +5 -6
  108. package/dist/commands/sync.js.map +1 -1
  109. package/dist/index.d.ts +3 -8
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +2 -13
  112. package/dist/index.js.map +1 -1
  113. package/dist/internal/NodeLayers.d.ts.map +1 -1
  114. package/dist/internal/NodeLayers.js +1 -2
  115. package/dist/internal/NodeLayers.js.map +1 -1
  116. package/dist/internal/adfMetadata.d.ts +30 -0
  117. package/dist/internal/adfMetadata.d.ts.map +1 -0
  118. package/dist/internal/adfMetadata.js +126 -0
  119. package/dist/internal/adfMetadata.js.map +1 -0
  120. package/dist/internal/cleanMarkdown.d.ts +5 -0
  121. package/dist/internal/cleanMarkdown.d.ts.map +1 -0
  122. package/dist/internal/cleanMarkdown.js +13 -0
  123. package/dist/internal/cleanMarkdown.js.map +1 -0
  124. package/dist/internal/frontmatter.d.ts.map +1 -1
  125. package/dist/internal/frontmatter.js +41 -8
  126. package/dist/internal/frontmatter.js.map +1 -1
  127. package/dist/internal/gitCommands.d.ts +9 -3
  128. package/dist/internal/gitCommands.d.ts.map +1 -1
  129. package/dist/internal/gitCommands.js +18 -9
  130. package/dist/internal/gitCommands.js.map +1 -1
  131. package/dist/internal/hashUtils.d.ts +1 -1
  132. package/dist/internal/hashUtils.d.ts.map +1 -1
  133. package/dist/internal/hashUtils.js +1 -1
  134. package/dist/internal/hashUtils.js.map +1 -1
  135. package/dist/internal/oauthServer.d.ts +10 -5
  136. package/dist/internal/oauthServer.d.ts.map +1 -1
  137. package/dist/internal/oauthServer.js +19 -40
  138. package/dist/internal/oauthServer.js.map +1 -1
  139. package/dist/internal/pathUtils.d.ts +1 -1
  140. package/dist/internal/pathUtils.d.ts.map +1 -1
  141. package/dist/internal/pathUtils.js +1 -1
  142. package/dist/internal/pathUtils.js.map +1 -1
  143. package/dist/internal/process.d.ts +15 -0
  144. package/dist/internal/process.d.ts.map +1 -0
  145. package/dist/internal/process.js +10 -0
  146. package/dist/internal/process.js.map +1 -0
  147. package/dist/internal/stdio.d.ts +6 -0
  148. package/dist/internal/stdio.d.ts.map +1 -0
  149. package/dist/internal/stdio.js +15 -0
  150. package/dist/internal/stdio.js.map +1 -0
  151. package/dist/internal/tokenStorage.d.ts +3 -13
  152. package/dist/internal/tokenStorage.d.ts.map +1 -1
  153. package/dist/internal/tokenStorage.js +26 -24
  154. package/dist/internal/tokenStorage.js.map +1 -1
  155. package/dist/internal/userCache.d.ts +1 -1
  156. package/dist/internal/userCache.d.ts.map +1 -1
  157. package/dist/internal/userCache.js +1 -1
  158. package/dist/internal/userCache.js.map +1 -1
  159. package/package.json +29 -20
  160. package/skills/confluence/SKILL.md +143 -0
  161. package/skills/confluence/agents/openai.yaml +4 -0
  162. package/src/AdfPlaceholders.ts +563 -0
  163. package/src/AdfSchemaValidator.ts +65 -0
  164. package/src/AdfWalker.ts +591 -0
  165. package/src/AtlaskitTransformers.ts +70 -0
  166. package/src/Brand.ts +11 -16
  167. package/src/ConfluenceAuth.ts +22 -30
  168. package/src/ConfluenceClient.ts +28 -24
  169. package/src/ConfluenceConfig.ts +14 -14
  170. package/src/ConfluenceError.ts +65 -3
  171. package/src/GitService.ts +39 -49
  172. package/src/LocalFileSystem.ts +7 -9
  173. package/src/MarkdownConverter.ts +108 -143
  174. package/src/Schemas.ts +17 -16
  175. package/src/SyncEngine.ts +272 -127
  176. package/src/atlaskit-adf-schema.d.ts +3 -0
  177. package/src/bin.ts +30 -56
  178. package/src/commands/auth.ts +21 -18
  179. package/src/commands/clone.ts +46 -38
  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 +64 -37
  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/index.ts +3 -18
  191. package/src/internal/NodeLayers.ts +1 -2
  192. package/src/internal/adfMetadata.ts +145 -0
  193. package/src/internal/cleanMarkdown.ts +15 -0
  194. package/src/internal/frontmatter.ts +45 -8
  195. package/src/internal/gitCommands.ts +23 -17
  196. package/src/internal/hashUtils.ts +2 -2
  197. package/src/internal/oauthServer.ts +84 -105
  198. package/src/internal/pathUtils.ts +1 -1
  199. package/src/internal/process.ts +15 -0
  200. package/src/internal/stdio.ts +22 -0
  201. package/src/internal/tokenStorage.ts +39 -29
  202. package/src/internal/userCache.ts +2 -2
  203. package/test/AdfPlaceholders.test.ts +508 -0
  204. package/test/AdfSchemaValidator.test.ts +34 -0
  205. package/test/AdfWalker.test.ts +676 -0
  206. package/test/AtlaskitTransformers.test.ts +25 -0
  207. package/test/Brand.test.ts +11 -11
  208. package/test/GitService.test.ts +6 -2
  209. package/test/MarkdownConverter.test.ts +121 -105
  210. package/test/RoundTrip.test.ts +521 -0
  211. package/test/Schemas.test.ts +40 -40
  212. package/test/adfMetadata.test.ts +110 -0
  213. package/test/cleanMarkdown.test.ts +36 -0
  214. package/test/commandHarness.test.ts +79 -0
  215. package/test/commandHarness.ts +147 -0
  216. package/test/fetch.test.ts +61 -0
  217. package/test/frontmatter.test.ts +41 -0
  218. package/test/integration.test.ts +569 -156
  219. package/test/layers.test.ts +12 -0
  220. package/test/oauthServer.test.ts +4 -5
  221. package/test/pageInput.test.ts +83 -0
  222. package/test/tokenStorage.test.ts +17 -17
  223. package/dist/SchemaConverterError.d.ts +0 -108
  224. package/dist/SchemaConverterError.d.ts.map +0 -1
  225. package/dist/SchemaConverterError.js +0 -84
  226. package/dist/SchemaConverterError.js.map +0 -1
  227. package/dist/ast/BlockNode.d.ts +0 -453
  228. package/dist/ast/BlockNode.d.ts.map +0 -1
  229. package/dist/ast/BlockNode.js +0 -310
  230. package/dist/ast/BlockNode.js.map +0 -1
  231. package/dist/ast/Document.d.ts +0 -216
  232. package/dist/ast/Document.d.ts.map +0 -1
  233. package/dist/ast/Document.js +0 -69
  234. package/dist/ast/Document.js.map +0 -1
  235. package/dist/ast/InlineNode.d.ts +0 -477
  236. package/dist/ast/InlineNode.d.ts.map +0 -1
  237. package/dist/ast/InlineNode.js +0 -263
  238. package/dist/ast/InlineNode.js.map +0 -1
  239. package/dist/ast/MacroNode.d.ts +0 -267
  240. package/dist/ast/MacroNode.d.ts.map +0 -1
  241. package/dist/ast/MacroNode.js +0 -164
  242. package/dist/ast/MacroNode.js.map +0 -1
  243. package/dist/ast/index.d.ts +0 -10
  244. package/dist/ast/index.d.ts.map +0 -1
  245. package/dist/ast/index.js +0 -14
  246. package/dist/ast/index.js.map +0 -1
  247. package/dist/parsers/ConfluenceParser.d.ts +0 -26
  248. package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
  249. package/dist/parsers/ConfluenceParser.js +0 -797
  250. package/dist/parsers/ConfluenceParser.js.map +0 -1
  251. package/dist/parsers/MarkdownParser.d.ts +0 -26
  252. package/dist/parsers/MarkdownParser.d.ts.map +0 -1
  253. package/dist/parsers/MarkdownParser.js +0 -982
  254. package/dist/parsers/MarkdownParser.js.map +0 -1
  255. package/dist/parsers/index.d.ts +0 -8
  256. package/dist/parsers/index.d.ts.map +0 -1
  257. package/dist/parsers/index.js +0 -8
  258. package/dist/parsers/index.js.map +0 -1
  259. package/dist/schemas/ConfluenceSchema.d.ts +0 -21
  260. package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
  261. package/dist/schemas/ConfluenceSchema.js +0 -38
  262. package/dist/schemas/ConfluenceSchema.js.map +0 -1
  263. package/dist/schemas/ConversionSchema.d.ts +0 -35
  264. package/dist/schemas/ConversionSchema.d.ts.map +0 -1
  265. package/dist/schemas/ConversionSchema.js +0 -208
  266. package/dist/schemas/ConversionSchema.js.map +0 -1
  267. package/dist/schemas/MarkdownSchema.d.ts +0 -21
  268. package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
  269. package/dist/schemas/MarkdownSchema.js +0 -38
  270. package/dist/schemas/MarkdownSchema.js.map +0 -1
  271. package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
  272. package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
  273. package/dist/schemas/hast/HastFromHtml.js +0 -107
  274. package/dist/schemas/hast/HastFromHtml.js.map +0 -1
  275. package/dist/schemas/hast/HastSchema.d.ts +0 -195
  276. package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
  277. package/dist/schemas/hast/HastSchema.js +0 -183
  278. package/dist/schemas/hast/HastSchema.js.map +0 -1
  279. package/dist/schemas/hast/index.d.ts +0 -9
  280. package/dist/schemas/hast/index.d.ts.map +0 -1
  281. package/dist/schemas/hast/index.js +0 -3
  282. package/dist/schemas/hast/index.js.map +0 -1
  283. package/dist/schemas/index.d.ts +0 -14
  284. package/dist/schemas/index.d.ts.map +0 -1
  285. package/dist/schemas/index.js +0 -16
  286. package/dist/schemas/index.js.map +0 -1
  287. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
  288. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
  289. package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
  290. package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
  291. package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
  292. package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
  293. package/dist/schemas/mdast/MdastSchema.js +0 -266
  294. package/dist/schemas/mdast/MdastSchema.js.map +0 -1
  295. package/dist/schemas/mdast/index.d.ts +0 -10
  296. package/dist/schemas/mdast/index.d.ts.map +0 -1
  297. package/dist/schemas/mdast/index.js +0 -4
  298. package/dist/schemas/mdast/index.js.map +0 -1
  299. package/dist/schemas/mdast/mdastToString.d.ts +0 -13
  300. package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
  301. package/dist/schemas/mdast/mdastToString.js +0 -85
  302. package/dist/schemas/mdast/mdastToString.js.map +0 -1
  303. package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
  304. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
  305. package/dist/schemas/nodes/block/BlockSchema.js +0 -634
  306. package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
  307. package/dist/schemas/nodes/block/index.d.ts +0 -7
  308. package/dist/schemas/nodes/block/index.d.ts.map +0 -1
  309. package/dist/schemas/nodes/block/index.js +0 -7
  310. package/dist/schemas/nodes/block/index.js.map +0 -1
  311. package/dist/schemas/nodes/index.d.ts +0 -9
  312. package/dist/schemas/nodes/index.d.ts.map +0 -1
  313. package/dist/schemas/nodes/index.js +0 -12
  314. package/dist/schemas/nodes/index.js.map +0 -1
  315. package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
  316. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
  317. package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
  318. package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
  319. package/dist/schemas/nodes/inline/index.d.ts +0 -7
  320. package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
  321. package/dist/schemas/nodes/inline/index.js +0 -7
  322. package/dist/schemas/nodes/inline/index.js.map +0 -1
  323. package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
  324. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
  325. package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
  326. package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
  327. package/dist/schemas/nodes/macro/index.d.ts +0 -7
  328. package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
  329. package/dist/schemas/nodes/macro/index.js +0 -7
  330. package/dist/schemas/nodes/macro/index.js.map +0 -1
  331. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -24
  332. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
  333. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -359
  334. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
  335. package/dist/schemas/preprocessing/index.d.ts +0 -8
  336. package/dist/schemas/preprocessing/index.d.ts.map +0 -1
  337. package/dist/schemas/preprocessing/index.js +0 -2
  338. package/dist/schemas/preprocessing/index.js.map +0 -1
  339. package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
  340. package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
  341. package/dist/serializers/ConfluenceSerializer.js +0 -560
  342. package/dist/serializers/ConfluenceSerializer.js.map +0 -1
  343. package/dist/serializers/MarkdownSerializer.d.ts +0 -34
  344. package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
  345. package/dist/serializers/MarkdownSerializer.js +0 -395
  346. package/dist/serializers/MarkdownSerializer.js.map +0 -1
  347. package/dist/serializers/index.d.ts +0 -8
  348. package/dist/serializers/index.d.ts.map +0 -1
  349. package/dist/serializers/index.js +0 -8
  350. package/dist/serializers/index.js.map +0 -1
  351. package/src/SchemaConverterError.ts +0 -108
  352. package/src/ast/BlockNode.ts +0 -469
  353. package/src/ast/Document.ts +0 -90
  354. package/src/ast/InlineNode.ts +0 -323
  355. package/src/ast/MacroNode.ts +0 -245
  356. package/src/ast/index.ts +0 -83
  357. package/src/parsers/ConfluenceParser.ts +0 -956
  358. package/src/parsers/MarkdownParser.ts +0 -1338
  359. package/src/parsers/index.ts +0 -8
  360. package/src/schemas/ConfluenceSchema.ts +0 -56
  361. package/src/schemas/ConversionSchema.ts +0 -318
  362. package/src/schemas/MarkdownSchema.ts +0 -56
  363. package/src/schemas/hast/HastFromHtml.ts +0 -153
  364. package/src/schemas/hast/HastSchema.ts +0 -274
  365. package/src/schemas/hast/index.ts +0 -35
  366. package/src/schemas/index.ts +0 -20
  367. package/src/schemas/mdast/MdastFromMarkdown.ts +0 -118
  368. package/src/schemas/mdast/MdastSchema.ts +0 -566
  369. package/src/schemas/mdast/index.ts +0 -59
  370. package/src/schemas/mdast/mdastToString.ts +0 -102
  371. package/src/schemas/nodes/block/BlockSchema.ts +0 -773
  372. package/src/schemas/nodes/block/index.ts +0 -13
  373. package/src/schemas/nodes/index.ts +0 -20
  374. package/src/schemas/nodes/inline/InlineSchema.ts +0 -523
  375. package/src/schemas/nodes/inline/index.ts +0 -14
  376. package/src/schemas/nodes/macro/MacroSchema.ts +0 -226
  377. package/src/schemas/nodes/macro/index.ts +0 -6
  378. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +0 -455
  379. package/src/schemas/preprocessing/index.ts +0 -8
  380. package/src/serializers/ConfluenceSerializer.ts +0 -737
  381. package/src/serializers/MarkdownSerializer.ts +0 -543
  382. package/src/serializers/index.ts +0 -8
  383. package/test/ast/BlockNode.test.ts +0 -265
  384. package/test/ast/Document.test.ts +0 -126
  385. package/test/ast/InlineNode.test.ts +0 -161
  386. package/test/fixtures/integration-test.html.fixture +0 -103
  387. package/test/fixtures/integration-test.md.expected +0 -257
  388. package/test/parsers/ConfluenceParser.test.ts +0 -452
  389. package/test/schemas/ConfluencePreprocessor.test.ts +0 -180
  390. package/test/schemas/ConversionSchema.test.ts +0 -159
  391. package/test/schemas/HastSchema.test.ts +0 -138
  392. package/test/schemas/MdastSchema.test.ts +0 -145
  393. package/test/schemas/nodes/block/BlockSchema.test.ts +0 -173
  394. package/test/schemas/nodes/inline/InlineSchema.test.ts +0 -198
  395. package/test/schemas/nodes/macro/MacroSchema.test.ts +0 -142
@@ -0,0 +1,591 @@
1
+ /**
2
+ * Owned ADF → Markdown tree walker.
3
+ *
4
+ * Pure recursive descent over an ADF document's `node.type` discriminant.
5
+ * Returns GFM markdown plus a list of warnings for lossy or unknown nodes.
6
+ *
7
+ * @module
8
+ */
9
+ import type { DocNode } from "@atlaskit/adf-schema"
10
+
11
+ /**
12
+ * Warning emitted by the walker. Surfaced via `Effect.logWarning` at the
13
+ * facade boundary; never escalated to errors so a single weird node cannot
14
+ * break a clone of many pages.
15
+ */
16
+ export type WalkerWarning =
17
+ | { readonly _tag: "UnsupportedNode"; readonly nodeType: string }
18
+ | { readonly _tag: "LossyMark"; readonly mark: string }
19
+ | { readonly _tag: "MediaWithoutUrl"; readonly mediaId: string }
20
+ | {
21
+ readonly _tag: "UnsupportedExtension"
22
+ readonly nodeType: "extension" | "bodiedExtension" | "inlineExtension"
23
+ readonly extensionKey: string
24
+ readonly extensionType: string
25
+ }
26
+
27
+ export interface WalkResult {
28
+ readonly markdown: string
29
+ readonly warnings: ReadonlyArray<WalkerWarning>
30
+ }
31
+
32
+ interface AdfNode {
33
+ readonly type: string
34
+ readonly attrs?: Record<string, unknown>
35
+ readonly content?: ReadonlyArray<AdfNode>
36
+ readonly text?: string
37
+ readonly marks?: ReadonlyArray<AdfNode>
38
+ }
39
+
40
+ interface Ctx {
41
+ readonly inTable: boolean
42
+ readonly warnings: Array<WalkerWarning>
43
+ }
44
+
45
+ // Mid-line characters that change inline parsing in GFM. We deliberately omit
46
+ // `.` and `-` because they only carry meaning at line-start (numbered lists,
47
+ // setext rules) and escaping them mid-line produces noisy output like
48
+ // `v1\.0\.0` for ordinary version strings. Same reasoning drops `#`, `+`, `>`
49
+ // (line-start only), `(`/`)` (only meaningful right after `]`, which we
50
+ // escape), `!` (only meaningful right before `[`, ditto) and `{`/`}` (not
51
+ // special in GFM at all) — escaping those produced noise like `\(v2\)`.
52
+ const ESCAPE_RE = /[\\`*_[\]<|]/g
53
+ const escapeText = (s: string): string => s.replace(ESCAPE_RE, "\\$&")
54
+ const escapeAttr = (s: string): string => s.replace(/[\\"]/g, "\\$&")
55
+ // For text inside HTML *blocks* (`<details>`/`<summary>`): CommonMark treats
56
+ // everything up to the closing blank line as raw HTML, so backslash escapes
57
+ // would render literally — entity-escape instead.
58
+ const escapeHtml = (s: string): string =>
59
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;")
60
+ // `(`/`)`/space/`<`/`>`/`\` in a destination break `[text](url)` — wrap in
61
+ // angle brackets, percent-encoding the characters that would terminate or
62
+ // escape the wrapper itself.
63
+ const safeHref = (href: string): string =>
64
+ /[() <>\\]/.test(href) ? `<${href.replace(/[<>\\]/g, (c) => encodeURIComponent(c))}>` : href
65
+ // Alt text is substituted rather than escaped: @atlaskit's media markdown
66
+ // plugin throws on `\[`/`\]` in alt (making the page un-pushable), and
67
+ // newlines split the construct outright.
68
+ const sanitizeAlt = (s: string): string =>
69
+ s.replace(/\[/g, "(").replace(/\]/g, ")").replace(/\\/g, "/").replace(/\s+/g, " ").trim()
70
+
71
+ // ESCAPE_RE deliberately skips characters that are only special at line
72
+ // start, so lines assembled from paragraph text (including after hardBreak)
73
+ // must neutralize leading block markers: ATX headings, blockquotes, list
74
+ // bullets, ordered-list markers, thematic breaks, and setext underlines.
75
+ // A superfluous escape is harmless when the line later lands mid-line
76
+ // (after a list marker etc.) — backslash before punctuation always renders
77
+ // as the bare character.
78
+ const escapeLineStart = (line: string): string => {
79
+ if (/^(#{1,6}|[-+])(\s|$)/.test(line) || line.startsWith(">") || /^-{3,}\s*$/.test(line) || /^=+\s*$/.test(line)) {
80
+ return "\\" + line
81
+ }
82
+ const ordered = /^(\d+)([.)])(\s|$)/.exec(line)?.[1]
83
+ if (ordered) return `${ordered}\\${line.slice(ordered.length)}`
84
+ return line
85
+ }
86
+ const escapeLineStarts = (s: string): string => s.split("\n").map(escapeLineStart).join("\n")
87
+
88
+ const attrStr = (n: AdfNode, key: string): string | undefined => {
89
+ const v = n.attrs?.[key]
90
+ return typeof v === "string" ? v : undefined
91
+ }
92
+ const attrNum = (n: AdfNode, key: string): number | undefined => {
93
+ const v = n.attrs?.[key]
94
+ return typeof v === "number" ? v : undefined
95
+ }
96
+ const attrRecord = (n: AdfNode, key: string): Record<string, unknown> | undefined => {
97
+ const v = n.attrs?.[key]
98
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : undefined
99
+ }
100
+
101
+ const CONFLUENCE_CORE_MACRO_TYPE = "com.atlassian.confluence.macro.core"
102
+
103
+ // Deterministic JSON for placeholder metadata: object keys are sorted
104
+ // recursively so the same attrs always produce the same marker/sidecar data,
105
+ // no matter what order Confluence happens to serialize them in. Keeps pull →
106
+ // push → pull a byte-level fixed point (and contentHash stable).
107
+ const stableStringify = (v: unknown): string => {
108
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`
109
+ if (v !== null && typeof v === "object") {
110
+ const entries = Object.entries(v as Record<string, unknown>)
111
+ .filter(([, value]) => value !== undefined)
112
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
113
+ .map(([k, value]) => `${JSON.stringify(k)}:${stableStringify(value)}`)
114
+ return `{${entries.join(",")}}`
115
+ }
116
+ return JSON.stringify(v) ?? "null"
117
+ }
118
+
119
+ const inline = (nodes: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
120
+ if (!nodes) return ""
121
+ let out = ""
122
+ for (const n of nodes) out += inlineNode(n, ctx)
123
+ return out
124
+ }
125
+
126
+ const inlineNode = (n: AdfNode, ctx: Ctx): string => {
127
+ switch (n.type) {
128
+ case "text": {
129
+ const marks = n.marks ?? []
130
+ const text = n.text ?? ""
131
+ // Code spans render their content literally — backslash-escaping inside
132
+ // backticks would emit literal backslashes, which the push side then
133
+ // preserves verbatim, doubling them on every pull/push round-trip.
134
+ const hasCode = marks.some((m) => m.type === "code")
135
+ return applyMarks(hasCode ? text : escapeText(text), marks, ctx)
136
+ }
137
+ case "hardBreak":
138
+ return ctx.inTable ? "<br>" : " \n"
139
+ case "mention": {
140
+ // Confluence stores the mention's `text` attr with the leading `@`
141
+ // already (e.g. "@John Doe"). Strip it so we don't emit `@@John Doe`.
142
+ const id = attrStr(n, "id")
143
+ const raw = attrStr(n, "text") ?? id ?? ""
144
+ const stripped = raw.startsWith("@") ? raw.slice(1) : raw
145
+ const display = `@${escapeText(stripped)}`
146
+ // Encode the accountId in a custom-scheme link so the push side can
147
+ // reconstruct a real mention node. Without `id` we can only emit plain
148
+ // text; on push, that becomes plain text in Confluence (lossy).
149
+ return id ? `[${display}](confluence-mention://${encodeURIComponent(id)})` : display
150
+ }
151
+ case "emoji": {
152
+ return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
153
+ }
154
+ case "inlineCard": {
155
+ const url = cardUrl(n)
156
+ if (!url) {
157
+ // data-payload smart links have no URL to render — losing one must
158
+ // at least be visible in the logs.
159
+ ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: "inlineCard" })
160
+ return ""
161
+ }
162
+ const attrs = n.attrs ?? { url }
163
+ return `<!-- adf:inlineCard attrs=${stableStringify(attrs)} -->`
164
+ }
165
+ case "date": {
166
+ return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
167
+ }
168
+ case "status": {
169
+ const text = attrStr(n, "text") ?? ""
170
+ const color = attrStr(n, "color") ?? "neutral"
171
+ return `<span class="adf-status" data-color="${color}">${escapeText(text)}</span>`
172
+ }
173
+ case "mediaInline": {
174
+ const id = attrStr(n, "id") ?? ""
175
+ ctx.warnings.push({ _tag: "MediaWithoutUrl", mediaId: id })
176
+ return `<!-- adf:media id=${id} -->`
177
+ }
178
+ case "inlineExtension":
179
+ return extensionPlaceholder(n, "inlineExtension", ctx)
180
+ default:
181
+ ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
182
+ return `<!-- unsupported ADF inline: ${n.type} -->`
183
+ }
184
+ }
185
+
186
+ const extensionPlaceholder = (
187
+ n: AdfNode,
188
+ nodeType: "extension" | "bodiedExtension" | "inlineExtension",
189
+ ctx: Ctx
190
+ ): string => {
191
+ const extensionKey = attrStr(n, "extensionKey") ?? ""
192
+ const extensionType = attrStr(n, "extensionType") ?? ""
193
+ ctx.warnings.push({ _tag: "UnsupportedExtension", nodeType, extensionKey, extensionType })
194
+ const keyPart = extensionKey ? ` key=${extensionKey}` : ""
195
+ const typePart = extensionType ? ` type=${extensionType}` : ""
196
+ // key/type are repeated for human readability; `attrs` is the source of
197
+ // truth on push — it carries the *full* attrs (parameters, localId, layout)
198
+ // so macros survive a pull → push round-trip with their configuration.
199
+ const attrs = n.attrs ?? {}
200
+ const attrsPart = Object.keys(attrs).length > 0
201
+ ? ` attrs=${stableStringify(attrs)}`
202
+ : ""
203
+ return `<!-- adf:${nodeType}${keyPart}${typePart}${attrsPart} -->`
204
+ }
205
+
206
+ const objectKeys = (v: Record<string, unknown> | undefined): ReadonlyArray<string> => Object.keys(v ?? {})
207
+ const isOnlyKeys = (v: Record<string, unknown> | undefined, keys: ReadonlyArray<string>): boolean => {
208
+ const allowed = new Set(keys)
209
+ return objectKeys(v).every((key) => allowed.has(key))
210
+ }
211
+
212
+ const tocLevel = (macroParams: Record<string, unknown> | undefined, key: "minLevel" | "maxLevel"): string | null => {
213
+ const param = macroParams?.[key]
214
+ if (param === undefined) return null
215
+ if (param === null || typeof param !== "object" || Array.isArray(param)) return null
216
+ const record = param as Record<string, unknown>
217
+ if (!isOnlyKeys(record, ["value"])) return null
218
+ const value = record["value"]
219
+ return typeof value === "string" && /^[1-6]$/.test(value) ? value : null
220
+ }
221
+
222
+ const tocMarkdown = (n: AdfNode): string | null => {
223
+ const attrs = n.attrs
224
+ if (!attrs) return null
225
+ if (attrStr(n, "extensionKey") !== "toc" || attrStr(n, "extensionType") !== CONFLUENCE_CORE_MACRO_TYPE) return null
226
+ if (!isOnlyKeys(attrs, ["extensionKey", "extensionType", "parameters"])) return null
227
+
228
+ const parameters = attrRecord(n, "parameters")
229
+ if (!parameters) return "[[toc]]"
230
+ if (!isOnlyKeys(parameters, ["macroParams"])) return null
231
+
232
+ const macroParams = parameters["macroParams"]
233
+ if (macroParams === null || typeof macroParams !== "object" || Array.isArray(macroParams)) return null
234
+ const macroParamRecord = macroParams as Record<string, unknown>
235
+ if (!isOnlyKeys(macroParamRecord, ["minLevel", "maxLevel"])) return null
236
+
237
+ const minLevel = tocLevel(macroParamRecord, "minLevel")
238
+ const maxLevel = tocLevel(macroParamRecord, "maxLevel")
239
+ if (macroParamRecord["minLevel"] !== undefined && minLevel === null) return null
240
+ if (macroParamRecord["maxLevel"] !== undefined && maxLevel === null) return null
241
+
242
+ const parts = [
243
+ minLevel ? `min=${minLevel}` : "",
244
+ maxLevel ? `max=${maxLevel}` : ""
245
+ ].filter((part) => part.length > 0)
246
+
247
+ return parts.length > 0 ? `[[toc:${parts.join(",")}]]` : "[[toc]]"
248
+ }
249
+
250
+ const bodiedExtension = (n: AdfNode, ctx: Ctx): string => {
251
+ const open = extensionPlaceholder(n, "bodiedExtension", ctx)
252
+ // Table cells flatten newlines to <br>, which would weld the markers and
253
+ // body into one un-revertible line — emit only the single-line marker
254
+ // there (body dropped; the placeholder warning above covers it).
255
+ if (ctx.inTable) return open
256
+ // Render the body so it stays visible/editable; the end marker lets the
257
+ // push side re-attach everything in between as the bodiedExtension's body.
258
+ // It is emitted even for an empty body so the push side can tell "bodied
259
+ // macro with nothing in it" apart from a legacy/corrupted open marker.
260
+ const body = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
261
+ const parts = body.length > 0 ? [open, body] : [open]
262
+ return [...parts, "<!-- adf:/bodiedExtension -->"].join("\n\n")
263
+ }
264
+
265
+ const applyMarks = (text: string, marks: ReadonlyArray<AdfNode>, ctx: Ctx): string => {
266
+ let out = text
267
+ for (const m of marks) {
268
+ switch (m.type) {
269
+ case "code": {
270
+ // Code-span content is unescaped, so per GFM the delimiter must be a
271
+ // backtick run longer than any run inside, space-padded when the
272
+ // content starts/ends with a backtick (or is empty).
273
+ const runs = out.match(/`+/g) ?? []
274
+ const fence = "`".repeat(runs.reduce((max, r) => Math.max(max, r.length), 0) + 1)
275
+ const pad = out === "" || out.startsWith("`") || out.endsWith("`") ? " " : ""
276
+ out = `${fence}${pad}${out}${pad}${fence}`
277
+ break
278
+ }
279
+ case "strong":
280
+ out = `**${out}**`
281
+ break
282
+ case "em":
283
+ out = `_${out}_`
284
+ break
285
+ case "strike":
286
+ out = `~~${out}~~`
287
+ break
288
+ case "link": {
289
+ const href = attrStr(m, "href") ?? ""
290
+ const title = attrStr(m, "title")
291
+ const titlePart = title ? ` "${escapeAttr(title)}"` : ""
292
+ out = `[${out}](${safeHref(href)}${titlePart})`
293
+ break
294
+ }
295
+ case "underline":
296
+ ctx.warnings.push({ _tag: "LossyMark", mark: "underline" })
297
+ out = `<u>${out}</u>`
298
+ break
299
+ case "textColor": {
300
+ const color = attrStr(m, "color") ?? ""
301
+ ctx.warnings.push({ _tag: "LossyMark", mark: "textColor" })
302
+ out = `<span style="color:${color}">${out}</span>`
303
+ break
304
+ }
305
+ case "backgroundColor": {
306
+ const color = attrStr(m, "color") ?? ""
307
+ ctx.warnings.push({ _tag: "LossyMark", mark: "backgroundColor" })
308
+ out = `<span style="background-color:${color}">${out}</span>`
309
+ break
310
+ }
311
+ case "subsup": {
312
+ const t = attrStr(m, "type") === "sup" ? "sup" : "sub"
313
+ out = `<${t}>${out}</${t}>`
314
+ break
315
+ }
316
+ default:
317
+ ctx.warnings.push({ _tag: "LossyMark", mark: m.type })
318
+ }
319
+ }
320
+ return out
321
+ }
322
+
323
+ const indentLines = (s: string, indent: string): string =>
324
+ s.split("\n").map((line, i) => (i === 0 ? line : indent + line)).join("\n")
325
+
326
+ const block = (n: AdfNode, ctx: Ctx): string => {
327
+ switch (n.type) {
328
+ case "paragraph":
329
+ return paragraph(n, ctx)
330
+ case "heading": {
331
+ const level = Math.min(6, Math.max(1, attrNum(n, "level") ?? 1))
332
+ return "#".repeat(level) + " " + inline(n.content, ctx)
333
+ }
334
+ case "rule":
335
+ return "---"
336
+ case "blockquote":
337
+ return blockquote(n.content, ctx)
338
+ case "codeBlock":
339
+ return codeBlock(n)
340
+ case "bulletList":
341
+ return list(n, ctx, false)
342
+ case "orderedList":
343
+ return list(n, ctx, true)
344
+ case "table":
345
+ return table(n, ctx)
346
+ case "panel":
347
+ return panel(n, ctx)
348
+ case "expand":
349
+ case "nestedExpand":
350
+ return expand(n, ctx)
351
+ case "taskList":
352
+ return taskList(n, ctx)
353
+ case "decisionList":
354
+ return decisionList(n, ctx)
355
+ case "layoutSection":
356
+ return layoutSection(n, ctx)
357
+ case "layoutColumn":
358
+ return layoutColumn(n, ctx)
359
+ case "mediaSingle":
360
+ return mediaSingle(n, ctx)
361
+ case "mediaGroup":
362
+ return mediaGroup(n, ctx)
363
+ case "blockCard":
364
+ case "embedCard":
365
+ return blockCard(n, ctx)
366
+ case "extension":
367
+ return tocMarkdown(n) ?? extensionPlaceholder(n, "extension", ctx)
368
+ case "bodiedExtension":
369
+ return bodiedExtension(n, ctx)
370
+ default:
371
+ ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
372
+ return `<!-- unsupported ADF node: ${n.type} -->`
373
+ }
374
+ }
375
+
376
+ const paragraph = (n: AdfNode, ctx: Ctx): string => {
377
+ const body = escapeLineStarts(inline(n.content, ctx))
378
+ const marks = n.marks ?? []
379
+ if (marks.length === 0 || ctx.inTable) return body
380
+ const marksPart = ` marks=${stableStringify(marks)}`
381
+ return `<!-- adf:paragraph${marksPart} -->\n\n${body}\n\n<!-- adf:/paragraph -->`
382
+ }
383
+
384
+ const blockquote = (content: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
385
+ const inner = (content ?? []).map((c) => block(c, ctx)).join("\n\n")
386
+ return inner.split("\n").map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\n")
387
+ }
388
+
389
+ const codeBlock = (n: AdfNode): string => {
390
+ // A fence's info string may not contain backticks (CommonMark) and a
391
+ // newline would inject lines into the code content — the editor UI uses a
392
+ // fixed language list, but the REST API accepts arbitrary strings.
393
+ const lang = (attrStr(n, "language") ?? "").replace(/[`\s]+/g, "")
394
+ const text = (n.content ?? []).map((c) => c.text ?? "").join("")
395
+ // A fixed ``` fence would be terminated early by code that itself contains
396
+ // a triple-backtick run — use one backtick more than the longest run inside.
397
+ const runs = text.match(/`+/g) ?? []
398
+ const fence = "`".repeat(Math.max(3, runs.reduce((max, r) => Math.max(max, r.length), 0) + 1))
399
+ return fence + lang + "\n" + text + "\n" + fence
400
+ }
401
+
402
+ const listItemBlocks = (item: AdfNode, ctx: Ctx): string => {
403
+ const blocks = item.content ?? []
404
+ const parts: Array<string> = []
405
+ for (const b of blocks) {
406
+ if (b.type === "paragraph") {
407
+ // Continuation lines (after hardBreak) sit at line start once the
408
+ // item is indented, so they need the same leading-marker escapes as
409
+ // top-level paragraphs.
410
+ parts.push(escapeLineStarts(inline(b.content, ctx)))
411
+ } else {
412
+ parts.push(block(b, ctx))
413
+ }
414
+ }
415
+ return parts.join("\n\n")
416
+ }
417
+
418
+ const list = (n: AdfNode, ctx: Ctx, ordered: boolean): string => {
419
+ const items = n.content ?? []
420
+ const startNum = ordered ? Math.max(1, attrNum(n, "order") ?? 1) : 1
421
+ const indent = " "
422
+ const inner: Array<string> = []
423
+ for (let i = 0; i < items.length; i++) {
424
+ const item = items[i]
425
+ if (!item) continue
426
+ const marker = ordered ? `${startNum + i}. ` : "- "
427
+ const body = listItemBlocks(item, ctx)
428
+ inner.push(marker + indentLines(body, indent))
429
+ }
430
+ return inner.join("\n")
431
+ }
432
+
433
+ const tableCellInline = (cell: AdfNode, ctx: Ctx): string => {
434
+ const cellCtx: Ctx = { ...ctx, inTable: true }
435
+ const blocks = cell.content ?? []
436
+ const parts: Array<string> = []
437
+ for (const b of blocks) {
438
+ if (b.type === "paragraph") parts.push(inline(b.content, cellCtx))
439
+ else parts.push(block(b, cellCtx).replace(/\n/g, "<br>"))
440
+ }
441
+ // Escape `|` so it can't open a new column — but only pipes that aren't
442
+ // already escaped (inline() escapes them in plain text; code spans, URLs
443
+ // and <br>-flattened blocks don't). A pipe is escaped iff it's preceded by
444
+ // an odd run of backslashes, so count the run rather than peek one char.
445
+ return parts.join("<br>").replace(
446
+ /(\\*)\|/g,
447
+ (match, backslashes: string) => backslashes.length % 2 === 0 ? `${backslashes}\\|` : match
448
+ )
449
+ }
450
+
451
+ const table = (n: AdfNode, ctx: Ctx): string => {
452
+ const rows = n.content ?? []
453
+ if (rows.length === 0) return ""
454
+ const renderRow = (row: AdfNode): Array<string> => (row.content ?? []).map((cell) => tableCellInline(cell, ctx))
455
+ const allRows = rows.map(renderRow)
456
+ const colCount = Math.max(...allRows.map((r) => r.length))
457
+ const pad = (cells: Array<string>): Array<string> => {
458
+ const out = cells.slice()
459
+ while (out.length < colCount) out.push("")
460
+ return out
461
+ }
462
+ const firstRow = rows[0]
463
+ const firstIsHeader = (firstRow?.content ?? []).every((c) => c.type === "tableHeader")
464
+ const header = firstIsHeader ? pad(allRows[0] ?? []) : Array<string>(colCount).fill("")
465
+ const separator = Array<string>(colCount).fill("---")
466
+ const bodyRows = (firstIsHeader ? allRows.slice(1) : allRows).map(pad)
467
+ const fmt = (cells: Array<string>): string => `| ${cells.join(" | ")} |`
468
+ return encodedBlockNode(n, [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n"), ctx)
469
+ }
470
+
471
+ const panel = (n: AdfNode, ctx: Ctx): string => {
472
+ const panelType = attrStr(n, "panelType") ?? "info"
473
+ const attrs = n.attrs ?? { panelType }
474
+ const attrsPart = Object.keys(attrs).length > 0 ? ` attrs=${stableStringify(attrs)}` : ""
475
+ const open = `<!-- adf:panel type=${panelType}${attrsPart} -->`
476
+ if (ctx.inTable) return open
477
+ const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
478
+ const parts = inner.length > 0 ? [open, inner] : [open]
479
+ return [...parts, "<!-- adf:/panel -->"].join("\n\n")
480
+ }
481
+
482
+ const encodedBlockNode = (n: AdfNode, body: string, ctx: Ctx): string => {
483
+ if (ctx.inTable) return body
484
+ const open = `<!-- adf:${n.type} node=${stableStringify(n)} -->`
485
+ const parts = body.length > 0 ? [open, body] : [open]
486
+ return [...parts, `<!-- adf:/${n.type} -->`].join("\n\n")
487
+ }
488
+
489
+ const expand = (n: AdfNode, ctx: Ctx): string => {
490
+ const title = attrStr(n, "title") ?? ""
491
+ // At block level <details> is a CommonMark type-6 HTML block, so the title
492
+ // needs entity escaping. Inside a table cell the flattened output becomes
493
+ // *inline* HTML where the text between tags is still markdown — there the
494
+ // backslash escapes are the correct (and only working) form.
495
+ const safeTitle = ctx.inTable ? escapeText(title) : escapeHtml(title)
496
+ const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
497
+ if (ctx.inTable) return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
498
+ return encodedBlockNode(n, `${title}\n\n${inner}`, ctx)
499
+ }
500
+
501
+ const taskList = (n: AdfNode, ctx: Ctx): string => {
502
+ const items = n.content ?? []
503
+ const lines: Array<string> = []
504
+ for (const item of items) {
505
+ if (item.type !== "taskItem") {
506
+ lines.push(block(item, ctx))
507
+ continue
508
+ }
509
+ const checked = attrStr(item, "state") === "DONE" ? "x" : " "
510
+ const text = inline(item.content, ctx)
511
+ lines.push(`- [${checked}] ${text}`)
512
+ }
513
+ return encodedBlockNode(n, lines.join("\n"), ctx)
514
+ }
515
+
516
+ const decisionList = (n: AdfNode, ctx: Ctx): string => {
517
+ const items = n.content ?? []
518
+ const lines: Array<string> = []
519
+ for (const item of items) {
520
+ if (item.type !== "decisionItem") {
521
+ lines.push(block(item, ctx))
522
+ continue
523
+ }
524
+ lines.push(`- 🔘 ${inline(item.content, ctx)}`)
525
+ }
526
+ return encodedBlockNode(n, lines.join("\n"), ctx)
527
+ }
528
+
529
+ const layoutSection = (n: AdfNode, ctx: Ctx): string => {
530
+ const body = (n.content ?? [])
531
+ .map((column) => block(column, ctx))
532
+ .filter((part) => part.trim().length > 0)
533
+ .join("\n\n")
534
+ return encodedBlockNode(n, body, ctx)
535
+ }
536
+
537
+ const layoutColumn = (n: AdfNode, ctx: Ctx): string => (n.content ?? []).map((child) => block(child, ctx)).join("\n\n")
538
+
539
+ const cardUrl = (n: AdfNode): string | undefined => {
540
+ const url = attrStr(n, "url")
541
+ if (url) return url
542
+ const data = attrRecord(n, "data")
543
+ const dataUrl = data?.["url"]
544
+ return typeof dataUrl === "string" ? dataUrl : undefined
545
+ }
546
+
547
+ const blockCard = (n: AdfNode, ctx: Ctx): string => {
548
+ const url = cardUrl(n)
549
+ if (!url) {
550
+ ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
551
+ return `<!-- unsupported ADF node: ${n.type} -->`
552
+ }
553
+ return encodedBlockNode(n, `<${url}>`, ctx)
554
+ }
555
+
556
+ const renderMedia = (media: AdfNode | undefined, ctx: Ctx): string => {
557
+ const id = (media && attrStr(media, "id")) ?? ""
558
+ const alt = (media && attrStr(media, "alt")) ?? ""
559
+ const url = media && attrStr(media, "url")
560
+ if (url) return `![${sanitizeAlt(alt)}](${safeHref(url)})`
561
+ ctx.warnings.push({ _tag: "MediaWithoutUrl", mediaId: id })
562
+ return `<!-- adf:media id=${id} -->`
563
+ }
564
+
565
+ const mediaSingle = (n: AdfNode, ctx: Ctx): string => {
566
+ const children = n.content ?? []
567
+ const rendered = renderMedia(children.find((c) => c.type === "media"), ctx)
568
+ const caption = children.find((c) => c.type === "caption")
569
+ const captionText = caption ? inline(caption.content, ctx).trim() : ""
570
+ if (captionText.length === 0) return rendered
571
+ // An em-marked caption already renders as `_…_`; wrapping again would make
572
+ // `__…__` (strong). Leave captions that touch an underscore unwrapped.
573
+ const line = captionText.startsWith("_") || captionText.endsWith("_") ? captionText : `_${captionText}_`
574
+ return `${rendered}\n${line}`
575
+ }
576
+
577
+ const mediaGroup = (n: AdfNode, ctx: Ctx): string =>
578
+ (n.content ?? []).map((media) => renderMedia(media, ctx)).join("\n\n")
579
+
580
+ /**
581
+ * Walk an ADF document and emit GFM markdown. Always synchronous; warnings
582
+ * are collected, not thrown.
583
+ */
584
+ export const walk = (doc: DocNode): WalkResult => {
585
+ const ctx: Ctx = { inTable: false, warnings: [] }
586
+ const root = doc as unknown as AdfNode
587
+ const blocks = (root.content ?? []).map((c) => block(c, ctx))
588
+ const body = blocks.join("\n\n")
589
+ const markdown = body.endsWith("\n") ? body : body + "\n"
590
+ return { markdown, warnings: ctx.warnings }
591
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Effect wrapper around the official @atlaskit markdown / JSON transformers.
3
+ *
4
+ * Push direction (markdown → ADF) routes through `MarkdownTransformer.parse()`
5
+ * (markdown → ProseMirror node) followed by `JSONTransformer.encode()`
6
+ * (ProseMirror node → ADF JSON). Both libraries are stateless once
7
+ * constructed, so we instantiate them once at module load.
8
+ *
9
+ * @module
10
+ */
11
+ // Deep import into the published CJS file: Atlaskit's `schema-default`
12
+ // subpath has no `exports` map (Node ESM rejects it as a directory import)
13
+ // and its ESM build uses extensionless relative imports (also rejected).
14
+ // The CJS file works under Node's CJS-into-ESM interop. Types come from the
15
+ // ambient declaration in `atlaskit-adf-schema.d.ts`.
16
+ import { defaultSchema } from "@atlaskit/adf-schema/dist/cjs/schema/default-schema.js"
17
+ import { type JSONDocNode, JSONTransformer } from "@atlaskit/editor-json-transformer"
18
+ import { MarkdownTransformer } from "@atlaskit/editor-markdown-transformer"
19
+ import * as Context from "effect/Context"
20
+ import * as Effect from "effect/Effect"
21
+ import * as Layer from "effect/Layer"
22
+ import { AtlaskitTransformersError } from "./ConfluenceError.js"
23
+
24
+ /**
25
+ * Bag of the two transformer instances handed to the `use` callback.
26
+ */
27
+ export interface Transformers {
28
+ readonly md: MarkdownTransformer
29
+ readonly json: JSONTransformer
30
+ }
31
+
32
+ const md = new MarkdownTransformer(defaultSchema)
33
+ const json = new JSONTransformer(defaultSchema)
34
+ const transformers: Transformers = { md, json }
35
+
36
+ /**
37
+ * Effect service exposing the @atlaskit markdown + JSON transformers via a
38
+ * `use` callback. Errors thrown synchronously by the underlying libraries are
39
+ * surfaced as `AtlaskitTransformersError`.
40
+ *
41
+ * @category Service
42
+ */
43
+ export class AtlaskitTransformers extends Context.Service<
44
+ AtlaskitTransformers,
45
+ {
46
+ readonly use: <A>(
47
+ fn: (t: Transformers) => A
48
+ ) => Effect.Effect<A, AtlaskitTransformersError>
49
+ }
50
+ >()("@knpkv/confluence-to-markdown/AtlaskitTransformers") {}
51
+
52
+ /**
53
+ * Live Layer providing the wrapped @atlaskit transformers. The transformer
54
+ * instances are module-level singletons; the layer just hands out a
55
+ * `use`-callback service that catches synchronous throws.
56
+ *
57
+ * @category Layers
58
+ */
59
+ export const layer: Layer.Layer<AtlaskitTransformers> = Layer.succeed(
60
+ AtlaskitTransformers,
61
+ AtlaskitTransformers.of({
62
+ use: <A>(fn: (t: Transformers) => A) =>
63
+ Effect.try({
64
+ try: () => fn(transformers),
65
+ catch: (cause) => new AtlaskitTransformersError({ cause })
66
+ })
67
+ })
68
+ )
69
+
70
+ export type { JSONDocNode }