@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,676 @@
1
+ import type { DocNode } from "@atlaskit/adf-schema"
2
+ import { describe, expect, it } from "vitest"
3
+ import { walk } from "../src/AdfWalker.js"
4
+
5
+ const doc = (content: ReadonlyArray<unknown>): DocNode => ({ version: 1, type: "doc", content } as unknown as DocNode)
6
+
7
+ const stableStringify = (v: unknown): string => {
8
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`
9
+ if (v !== null && typeof v === "object") {
10
+ return `{${
11
+ Object.entries(v as Record<string, unknown>)
12
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
13
+ .map(([k, value]) => `${JSON.stringify(k)}:${stableStringify(value)}`)
14
+ .join(",")
15
+ }}`
16
+ }
17
+ return JSON.stringify(v) ?? "null"
18
+ }
19
+
20
+ describe("AdfWalker", () => {
21
+ it("emits a heading at the right level", () => {
22
+ const r = walk(doc([{ type: "heading", attrs: { level: 3 }, content: [{ type: "text", text: "Hi" }] }]))
23
+ expect(r.markdown).toContain("### Hi")
24
+ })
25
+
26
+ it("escapes special characters in text", () => {
27
+ const r = walk(doc([{ type: "paragraph", content: [{ type: "text", text: "use *stars*" }] }]))
28
+ expect(r.markdown).toContain("\\*stars\\*")
29
+ })
30
+
31
+ it("does not escape characters that are only special at line-start or in link syntax", () => {
32
+ const r = walk(doc([{
33
+ type: "paragraph",
34
+ content: [{ type: "text", text: "a (b) c+ d! {e} #1 > #2" }]
35
+ }]))
36
+ expect(r.markdown).toContain("a (b) c+ d! {e} #1 > #2")
37
+ })
38
+
39
+ it("does not escape code-marked text", () => {
40
+ // Backslashes inside code spans are literal; escaping here made every
41
+ // pull/push round-trip double them (a\_b → a\\\_b → …).
42
+ const r = walk(doc([{
43
+ type: "paragraph",
44
+ content: [{ type: "text", text: "a_b (c*)", marks: [{ type: "code" }] }]
45
+ }]))
46
+ expect(r.markdown).toContain("`a_b (c*)`")
47
+ })
48
+
49
+ it("fences code spans containing backticks with a longer delimiter", () => {
50
+ const r = walk(doc([{
51
+ type: "paragraph",
52
+ content: [{ type: "text", text: "a `b` c", marks: [{ type: "code" }] }]
53
+ }]))
54
+ expect(r.markdown).toContain("``a `b` c``")
55
+ })
56
+
57
+ it("space-pads code spans that start or end with a backtick", () => {
58
+ const r = walk(doc([{
59
+ type: "paragraph",
60
+ content: [{ type: "text", text: "`tick", marks: [{ type: "code" }] }]
61
+ }]))
62
+ expect(r.markdown).toContain("`` `tick ``")
63
+ })
64
+
65
+ it("renders inline marks", () => {
66
+ const r = walk(doc([{
67
+ type: "paragraph",
68
+ content: [
69
+ { type: "text", text: "a", marks: [{ type: "strong" }] },
70
+ { type: "text", text: "b", marks: [{ type: "em" }] },
71
+ { type: "text", text: "c", marks: [{ type: "code" }] },
72
+ { type: "text", text: "d", marks: [{ type: "strike" }] }
73
+ ]
74
+ }]))
75
+ expect(r.markdown).toContain("**a**")
76
+ expect(r.markdown).toContain("_b_")
77
+ expect(r.markdown).toContain("`c`")
78
+ expect(r.markdown).toContain("~~d~~")
79
+ })
80
+
81
+ it("renders a link with title", () => {
82
+ const r = walk(doc([{
83
+ type: "paragraph",
84
+ content: [{
85
+ type: "text",
86
+ text: "go",
87
+ marks: [{ type: "link", attrs: { href: "https://x.test", title: "T" } }]
88
+ }]
89
+ }]))
90
+ expect(r.markdown).toContain(`[go](https://x.test "T")`)
91
+ })
92
+
93
+ it("falls back lossy marks to HTML and warns", () => {
94
+ const r = walk(doc([{
95
+ type: "paragraph",
96
+ content: [{ type: "text", text: "U", marks: [{ type: "underline" }] }]
97
+ }]))
98
+ expect(r.markdown).toContain("<u>U</u>")
99
+ expect(r.warnings.some((w) => w._tag === "LossyMark" && w.mark === "underline")).toBe(true)
100
+ })
101
+
102
+ it("renders nested bullet lists", () => {
103
+ const r = walk(doc([{
104
+ type: "bulletList",
105
+ content: [{
106
+ type: "listItem",
107
+ content: [
108
+ { type: "paragraph", content: [{ type: "text", text: "outer" }] },
109
+ {
110
+ type: "bulletList",
111
+ content: [{
112
+ type: "listItem",
113
+ content: [{ type: "paragraph", content: [{ type: "text", text: "inner" }] }]
114
+ }]
115
+ }
116
+ ]
117
+ }]
118
+ }]))
119
+ expect(r.markdown).toContain("- outer")
120
+ expect(r.markdown).toContain("inner")
121
+ })
122
+
123
+ it("renders ordered lists with attrs.order", () => {
124
+ const r = walk(doc([{
125
+ type: "orderedList",
126
+ attrs: { order: 5 },
127
+ content: [{
128
+ type: "listItem",
129
+ content: [{ type: "paragraph", content: [{ type: "text", text: "first" }] }]
130
+ }]
131
+ }]))
132
+ expect(r.markdown).toContain("5. first")
133
+ })
134
+
135
+ it("renders a code block with language", () => {
136
+ const r = walk(doc([{
137
+ type: "codeBlock",
138
+ attrs: { language: "ts" },
139
+ content: [{ type: "text", text: "const x = 1" }]
140
+ }]))
141
+ expect(r.markdown).toContain("```ts")
142
+ expect(r.markdown).toContain("const x = 1")
143
+ expect(r.markdown).toContain("```")
144
+ })
145
+
146
+ it("renders a table with header row", () => {
147
+ const r = walk(doc([{
148
+ type: "table",
149
+ content: [
150
+ {
151
+ type: "tableRow",
152
+ content: [
153
+ { type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "A" }] }] },
154
+ { type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "B" }] }] }
155
+ ]
156
+ },
157
+ {
158
+ type: "tableRow",
159
+ content: [
160
+ { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "1" }] }] },
161
+ { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "2" }] }] }
162
+ ]
163
+ }
164
+ ]
165
+ }]))
166
+ expect(r.markdown).toContain("| A | B |")
167
+ expect(r.markdown).toContain("| --- | --- |")
168
+ expect(r.markdown).toContain("| 1 | 2 |")
169
+ })
170
+
171
+ it("renders a panel as a Confluence-preserving placeholder", () => {
172
+ const r = walk(doc([{
173
+ type: "panel",
174
+ attrs: { panelType: "warning" },
175
+ content: [{ type: "paragraph", content: [{ type: "text", text: "be careful" }] }]
176
+ }]))
177
+ expect(r.markdown).toContain(
178
+ `<!-- adf:panel type=warning attrs=${stableStringify({ panelType: "warning" })} -->`
179
+ )
180
+ expect(r.markdown).toContain("be careful")
181
+ expect(r.markdown).toContain("<!-- adf:/panel -->")
182
+ })
183
+
184
+ it("renders task lists with checkbox state", () => {
185
+ const node = {
186
+ type: "taskList",
187
+ attrs: { localId: "tasks-1" },
188
+ content: [
189
+ {
190
+ type: "taskItem",
191
+ attrs: { localId: "task-1", state: "DONE" },
192
+ content: [{ type: "text", text: "done" }]
193
+ },
194
+ {
195
+ type: "taskItem",
196
+ attrs: { localId: "task-2", state: "TODO" },
197
+ content: [{ type: "text", text: "todo" }]
198
+ }
199
+ ]
200
+ }
201
+ const r = walk(doc([node]))
202
+ expect(r.markdown).toContain(`<!-- adf:taskList node=${stableStringify(node)} -->`)
203
+ expect(r.markdown).toContain("- [x] done")
204
+ expect(r.markdown).toContain("- [ ] todo")
205
+ expect(r.markdown).toContain("<!-- adf:/taskList -->")
206
+ })
207
+
208
+ it("wraps decision lists so they survive push as native decisions", () => {
209
+ const node = {
210
+ type: "decisionList",
211
+ attrs: { localId: "decisions-1" },
212
+ content: [{
213
+ type: "decisionItem",
214
+ attrs: { localId: "decision-1", state: "DECIDED" },
215
+ content: [{ type: "text", text: "decide" }]
216
+ }]
217
+ }
218
+ const r = walk(doc([node]))
219
+ expect(r.markdown).toContain(`<!-- adf:decisionList node=${stableStringify(node)} -->`)
220
+ expect(r.markdown).toContain("- 🔘 decide")
221
+ expect(r.markdown).toContain("<!-- adf:/decisionList -->")
222
+ })
223
+
224
+ it("renders every child of a mediaGroup", () => {
225
+ const r = walk(doc([{
226
+ type: "mediaGroup",
227
+ content: [
228
+ { type: "media", attrs: { id: "m1", alt: "first", url: "https://x.test/1.png" } },
229
+ { type: "media", attrs: { id: "m2", alt: "second", url: "https://x.test/2.png" } },
230
+ { type: "media", attrs: { id: "m3" } }
231
+ ]
232
+ }]))
233
+ expect(r.markdown).toContain("![first](https://x.test/1.png)")
234
+ expect(r.markdown).toContain("![second](https://x.test/2.png)")
235
+ expect(r.markdown).toContain("<!-- adf:media id=m3 -->")
236
+ expect(r.warnings.some((w) => w._tag === "MediaWithoutUrl" && w.mediaId === "m3")).toBe(true)
237
+ })
238
+
239
+ it("emits placeholders + warnings for unknown nodes", () => {
240
+ const r = walk(doc([{ type: "totallyMadeUp" }]))
241
+ expect(r.markdown).toContain("<!-- unsupported ADF node: totallyMadeUp -->")
242
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode")).toBe(true)
243
+ })
244
+
245
+ it("does not double the @ on mentions whose text already starts with @", () => {
246
+ const r = walk(doc([{
247
+ type: "paragraph",
248
+ content: [{ type: "mention", attrs: { id: "557057:abc", text: "@Andrey Konopkov" } }]
249
+ }]))
250
+ expect(r.markdown).toContain("@Andrey Konopkov")
251
+ expect(r.markdown).not.toContain("@@")
252
+ })
253
+
254
+ it("encodes the mention accountId in a custom-scheme link", () => {
255
+ const r = walk(doc([{
256
+ type: "paragraph",
257
+ content: [{ type: "mention", attrs: { id: "557057:abc-123", text: "@Andrey Konopkov" } }]
258
+ }]))
259
+ // ":" gets percent-encoded by encodeURIComponent so the URL is unambiguous.
260
+ expect(r.markdown).toContain("[@Andrey Konopkov](confluence-mention://557057%3Aabc-123)")
261
+ })
262
+
263
+ it("falls back to plain @text when the mention has no id", () => {
264
+ const r = walk(doc([{
265
+ type: "paragraph",
266
+ content: [{ type: "mention", attrs: { text: "@Anon" } }]
267
+ }]))
268
+ expect(r.markdown).toContain("@Anon")
269
+ expect(r.markdown).not.toContain("confluence-mention")
270
+ })
271
+
272
+ it("renders a representable TOC extension as readable native syntax", () => {
273
+ const r = walk(doc([{
274
+ type: "extension",
275
+ attrs: {
276
+ extensionKey: "toc",
277
+ extensionType: "com.atlassian.confluence.macro.core"
278
+ }
279
+ }]))
280
+ expect(r.markdown).toContain("[[toc]]")
281
+ expect(r.warnings).not.toContainEqual(
282
+ expect.objectContaining({ _tag: "UnsupportedExtension", extensionKey: "toc" })
283
+ )
284
+ })
285
+
286
+ it("renders representable TOC levels in readable native syntax", () => {
287
+ const r = walk(doc([{
288
+ type: "extension",
289
+ attrs: {
290
+ extensionType: "com.atlassian.confluence.macro.core",
291
+ extensionKey: "toc",
292
+ parameters: {
293
+ macroParams: {
294
+ minLevel: { value: "2" },
295
+ maxLevel: { value: "4" }
296
+ }
297
+ }
298
+ }
299
+ }]))
300
+ expect(r.markdown).toContain("[[toc:min=2,max=4]]")
301
+ })
302
+
303
+ it("falls back to a generic placeholder when TOC attrs are not fully representable", () => {
304
+ const rawAttrs = {
305
+ extensionKey: "toc",
306
+ extensionType: "com.atlassian.confluence.macro.core",
307
+ layout: "default",
308
+ localId: "abc-123",
309
+ parameters: { macroParams: { maxLevel: { value: "3" } } }
310
+ }
311
+ const r = walk(doc([{ type: "extension", attrs: rawAttrs }]))
312
+ const attrs = stableStringify(rawAttrs)
313
+ expect(r.markdown).toContain(
314
+ `<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=${attrs} -->`
315
+ )
316
+ expect(r.markdown).not.toContain("[[toc")
317
+ expect(
318
+ r.warnings.some((w) =>
319
+ w._tag === "UnsupportedExtension" && w.extensionKey === "toc" && w.nodeType === "extension"
320
+ )
321
+ ).toBe(true)
322
+ })
323
+
324
+ it("falls back to a generic placeholder for Confluence TOC macroMetadata", () => {
325
+ const rawAttrs = {
326
+ extensionKey: "toc",
327
+ extensionType: "com.atlassian.confluence.macro.core",
328
+ layout: "default",
329
+ parameters: {
330
+ macroMetadata: {
331
+ schemaVersion: { value: "1" },
332
+ title: "Table of Contents"
333
+ },
334
+ macroParams: {}
335
+ }
336
+ }
337
+ const r = walk(doc([{ type: "extension", attrs: rawAttrs }]))
338
+ const attrs = stableStringify(rawAttrs)
339
+ expect(r.markdown).toContain(
340
+ `<!-- adf:extension key=toc type=com.atlassian.confluence.macro.core attrs=${attrs} -->`
341
+ )
342
+ expect(r.markdown).not.toContain("[[toc")
343
+ })
344
+
345
+ it("emits the same attrs blob regardless of source key order", () => {
346
+ const a = walk(doc([{ type: "extension", attrs: { extensionKey: "toc", extensionType: "t" } }]))
347
+ const b = walk(doc([{ type: "extension", attrs: { extensionType: "t", extensionKey: "toc" } }]))
348
+ expect(a.markdown).toBe(b.markdown)
349
+ })
350
+
351
+ it("handles inline and bodied extensions", () => {
352
+ const r = walk(doc([
353
+ {
354
+ type: "paragraph",
355
+ content: [
356
+ { type: "text", text: "before " },
357
+ { type: "inlineExtension", attrs: { extensionKey: "jira-issue", extensionType: "com.example" } },
358
+ { type: "text", text: " after" }
359
+ ]
360
+ },
361
+ {
362
+ type: "bodiedExtension",
363
+ attrs: { extensionKey: "details", extensionType: "com.example" },
364
+ content: [{ type: "paragraph", content: [{ type: "text", text: "body" }] }]
365
+ }
366
+ ]))
367
+ const inlineAttrs = stableStringify({ extensionKey: "jira-issue", extensionType: "com.example" })
368
+ const bodiedAttrs = stableStringify({ extensionKey: "details", extensionType: "com.example" })
369
+ expect(r.markdown).toContain(
370
+ `<!-- adf:inlineExtension key=jira-issue type=com.example attrs=${inlineAttrs} -->`
371
+ )
372
+ // The bodied extension renders its body between an open and an end marker
373
+ // so the push side can re-attach it.
374
+ expect(r.markdown).toContain(
375
+ `<!-- adf:bodiedExtension key=details type=com.example attrs=${bodiedAttrs} -->\n\nbody\n\n<!-- adf:/bodiedExtension -->`
376
+ )
377
+ expect(r.warnings.filter((w) => w._tag === "UnsupportedExtension")).toHaveLength(2)
378
+ })
379
+
380
+ it("emits the end marker even for an empty bodied extension", () => {
381
+ // Without it the push side cannot tell "bodied macro with empty body"
382
+ // apart from a legacy/corrupted open marker, and would change node type.
383
+ const r = walk(doc([{
384
+ type: "bodiedExtension",
385
+ attrs: { extensionKey: "excerpt", extensionType: "com.example" },
386
+ content: [{ type: "paragraph", content: [] }]
387
+ }]))
388
+ expect(r.markdown).toContain("<!-- adf:/bodiedExtension -->")
389
+ })
390
+
391
+ it("emits only the single-line marker for a bodied extension inside a table cell", () => {
392
+ // <br>-flattened multi-block emission cannot be reverted on push; the
393
+ // bare marker at least comes back as a clean extension node.
394
+ const r = walk(doc([{
395
+ type: "table",
396
+ content: [{
397
+ type: "tableRow",
398
+ content: [{
399
+ type: "tableCell",
400
+ content: [{
401
+ type: "bodiedExtension",
402
+ attrs: { extensionKey: "details", extensionType: "com.example" },
403
+ content: [{ type: "paragraph", content: [{ type: "text", text: "body" }] }]
404
+ }]
405
+ }]
406
+ }]
407
+ }]))
408
+ expect(r.markdown).not.toContain("adf:/bodiedExtension")
409
+ expect(r.markdown).not.toContain("\n\nbody\n\n")
410
+ expect(r.markdown).toContain("<!-- adf:bodiedExtension key=details type=com.example")
411
+ })
412
+
413
+ it("escapes a pipe in a table cell exactly once", () => {
414
+ const cell = (content: ReadonlyArray<unknown>) => ({
415
+ type: "tableCell",
416
+ content: [{ type: "paragraph", content }]
417
+ })
418
+ const r = walk(doc([{
419
+ type: "table",
420
+ content: [{
421
+ type: "tableRow",
422
+ content: [
423
+ cell([{ type: "text", text: "a|b" }]),
424
+ // Code spans skip escapeText, so this pipe is only caught by the
425
+ // table-cell pass — both cells must end up single-escaped.
426
+ cell([{ type: "text", text: "x|y", marks: [{ type: "code" }] }])
427
+ ]
428
+ }]
429
+ }]))
430
+ expect(r.markdown).toContain("a\\|b")
431
+ expect(r.markdown).not.toContain("a\\\\|b")
432
+ expect(r.markdown).toContain("`x\\|y`")
433
+ })
434
+
435
+ it("renders a mediaSingle caption as an italic line under the media", () => {
436
+ const r = walk(doc([{
437
+ type: "mediaSingle",
438
+ content: [
439
+ { type: "media", attrs: { id: "m1", alt: "diagram", url: "https://x.test/d.png" } },
440
+ { type: "caption", content: [{ type: "text", text: "Figure 1" }] }
441
+ ]
442
+ }]))
443
+ expect(r.markdown).toContain("![diagram](https://x.test/d.png)\n_Figure 1_")
444
+ })
445
+
446
+ it("renders layout sections and columns as visible markdown content", () => {
447
+ const r = walk(doc([{
448
+ type: "layoutSection",
449
+ content: [
450
+ {
451
+ type: "layoutColumn",
452
+ attrs: { width: 50 },
453
+ content: [{ type: "paragraph", content: [{ type: "text", text: "left" }] }]
454
+ },
455
+ {
456
+ type: "layoutColumn",
457
+ attrs: { width: 50 },
458
+ content: [{ type: "paragraph", content: [{ type: "text", text: "right" }] }]
459
+ }
460
+ ]
461
+ }]))
462
+ expect(r.markdown).toContain("left\n\nright")
463
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "layoutSection")).toBe(false)
464
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "layoutColumn")).toBe(false)
465
+ })
466
+
467
+ it("renders block and embed smart cards from direct and nested urls", () => {
468
+ const blockCard = { type: "blockCard", attrs: { url: "https://x.test/block" } }
469
+ const embedCard = { type: "embedCard", attrs: { data: { url: "https://x.test/embed" } } }
470
+ const r = walk(doc([
471
+ blockCard,
472
+ embedCard
473
+ ]))
474
+ expect(r.markdown).toContain(`<!-- adf:blockCard node=${stableStringify(blockCard)} -->`)
475
+ expect(r.markdown).toContain("<https://x.test/block>")
476
+ expect(r.markdown).toContain(`<!-- adf:embedCard node=${stableStringify(embedCard)} -->`)
477
+ expect(r.markdown).toContain("<https://x.test/embed>")
478
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "blockCard")).toBe(false)
479
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "embedCard")).toBe(false)
480
+ })
481
+
482
+ it("backslash-escapes nestedExpand titles inside table cells (inline HTML context)", () => {
483
+ const r = walk(doc([{
484
+ type: "table",
485
+ content: [{
486
+ type: "tableRow",
487
+ content: [{
488
+ type: "tableCell",
489
+ content: [{
490
+ type: "nestedExpand",
491
+ attrs: { title: "v2 *beta*" },
492
+ content: [{ type: "paragraph", content: [{ type: "text", text: "inner" }] }]
493
+ }]
494
+ }]
495
+ }]
496
+ }]))
497
+ expect(r.markdown).toContain("<summary>v2 \\*beta\\*</summary>")
498
+ })
499
+
500
+ it("entity-escapes expand titles instead of backslash-escaping them", () => {
501
+ const r = walk(doc([{
502
+ type: "expand",
503
+ attrs: { title: `v2 *beta* <a href="x">` },
504
+ content: [{ type: "paragraph", content: [{ type: "text", text: "inner" }] }]
505
+ }]))
506
+ expect(r.markdown).toContain(`<!-- adf:expand node=`)
507
+ expect(r.markdown).toContain(`v2 *beta* <a href="x">`)
508
+ expect(r.markdown).not.toContain("\\*beta\\*")
509
+ })
510
+
511
+ it("lengthens the code-block fence when the code contains backtick runs", () => {
512
+ const r = walk(doc([{
513
+ type: "codeBlock",
514
+ attrs: { language: "md" },
515
+ content: [{ type: "text", text: "```js\ncode\n```" }]
516
+ }]))
517
+ expect(r.markdown).toContain("````md\n```js\ncode\n```\n````")
518
+ })
519
+
520
+ it("sanitizes media alt text and wraps unsafe media urls", () => {
521
+ // Brackets are substituted, not backslash-escaped: @atlaskit's media
522
+ // markdown plugin throws on `\[` in alt, which would make pushes fail.
523
+ const r = walk(doc([{
524
+ type: "mediaSingle",
525
+ content: [{
526
+ type: "media",
527
+ attrs: { id: "m1", alt: "a [b]\nc", url: "https://x.test/a (1).png" }
528
+ }]
529
+ }]))
530
+ expect(r.markdown).toContain("![a (b) c](<https://x.test/a (1).png>)")
531
+ })
532
+
533
+ it("percent-encodes wrapper-breaking characters in unsafe urls", () => {
534
+ const r = walk(doc([{
535
+ type: "paragraph",
536
+ content: [{
537
+ type: "text",
538
+ text: "go",
539
+ marks: [{ type: "link", attrs: { href: "https://x.test/a b<c>d\\e" } }]
540
+ }]
541
+ }]))
542
+ expect(r.markdown).toContain("[go](<https://x.test/a b%3Cc%3Ed%5Ce>)")
543
+ })
544
+
545
+ it("escapes block markers at line start so text cannot become structure", () => {
546
+ const r = walk(doc([
547
+ { type: "paragraph", content: [{ type: "text", text: "# not a heading" }] },
548
+ { type: "paragraph", content: [{ type: "text", text: "> not a quote" }] },
549
+ { type: "paragraph", content: [{ type: "text", text: "+ not a list" }] },
550
+ { type: "paragraph", content: [{ type: "text", text: "1. not a list" }] },
551
+ {
552
+ type: "paragraph",
553
+ content: [
554
+ { type: "text", text: "a" },
555
+ { type: "hardBreak" },
556
+ { type: "text", text: "# b stays text" }
557
+ ]
558
+ }
559
+ ]))
560
+ expect(r.markdown).toContain("\\# not a heading")
561
+ expect(r.markdown).toContain("\\> not a quote")
562
+ expect(r.markdown).toContain("\\+ not a list")
563
+ expect(r.markdown).toContain("1\\. not a list")
564
+ expect(r.markdown).toContain("\\# b stays text")
565
+ })
566
+
567
+ it("does not escape mid-line hashes or list-like text after the first word", () => {
568
+ const r = walk(doc([{ type: "paragraph", content: [{ type: "text", text: "a #1 > #2 + b" }] }]))
569
+ expect(r.markdown).toContain("a #1 > #2 + b")
570
+ })
571
+
572
+ it("strips backticks and whitespace from the code-block language", () => {
573
+ const r = walk(doc([{
574
+ type: "codeBlock",
575
+ attrs: { language: "c`x\ninjected" },
576
+ content: [{ type: "text", text: "hello" }]
577
+ }]))
578
+ expect(r.markdown).toContain("```cxinjected\nhello\n```")
579
+ })
580
+
581
+ it("warns when an inlineCard has no url to render", () => {
582
+ const r = walk(doc([{
583
+ type: "paragraph",
584
+ content: [
585
+ { type: "text", text: "before " },
586
+ { type: "inlineCard", attrs: { data: { title: "hidden" } } },
587
+ { type: "text", text: " after" }
588
+ ]
589
+ }]))
590
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "inlineCard")).toBe(true)
591
+ })
592
+
593
+ it("renders inline cards from nested data urls", () => {
594
+ const r = walk(doc([{
595
+ type: "paragraph",
596
+ content: [
597
+ { type: "text", text: "see " },
598
+ { type: "inlineCard", attrs: { data: { url: "https://x.test/inline" } } }
599
+ ]
600
+ }]))
601
+ expect(r.markdown).toContain(
602
+ `see <!-- adf:inlineCard attrs=${stableStringify({ data: { url: "https://x.test/inline" } })} -->`
603
+ )
604
+ expect(r.warnings.some((w) => w._tag === "UnsupportedNode" && w.nodeType === "inlineCard")).toBe(false)
605
+ })
606
+
607
+ it("wraps paragraph-level marks so alignment and indentation survive push", () => {
608
+ const marks = [{ type: "alignment", attrs: { align: "center" } }]
609
+ const r = walk(doc([{
610
+ type: "paragraph",
611
+ marks,
612
+ content: [{ type: "text", text: "centered" }]
613
+ }]))
614
+ expect(r.markdown).toContain(`<!-- adf:paragraph marks=${stableStringify(marks)} -->`)
615
+ expect(r.markdown).toContain("centered")
616
+ expect(r.markdown).toContain("<!-- adf:/paragraph -->")
617
+ })
618
+
619
+ it("does not double-wrap an em-marked caption", () => {
620
+ const r = walk(doc([{
621
+ type: "mediaSingle",
622
+ content: [
623
+ { type: "media", attrs: { id: "m1", url: "https://x.test/d.png" } },
624
+ { type: "caption", content: [{ type: "text", text: "a caption", marks: [{ type: "em" }] }] }
625
+ ]
626
+ }]))
627
+ expect(r.markdown).toContain("_a caption_")
628
+ expect(r.markdown).not.toContain("__a caption__")
629
+ })
630
+
631
+ it("omits the caption line when the caption is only whitespace", () => {
632
+ const r = walk(doc([{
633
+ type: "mediaSingle",
634
+ content: [
635
+ { type: "media", attrs: { id: "m1", url: "https://x.test/d.png" } },
636
+ { type: "caption", content: [{ type: "text", text: " " }] }
637
+ ]
638
+ }]))
639
+ expect(r.markdown).toContain("![](https://x.test/d.png)")
640
+ expect(r.markdown).not.toContain("_")
641
+ })
642
+
643
+ it("never renders a caption as the media when the media child is missing", () => {
644
+ const r = walk(doc([{
645
+ type: "mediaSingle",
646
+ content: [{ type: "caption", content: [{ type: "text", text: "orphan" }] }]
647
+ }]))
648
+ expect(r.markdown).toContain("<!-- adf:media id= -->")
649
+ expect(r.markdown).toContain("_orphan_")
650
+ })
651
+
652
+ it("preserves note and success panel types", () => {
653
+ const r = walk(doc([
654
+ {
655
+ type: "panel",
656
+ attrs: { panelType: "note" },
657
+ content: [{ type: "paragraph", content: [{ type: "text", text: "n" }] }]
658
+ },
659
+ {
660
+ type: "panel",
661
+ attrs: { panelType: "success" },
662
+ content: [{ type: "paragraph", content: [{ type: "text", text: "s" }] }]
663
+ }
664
+ ]))
665
+ expect(r.markdown).toContain(`<!-- adf:panel type=note attrs=${stableStringify({ panelType: "note" })} -->`)
666
+ expect(r.markdown).toContain(
667
+ `<!-- adf:panel type=success attrs=${stableStringify({ panelType: "success" })} -->`
668
+ )
669
+ })
670
+
671
+ it("ends output with exactly one newline", () => {
672
+ const r = walk(doc([{ type: "paragraph", content: [{ type: "text", text: "x" }] }]))
673
+ expect(r.markdown.endsWith("\n")).toBe(true)
674
+ expect(r.markdown.endsWith("\n\n")).toBe(false)
675
+ })
676
+ })