@knpkv/confluence-to-markdown 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +22 -13
  4. package/dist/AdfPlaceholders.d.ts +42 -0
  5. package/dist/AdfPlaceholders.d.ts.map +1 -0
  6. package/dist/AdfPlaceholders.js +547 -0
  7. package/dist/AdfPlaceholders.js.map +1 -0
  8. package/dist/AdfSchemaValidator.d.ts +37 -0
  9. package/dist/AdfSchemaValidator.d.ts.map +1 -0
  10. package/dist/AdfSchemaValidator.js +37 -0
  11. package/dist/AdfSchemaValidator.js.map +1 -0
  12. package/dist/AdfWalker.d.ts +39 -0
  13. package/dist/AdfWalker.d.ts.map +1 -0
  14. package/dist/AdfWalker.js +527 -0
  15. package/dist/AdfWalker.js.map +1 -0
  16. package/dist/AtlaskitTransformers.d.ts +35 -0
  17. package/dist/AtlaskitTransformers.d.ts.map +1 -0
  18. package/dist/AtlaskitTransformers.js +48 -0
  19. package/dist/AtlaskitTransformers.js.map +1 -0
  20. package/dist/Brand.d.ts +6 -6
  21. package/dist/Brand.d.ts.map +1 -1
  22. package/dist/Brand.js +8 -6
  23. package/dist/Brand.js.map +1 -1
  24. package/dist/ConfluenceAuth.d.ts +4 -4
  25. package/dist/ConfluenceAuth.d.ts.map +1 -1
  26. package/dist/ConfluenceAuth.js +37 -39
  27. package/dist/ConfluenceAuth.js.map +1 -1
  28. package/dist/ConfluenceClient.d.ts +7 -17
  29. package/dist/ConfluenceClient.d.ts.map +1 -1
  30. package/dist/ConfluenceClient.js +81 -38
  31. package/dist/ConfluenceClient.js.map +1 -1
  32. package/dist/ConfluenceConfig.d.ts +3 -3
  33. package/dist/ConfluenceConfig.d.ts.map +1 -1
  34. package/dist/ConfluenceConfig.js +13 -11
  35. package/dist/ConfluenceConfig.js.map +1 -1
  36. package/dist/ConfluenceError.d.ts +68 -16
  37. package/dist/ConfluenceError.d.ts.map +1 -1
  38. package/dist/ConfluenceError.js +30 -1
  39. package/dist/ConfluenceError.js.map +1 -1
  40. package/dist/GitError.d.ts +5 -5
  41. package/dist/GitService.d.ts +11 -3
  42. package/dist/GitService.d.ts.map +1 -1
  43. package/dist/GitService.js +22 -27
  44. package/dist/GitService.js.map +1 -1
  45. package/dist/LocalFileSystem.d.ts +3 -3
  46. package/dist/LocalFileSystem.d.ts.map +1 -1
  47. package/dist/LocalFileSystem.js +6 -6
  48. package/dist/LocalFileSystem.js.map +1 -1
  49. package/dist/MarkdownConverter.d.ts +16 -65
  50. package/dist/MarkdownConverter.d.ts.map +1 -1
  51. package/dist/MarkdownConverter.js +64 -85
  52. package/dist/MarkdownConverter.js.map +1 -1
  53. package/dist/Schemas.d.ts +128 -141
  54. package/dist/Schemas.d.ts.map +1 -1
  55. package/dist/Schemas.js +21 -23
  56. package/dist/Schemas.js.map +1 -1
  57. package/dist/SyncEngine.d.ts +8 -5
  58. package/dist/SyncEngine.d.ts.map +1 -1
  59. package/dist/SyncEngine.js +189 -113
  60. package/dist/SyncEngine.js.map +1 -1
  61. package/dist/bin.js +23 -35
  62. package/dist/bin.js.map +1 -1
  63. package/dist/commands/auth.d.ts +2 -14
  64. package/dist/commands/auth.d.ts.map +1 -1
  65. package/dist/commands/auth.js +11 -16
  66. package/dist/commands/auth.js.map +1 -1
  67. package/dist/commands/clone.d.ts +4 -6
  68. package/dist/commands/clone.d.ts.map +1 -1
  69. package/dist/commands/clone.js +34 -32
  70. package/dist/commands/clone.js.map +1 -1
  71. package/dist/commands/delete.d.ts +2 -10
  72. package/dist/commands/delete.d.ts.map +1 -1
  73. package/dist/commands/delete.js +5 -4
  74. package/dist/commands/delete.js.map +1 -1
  75. package/dist/commands/errorHandler.d.ts +2 -1
  76. package/dist/commands/errorHandler.d.ts.map +1 -1
  77. package/dist/commands/errorHandler.js +22 -15
  78. package/dist/commands/errorHandler.js.map +1 -1
  79. package/dist/commands/fetch.d.ts +27 -0
  80. package/dist/commands/fetch.d.ts.map +1 -0
  81. package/dist/commands/fetch.js +48 -0
  82. package/dist/commands/fetch.js.map +1 -0
  83. package/dist/commands/git.d.ts +7 -10
  84. package/dist/commands/git.d.ts.map +1 -1
  85. package/dist/commands/git.js +6 -6
  86. package/dist/commands/git.js.map +1 -1
  87. package/dist/commands/index.d.ts +1 -0
  88. package/dist/commands/index.d.ts.map +1 -1
  89. package/dist/commands/index.js +1 -0
  90. package/dist/commands/index.js.map +1 -1
  91. package/dist/commands/layers.d.ts +10 -9
  92. package/dist/commands/layers.d.ts.map +1 -1
  93. package/dist/commands/layers.js +41 -30
  94. package/dist/commands/layers.js.map +1 -1
  95. package/dist/commands/new.d.ts +2 -6
  96. package/dist/commands/new.d.ts.map +1 -1
  97. package/dist/commands/new.js +5 -4
  98. package/dist/commands/new.js.map +1 -1
  99. package/dist/commands/pageInput.d.ts +19 -0
  100. package/dist/commands/pageInput.d.ts.map +1 -0
  101. package/dist/commands/pageInput.js +68 -0
  102. package/dist/commands/pageInput.js.map +1 -0
  103. package/dist/commands/root.d.ts +8 -0
  104. package/dist/commands/root.d.ts.map +1 -0
  105. package/dist/commands/root.js +29 -0
  106. package/dist/commands/root.js.map +1 -0
  107. package/dist/commands/sync.d.ts +6 -9
  108. package/dist/commands/sync.d.ts.map +1 -1
  109. package/dist/commands/sync.js +5 -6
  110. package/dist/commands/sync.js.map +1 -1
  111. package/dist/index.d.ts +3 -8
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -13
  114. package/dist/index.js.map +1 -1
  115. package/dist/internal/NodeLayers.d.ts.map +1 -1
  116. package/dist/internal/NodeLayers.js +1 -2
  117. package/dist/internal/NodeLayers.js.map +1 -1
  118. package/dist/internal/adfMetadata.d.ts +30 -0
  119. package/dist/internal/adfMetadata.d.ts.map +1 -0
  120. package/dist/internal/adfMetadata.js +126 -0
  121. package/dist/internal/adfMetadata.js.map +1 -0
  122. package/dist/internal/cleanMarkdown.d.ts +5 -0
  123. package/dist/internal/cleanMarkdown.d.ts.map +1 -0
  124. package/dist/internal/cleanMarkdown.js +13 -0
  125. package/dist/internal/cleanMarkdown.js.map +1 -0
  126. package/dist/internal/frontmatter.d.ts.map +1 -1
  127. package/dist/internal/frontmatter.js +41 -8
  128. package/dist/internal/frontmatter.js.map +1 -1
  129. package/dist/internal/gitCommands.d.ts +9 -3
  130. package/dist/internal/gitCommands.d.ts.map +1 -1
  131. package/dist/internal/gitCommands.js +18 -9
  132. package/dist/internal/gitCommands.js.map +1 -1
  133. package/dist/internal/hashUtils.d.ts +1 -1
  134. package/dist/internal/hashUtils.d.ts.map +1 -1
  135. package/dist/internal/hashUtils.js +1 -1
  136. package/dist/internal/hashUtils.js.map +1 -1
  137. package/dist/internal/oauthServer.d.ts +10 -5
  138. package/dist/internal/oauthServer.d.ts.map +1 -1
  139. package/dist/internal/oauthServer.js +19 -40
  140. package/dist/internal/oauthServer.js.map +1 -1
  141. package/dist/internal/pathUtils.d.ts +1 -1
  142. package/dist/internal/pathUtils.d.ts.map +1 -1
  143. package/dist/internal/pathUtils.js +1 -1
  144. package/dist/internal/pathUtils.js.map +1 -1
  145. package/dist/internal/process.d.ts +15 -0
  146. package/dist/internal/process.d.ts.map +1 -0
  147. package/dist/internal/process.js +10 -0
  148. package/dist/internal/process.js.map +1 -0
  149. package/dist/internal/stdio.d.ts +6 -0
  150. package/dist/internal/stdio.d.ts.map +1 -0
  151. package/dist/internal/stdio.js +15 -0
  152. package/dist/internal/stdio.js.map +1 -0
  153. package/dist/internal/tokenStorage.d.ts +3 -13
  154. package/dist/internal/tokenStorage.d.ts.map +1 -1
  155. package/dist/internal/tokenStorage.js +26 -24
  156. package/dist/internal/tokenStorage.js.map +1 -1
  157. package/dist/internal/userCache.d.ts +1 -1
  158. package/dist/internal/userCache.d.ts.map +1 -1
  159. package/dist/internal/userCache.js +1 -1
  160. package/dist/internal/userCache.js.map +1 -1
  161. package/package.json +30 -30
  162. package/skills/confluence/SKILL.md +143 -0
  163. package/skills/confluence/agents/openai.yaml +4 -0
  164. package/src/AdfPlaceholders.ts +310 -13
  165. package/src/AdfSchemaValidator.ts +2 -4
  166. package/src/AdfWalker.ts +122 -42
  167. package/src/AtlaskitTransformers.ts +2 -4
  168. package/src/Brand.ts +11 -16
  169. package/src/ConfluenceAuth.ts +22 -30
  170. package/src/ConfluenceClient.ts +24 -20
  171. package/src/ConfluenceConfig.ts +14 -14
  172. package/src/GitService.ts +39 -49
  173. package/src/LocalFileSystem.ts +7 -9
  174. package/src/MarkdownConverter.ts +2 -4
  175. package/src/Schemas.ts +13 -12
  176. package/src/SyncEngine.ts +151 -53
  177. package/src/bin.ts +30 -56
  178. package/src/commands/auth.ts +21 -18
  179. package/src/commands/clone.ts +38 -37
  180. package/src/commands/delete.ts +5 -4
  181. package/src/commands/errorHandler.ts +25 -18
  182. package/src/commands/fetch.ts +90 -0
  183. package/src/commands/git.ts +6 -6
  184. package/src/commands/index.ts +1 -0
  185. package/src/commands/layers.ts +53 -33
  186. package/src/commands/new.ts +5 -4
  187. package/src/commands/pageInput.ts +103 -0
  188. package/src/commands/root.ts +59 -0
  189. package/src/commands/sync.ts +7 -6
  190. package/src/internal/NodeLayers.ts +1 -2
  191. package/src/internal/adfMetadata.ts +145 -0
  192. package/src/internal/cleanMarkdown.ts +15 -0
  193. package/src/internal/frontmatter.ts +45 -8
  194. package/src/internal/gitCommands.ts +23 -17
  195. package/src/internal/hashUtils.ts +2 -2
  196. package/src/internal/oauthServer.ts +84 -105
  197. package/src/internal/pathUtils.ts +1 -1
  198. package/src/internal/process.ts +15 -0
  199. package/src/internal/stdio.ts +22 -0
  200. package/src/internal/tokenStorage.ts +39 -29
  201. package/src/internal/userCache.ts +2 -2
  202. package/test/AdfPlaceholders.test.ts +213 -0
  203. package/test/AdfSchemaValidator.test.ts +6 -6
  204. package/test/AdfWalker.test.ts +167 -21
  205. package/test/AtlaskitTransformers.test.ts +4 -4
  206. package/test/Brand.test.ts +11 -11
  207. package/test/GitService.test.ts +6 -2
  208. package/test/MarkdownConverter.test.ts +12 -11
  209. package/test/RoundTrip.test.ts +258 -3
  210. package/test/Schemas.test.ts +40 -40
  211. package/test/adfMetadata.test.ts +110 -0
  212. package/test/cleanMarkdown.test.ts +36 -0
  213. package/test/commandHarness.test.ts +79 -0
  214. package/test/commandHarness.ts +147 -0
  215. package/test/fetch.test.ts +61 -0
  216. package/test/frontmatter.test.ts +41 -0
  217. package/test/integration.test.ts +569 -156
  218. package/test/layers.test.ts +12 -0
  219. package/test/oauthServer.test.ts +4 -5
  220. package/test/pageInput.test.ts +83 -0
  221. package/test/tokenStorage.test.ts +17 -17
  222. package/dist/SchemaConverterError.d.ts +0 -108
  223. package/dist/SchemaConverterError.d.ts.map +0 -1
  224. package/dist/SchemaConverterError.js +0 -84
  225. package/dist/SchemaConverterError.js.map +0 -1
  226. package/dist/ast/BlockNode.d.ts +0 -468
  227. package/dist/ast/BlockNode.d.ts.map +0 -1
  228. package/dist/ast/BlockNode.js +0 -319
  229. package/dist/ast/BlockNode.js.map +0 -1
  230. package/dist/ast/Document.d.ts +0 -244
  231. package/dist/ast/Document.d.ts.map +0 -1
  232. package/dist/ast/Document.js +0 -69
  233. package/dist/ast/Document.js.map +0 -1
  234. package/dist/ast/InlineNode.d.ts +0 -477
  235. package/dist/ast/InlineNode.d.ts.map +0 -1
  236. package/dist/ast/InlineNode.js +0 -263
  237. package/dist/ast/InlineNode.js.map +0 -1
  238. package/dist/ast/MacroNode.d.ts +0 -267
  239. package/dist/ast/MacroNode.d.ts.map +0 -1
  240. package/dist/ast/MacroNode.js +0 -164
  241. package/dist/ast/MacroNode.js.map +0 -1
  242. package/dist/ast/index.d.ts +0 -10
  243. package/dist/ast/index.d.ts.map +0 -1
  244. package/dist/ast/index.js +0 -14
  245. package/dist/ast/index.js.map +0 -1
  246. package/dist/parsers/ConfluenceParser.d.ts +0 -26
  247. package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
  248. package/dist/parsers/ConfluenceParser.js +0 -792
  249. package/dist/parsers/ConfluenceParser.js.map +0 -1
  250. package/dist/parsers/MarkdownParser.d.ts +0 -26
  251. package/dist/parsers/MarkdownParser.d.ts.map +0 -1
  252. package/dist/parsers/MarkdownParser.js +0 -873
  253. package/dist/parsers/MarkdownParser.js.map +0 -1
  254. package/dist/parsers/index.d.ts +0 -8
  255. package/dist/parsers/index.d.ts.map +0 -1
  256. package/dist/parsers/index.js +0 -8
  257. package/dist/parsers/index.js.map +0 -1
  258. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +0 -23
  259. package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +0 -1
  260. package/dist/parsers/preprocessing/ConfluencePreprocessing.js +0 -323
  261. package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +0 -1
  262. package/dist/parsers/preprocessing/index.d.ts +0 -7
  263. package/dist/parsers/preprocessing/index.d.ts.map +0 -1
  264. package/dist/parsers/preprocessing/index.js +0 -7
  265. package/dist/parsers/preprocessing/index.js.map +0 -1
  266. package/dist/schemas/ConfluenceSchema.d.ts +0 -21
  267. package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
  268. package/dist/schemas/ConfluenceSchema.js +0 -38
  269. package/dist/schemas/ConfluenceSchema.js.map +0 -1
  270. package/dist/schemas/ConversionSchema.d.ts +0 -35
  271. package/dist/schemas/ConversionSchema.d.ts.map +0 -1
  272. package/dist/schemas/ConversionSchema.js +0 -208
  273. package/dist/schemas/ConversionSchema.js.map +0 -1
  274. package/dist/schemas/MarkdownSchema.d.ts +0 -21
  275. package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
  276. package/dist/schemas/MarkdownSchema.js +0 -38
  277. package/dist/schemas/MarkdownSchema.js.map +0 -1
  278. package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
  279. package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
  280. package/dist/schemas/hast/HastFromHtml.js +0 -107
  281. package/dist/schemas/hast/HastFromHtml.js.map +0 -1
  282. package/dist/schemas/hast/HastSchema.d.ts +0 -195
  283. package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
  284. package/dist/schemas/hast/HastSchema.js +0 -183
  285. package/dist/schemas/hast/HastSchema.js.map +0 -1
  286. package/dist/schemas/hast/index.d.ts +0 -9
  287. package/dist/schemas/hast/index.d.ts.map +0 -1
  288. package/dist/schemas/hast/index.js +0 -3
  289. package/dist/schemas/hast/index.js.map +0 -1
  290. package/dist/schemas/index.d.ts +0 -14
  291. package/dist/schemas/index.d.ts.map +0 -1
  292. package/dist/schemas/index.js +0 -16
  293. package/dist/schemas/index.js.map +0 -1
  294. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
  295. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
  296. package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
  297. package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
  298. package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
  299. package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
  300. package/dist/schemas/mdast/MdastSchema.js +0 -266
  301. package/dist/schemas/mdast/MdastSchema.js.map +0 -1
  302. package/dist/schemas/mdast/index.d.ts +0 -10
  303. package/dist/schemas/mdast/index.d.ts.map +0 -1
  304. package/dist/schemas/mdast/index.js +0 -4
  305. package/dist/schemas/mdast/index.js.map +0 -1
  306. package/dist/schemas/mdast/mdastToString.d.ts +0 -13
  307. package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
  308. package/dist/schemas/mdast/mdastToString.js +0 -85
  309. package/dist/schemas/mdast/mdastToString.js.map +0 -1
  310. package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
  311. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
  312. package/dist/schemas/nodes/block/BlockSchema.js +0 -634
  313. package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
  314. package/dist/schemas/nodes/block/index.d.ts +0 -7
  315. package/dist/schemas/nodes/block/index.d.ts.map +0 -1
  316. package/dist/schemas/nodes/block/index.js +0 -7
  317. package/dist/schemas/nodes/block/index.js.map +0 -1
  318. package/dist/schemas/nodes/index.d.ts +0 -9
  319. package/dist/schemas/nodes/index.d.ts.map +0 -1
  320. package/dist/schemas/nodes/index.js +0 -12
  321. package/dist/schemas/nodes/index.js.map +0 -1
  322. package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
  323. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
  324. package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
  325. package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
  326. package/dist/schemas/nodes/inline/index.d.ts +0 -7
  327. package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
  328. package/dist/schemas/nodes/inline/index.js +0 -7
  329. package/dist/schemas/nodes/inline/index.js.map +0 -1
  330. package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
  331. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
  332. package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
  333. package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
  334. package/dist/schemas/nodes/macro/index.d.ts +0 -7
  335. package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
  336. package/dist/schemas/nodes/macro/index.js +0 -7
  337. package/dist/schemas/nodes/macro/index.js.map +0 -1
  338. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -53
  339. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
  340. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -349
  341. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
  342. package/dist/schemas/preprocessing/index.d.ts +0 -8
  343. package/dist/schemas/preprocessing/index.d.ts.map +0 -1
  344. package/dist/schemas/preprocessing/index.js +0 -2
  345. package/dist/schemas/preprocessing/index.js.map +0 -1
  346. package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
  347. package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
  348. package/dist/serializers/ConfluenceSerializer.js +0 -551
  349. package/dist/serializers/ConfluenceSerializer.js.map +0 -1
  350. package/dist/serializers/MarkdownSerializer.d.ts +0 -34
  351. package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
  352. package/dist/serializers/MarkdownSerializer.js +0 -355
  353. package/dist/serializers/MarkdownSerializer.js.map +0 -1
  354. package/dist/serializers/index.d.ts +0 -8
  355. package/dist/serializers/index.d.ts.map +0 -1
  356. package/dist/serializers/index.js +0 -8
  357. package/dist/serializers/index.js.map +0 -1
@@ -15,16 +15,30 @@
15
15
  * the whole paragraph is just this comment; `attrs` is base64 JSON of
16
16
  * the node's full attrs — parameters, localId, layout — and wins over
17
17
  * the readable key/type parts; key/type-only is the legacy form)
18
+ * - `<!-- adf:paragraph marks=BASE64 --> BODY <!-- adf:/paragraph -->`
19
+ * (the body paragraph regains its paragraph-level marks)
18
20
  * - `<!-- adf:bodiedExtension … --> BODY <!-- adf:/bodiedExtension -->`
19
21
  * (the sibling blocks between the markers become the extension's body)
22
+ * - `<!-- adf:inlineCard attrs=BASE64 -->` (inline)
20
23
  * - `<!-- adf:inlineExtension key=KEY type=TYPE attrs=BASE64 -->` (inline)
24
+ * - `<!-- adf:date node=BASE64 -->` and `<!-- adf:emoji node=BASE64 -->`
25
+ * (inline)
26
+ * - `<!-- adf:panel type=TYPE attrs=BASE64 --> BODY <!-- adf:/panel -->`
27
+ * (the sibling blocks between the markers become the panel's body)
28
+ * - `<!-- adf:TYPE node=BASE64 --> BODY <!-- adf:/TYPE -->` for selected
29
+ * native block nodes such as task/decision lists, expands, layouts,
30
+ * cards, and tables
31
+ * - `<u>TEXT</u>`, `<sub>TEXT</sub>`, `<sup>TEXT</sup>`, and exact styled
32
+ * spans emitted for Confluence-only inline marks
21
33
  * - `[@Name](confluence-mention://ACCOUNT_ID)` (link mark with a
22
34
  * custom scheme — the only way to round-trip mention accountIds)
35
+ * - `[[toc]]`, `[[toc:min=1,max=3]]` (block-level native syntax for the
36
+ * Confluence Table of Contents macro)
23
37
  *
24
38
  * @module
25
39
  */
26
40
 
27
- import * as Either from "effect/Either"
41
+ import * as Option from "effect/Option"
28
42
  import * as Schema from "effect/Schema"
29
43
 
30
44
  interface AdfNode {
@@ -36,17 +50,50 @@ interface AdfNode {
36
50
  }
37
51
 
38
52
  const STATUS_RE = /<span class="adf-status"\s+data-color="([^"]+)">([^<]*)<\/span>/g
53
+ const INLINE_NODE_RE = /<!--\s*adf:(date|emoji)(?:\s+node=([\s\S]*?))?\s*-->/g
54
+ const INLINE_CARD_RE = /<!--\s*adf:inlineCard(?:\s+attrs=([\s\S]*?))?\s*-->/g
39
55
  const INLINE_EXTENSION_RE =
40
- /<!--\s*adf:inlineExtension(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([A-Za-z0-9+/=]+))?\s*-->/g
41
- const COMBINED_INLINE_RE = new RegExp(`${STATUS_RE.source}|${INLINE_EXTENSION_RE.source}`, "g")
56
+ /<!--\s*adf:inlineExtension(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->/g
57
+ const UNDERLINE_RE = /<u>([^<]*)<\/u>/g
58
+ const SUBSCRIPT_RE = /<sub>([^<]*)<\/sub>/g
59
+ const SUPERSCRIPT_RE = /<sup>([^<]*)<\/sup>/g
60
+ const TEXT_COLOR_RE = /<span style="color:([^"<>]+)">([^<]*)<\/span>/g
61
+ const BACKGROUND_COLOR_RE = /<span style="background-color:([^"<>]+)">([^<]*)<\/span>/g
62
+ const COMBINED_INLINE_RE = new RegExp(
63
+ [
64
+ INLINE_NODE_RE.source,
65
+ STATUS_RE.source,
66
+ INLINE_CARD_RE.source,
67
+ INLINE_EXTENSION_RE.source,
68
+ UNDERLINE_RE.source,
69
+ SUBSCRIPT_RE.source,
70
+ SUPERSCRIPT_RE.source,
71
+ TEXT_COLOR_RE.source,
72
+ BACKGROUND_COLOR_RE.source
73
+ ].join("|"),
74
+ "g"
75
+ )
42
76
 
43
77
  const BLOCK_EXTENSION_RE =
44
- /^\s*<!--\s*adf:(extension|bodiedExtension)(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([A-Za-z0-9+/=]+))?\s*-->\s*$/
78
+ /^\s*<!--\s*adf:(extension|bodiedExtension)(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->\s*$/
45
79
  const BODIED_EXTENSION_END_RE = /^\s*<!--\s*adf:\/bodiedExtension\s*-->\s*$/
80
+ const PANEL_RE = /^\s*<!--\s*adf:panel(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->\s*$/
81
+ const PANEL_END_RE = /^\s*<!--\s*adf:\/panel\s*-->\s*$/
82
+ const ENCODED_BLOCK_NODE_RE =
83
+ /^\s*<!--\s*adf:(taskList|decisionList|expand|nestedExpand|table|layoutSection|blockCard|embedCard)(?:\s+node=([\s\S]*?))?\s*-->\s*$/
84
+ const ENCODED_BLOCK_NODE_END_RE =
85
+ /^\s*<!--\s*adf:\/(taskList|decisionList|expand|nestedExpand|table|layoutSection|blockCard|embedCard)\s*-->\s*$/
86
+ const PARAGRAPH_MARKS_RE = /^\s*<!--\s*adf:paragraph(?:\s+marks=([\s\S]*?))?\s*-->\s*$/
87
+ const PARAGRAPH_MARKS_END_RE = /^\s*<!--\s*adf:\/paragraph\s*-->\s*$/
88
+ const TOC_RE = /^\s*\[\[toc(?::([^\]]+))?\]\]\s*$/
89
+ const CONFLUENCE_CORE_MACRO_TYPE = "com.atlassian.confluence.macro.core"
46
90
 
47
91
  const textNode = (text: string, marks: ReadonlyArray<AdfNode> | undefined): AdfNode =>
48
92
  marks && marks.length > 0 ? { type: "text", text, marks } : { type: "text", text }
49
93
 
94
+ const addMark = (marks: ReadonlyArray<AdfNode> | undefined, mark: AdfNode): ReadonlyArray<AdfNode> =>
95
+ marks && marks.length > 0 ? [...marks, mark] : [mark]
96
+
50
97
  // Code-marked text is a *quotation* of placeholder syntax, not a placeholder
51
98
  // (the walker never emits placeholders with a code mark) — expanding it would
52
99
  // corrupt documentation that demonstrates the syntax.
@@ -65,16 +112,18 @@ const fromBase64 = (b64: string): string => {
65
112
  }
66
113
 
67
114
  // JSON string → free-form attrs record; rejects null/arrays/primitives.
68
- const AttrsBlob = Schema.parseJson(Schema.Record({ key: Schema.String, value: Schema.Unknown }))
69
- const decodeAttrsBlob = Schema.decodeUnknownEither(AttrsBlob)
115
+ const AttrsBlob = Schema.Record(Schema.String, Schema.Unknown)
116
+ const decodeAttrsBlob = Schema.decodeUnknownOption(AttrsBlob)
70
117
 
71
118
  const decodeAttrs = (b64: string | undefined): Record<string, unknown> | null => {
72
119
  if (!b64) return null
73
120
  try {
74
- const decoded = decodeAttrsBlob(fromBase64(b64))
75
- return Either.isRight(decoded) ? decoded.right : null
121
+ const raw = b64.trim()
122
+ const parsed = JSON.parse(raw.startsWith("{") ? raw : fromBase64(raw)) as unknown
123
+ const decoded = decodeAttrsBlob(parsed)
124
+ return Option.isSome(decoded) ? decoded.value : null
76
125
  } catch {
77
- // Invalid base64 (hand-edited file?) — fall back to the readable key/type.
126
+ // Invalid JSON/base64 (hand-edited file?) — fall back to the readable key/type.
78
127
  return null
79
128
  }
80
129
  }
@@ -92,6 +141,43 @@ const buildExtensionAttrs = (
92
141
  return attrs
93
142
  }
94
143
 
144
+ const buildPanelAttrs = (type: string | undefined, attrsB64: string | undefined): Record<string, unknown> => {
145
+ const decoded = decodeAttrs(attrsB64)
146
+ if (decoded) return decoded
147
+ return type ? { panelType: type } : {}
148
+ }
149
+
150
+ const buildInlineCardAttrs = (attrsB64: string | undefined): Record<string, unknown> => decodeAttrs(attrsB64) ?? {}
151
+
152
+ const decodeMarks = (b64: string | undefined): ReadonlyArray<AdfNode> => {
153
+ if (!b64) return []
154
+ try {
155
+ const raw = b64.trim()
156
+ const parsed = JSON.parse(raw.startsWith("[") ? raw : fromBase64(raw)) as unknown
157
+ return Array.isArray(parsed)
158
+ ? parsed.filter((mark): mark is AdfNode =>
159
+ mark !== null && typeof mark === "object" && typeof (mark as Record<string, unknown>)["type"] === "string"
160
+ )
161
+ : []
162
+ } catch {
163
+ return []
164
+ }
165
+ }
166
+
167
+ const decodeNode = (b64: string | undefined): AdfNode | null => {
168
+ if (!b64) return null
169
+ try {
170
+ const raw = b64.trim()
171
+ const parsed = JSON.parse(raw.startsWith("{") ? raw : fromBase64(raw)) as unknown
172
+ return parsed !== null && typeof parsed === "object" &&
173
+ typeof (parsed as Record<string, unknown>)["type"] === "string"
174
+ ? parsed as AdfNode
175
+ : null
176
+ } catch {
177
+ return null
178
+ }
179
+ }
180
+
95
181
  /** Split a text node into a sequence of text + status + inlineExtension nodes. */
96
182
  const expandInlineText = (
97
183
  text: string,
@@ -107,11 +193,30 @@ const expandInlineText = (
107
193
  if (match.index > lastIndex) {
108
194
  out.push(textNode(text.slice(lastIndex, match.index), marks))
109
195
  }
110
- // Status capture groups: 1=color, 2=text. InlineExtension: 3=key, 4=type, 5=attrs.
196
+ // Capture groups follow COMBINED_INLINE_RE order.
197
+ // InlineNode: 1=type, 2=node. Status: 3=color, 4=text.
198
+ // InlineCard: 5=attrs. InlineExtension: 6=key, 7=type, 8=attrs.
199
+ // Underline: 9=text. Sub: 10=text. Sup: 11=text.
200
+ // Text color: 12=color, 13=text. Background color: 14=color, 15=text.
111
201
  if (match[1] !== undefined) {
112
- out.push({ type: "status", attrs: { text: match[2] ?? "", color: match[1] } })
202
+ const decoded = decodeNode(match[2])
203
+ if (decoded && decoded.type === match[1]) out.push(decoded)
204
+ } else if (match[3] !== undefined) {
205
+ out.push({ type: "status", attrs: { text: match[4] ?? "", color: match[3] } })
206
+ } else if (match[5] !== undefined) {
207
+ out.push({ type: "inlineCard", attrs: buildInlineCardAttrs(match[5]) })
208
+ } else if (match[9] !== undefined) {
209
+ out.push(textNode(match[9], addMark(marks, { type: "underline" })))
210
+ } else if (match[10] !== undefined) {
211
+ out.push(textNode(match[10], addMark(marks, { type: "subsup", attrs: { type: "sub" } })))
212
+ } else if (match[11] !== undefined) {
213
+ out.push(textNode(match[11], addMark(marks, { type: "subsup", attrs: { type: "sup" } })))
214
+ } else if (match[12] !== undefined) {
215
+ out.push(textNode(match[13] ?? "", addMark(marks, { type: "textColor", attrs: { color: match[12] } })))
216
+ } else if (match[14] !== undefined) {
217
+ out.push(textNode(match[15] ?? "", addMark(marks, { type: "backgroundColor", attrs: { color: match[14] } })))
113
218
  } else {
114
- out.push({ type: "inlineExtension", attrs: buildExtensionAttrs(match[3], match[4], match[5]) })
219
+ out.push({ type: "inlineExtension", attrs: buildExtensionAttrs(match[6], match[7], match[8]) })
115
220
  }
116
221
  lastIndex = match.index + match[0].length
117
222
  }
@@ -173,11 +278,96 @@ const parseBlockExtensionParagraph = (node: AdfNode): BlockExtensionMarker | nul
173
278
  }
174
279
  }
175
280
 
281
+ const parseTocParagraph = (node: AdfNode): AdfNode | null => {
282
+ const child = soleTextChild(node)
283
+ if (!child || !child.text) return null
284
+ const match = TOC_RE.exec(child.text)
285
+ if (!match) return null
286
+
287
+ let minLevel: string | undefined
288
+ let maxLevel: string | undefined
289
+ const params = match[1]?.trim()
290
+
291
+ if (params && params.length > 0) {
292
+ const seen = new Set<string>()
293
+ for (const part of params.split(",")) {
294
+ const [rawKey, rawValue, ...rest] = part.split("=")
295
+ if (rest.length > 0) return null
296
+ const key = rawKey?.trim()
297
+ const value = rawValue?.trim()
298
+ if ((key !== "min" && key !== "max") || !value || !/^[1-6]$/.test(value) || seen.has(key)) return null
299
+ seen.add(key)
300
+ if (key === "min") minLevel = value
301
+ else maxLevel = value
302
+ }
303
+ } else if (params !== undefined) {
304
+ return null
305
+ }
306
+
307
+ const macroParams: Record<string, { readonly value: string }> = {}
308
+ if (minLevel) macroParams.minLevel = { value: minLevel }
309
+ if (maxLevel) macroParams.maxLevel = { value: maxLevel }
310
+
311
+ const attrs: Record<string, unknown> = {
312
+ extensionKey: "toc",
313
+ extensionType: CONFLUENCE_CORE_MACRO_TYPE
314
+ }
315
+ if (Object.keys(macroParams).length > 0) {
316
+ attrs.parameters = { macroParams }
317
+ }
318
+
319
+ return { type: "extension", attrs }
320
+ }
321
+
176
322
  const isBodiedExtensionEnd = (node: AdfNode): boolean => {
177
323
  const child = soleTextChild(node)
178
324
  return child !== null && typeof child.text === "string" && BODIED_EXTENSION_END_RE.test(child.text)
179
325
  }
180
326
 
327
+ const parsePanelParagraph = (node: AdfNode): Record<string, unknown> | null => {
328
+ const child = soleTextChild(node)
329
+ if (!child || !child.text) return null
330
+ const match = PANEL_RE.exec(child.text)
331
+ if (!match) return null
332
+ const [, type, attrsB64] = match
333
+ return buildPanelAttrs(type, attrsB64)
334
+ }
335
+
336
+ const isPanelEnd = (node: AdfNode): boolean => {
337
+ const child = soleTextChild(node)
338
+ return child !== null && typeof child.text === "string" && PANEL_END_RE.test(child.text)
339
+ }
340
+
341
+ const parseEncodedBlockNodeParagraph = (node: AdfNode): { readonly type: string; readonly node: AdfNode } | null => {
342
+ const child = soleTextChild(node)
343
+ if (!child || !child.text) return null
344
+ const match = ENCODED_BLOCK_NODE_RE.exec(child.text)
345
+ if (!match) return null
346
+ const decoded = decodeNode(match[2])
347
+ if (!decoded || decoded.type !== match[1]) return null
348
+ return { type: match[1]!, node: decoded }
349
+ }
350
+
351
+ const isEncodedBlockNodeEnd = (node: AdfNode, type: string): boolean => {
352
+ const child = soleTextChild(node)
353
+ if (child === null || typeof child.text !== "string") return false
354
+ const match = ENCODED_BLOCK_NODE_END_RE.exec(child.text)
355
+ return match?.[1] === type
356
+ }
357
+
358
+ const parseParagraphMarksParagraph = (node: AdfNode): ReadonlyArray<AdfNode> | null => {
359
+ const child = soleTextChild(node)
360
+ if (!child || !child.text) return null
361
+ const match = PARAGRAPH_MARKS_RE.exec(child.text)
362
+ if (!match) return null
363
+ return decodeMarks(match[1])
364
+ }
365
+
366
+ const isParagraphMarksEnd = (node: AdfNode): boolean => {
367
+ const child = soleTextChild(node)
368
+ return child !== null && typeof child.text === "string" && PARAGRAPH_MARKS_END_RE.test(child.text)
369
+ }
370
+
181
371
  /**
182
372
  * Replace block-extension marker paragraphs among `children`. A bare
183
373
  * `extension` marker becomes an extension node; a `bodiedExtension` marker
@@ -237,6 +427,110 @@ const groupBlockExtensions = (children: ReadonlyArray<AdfNode>, parentType: stri
237
427
  return out
238
428
  }
239
429
 
430
+ const groupPanels = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
431
+ const out: Array<AdfNode> = []
432
+ for (let i = 0; i < children.length; i++) {
433
+ const child = children[i]
434
+ if (!child) continue
435
+ const attrs = parsePanelParagraph(child)
436
+ if (!attrs) {
437
+ if (!isPanelEnd(child)) out.push(child)
438
+ continue
439
+ }
440
+
441
+ let end = -1
442
+ for (let j = i + 1; j < children.length; j++) {
443
+ if (isPanelEnd(children[j]!)) {
444
+ end = j
445
+ break
446
+ }
447
+ if (parsePanelParagraph(children[j]!)) break
448
+ }
449
+
450
+ if (end === -1) {
451
+ out.push({ type: "panel", attrs, content: [{ type: "paragraph", content: [] }] })
452
+ continue
453
+ }
454
+
455
+ const body = groupPanels(children.slice(i + 1, end))
456
+ out.push({
457
+ type: "panel",
458
+ attrs,
459
+ content: body.length > 0 ? body : [{ type: "paragraph", content: [] }]
460
+ })
461
+ i = end
462
+ }
463
+ return out
464
+ }
465
+
466
+ const groupEncodedBlockNodes = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
467
+ const out: Array<AdfNode> = []
468
+ for (let i = 0; i < children.length; i++) {
469
+ const child = children[i]
470
+ if (!child) continue
471
+ const marker = parseEncodedBlockNodeParagraph(child)
472
+ if (!marker) {
473
+ out.push(child)
474
+ continue
475
+ }
476
+
477
+ let end = -1
478
+ for (let j = i + 1; j < children.length; j++) {
479
+ if (isEncodedBlockNodeEnd(children[j]!, marker.type)) {
480
+ end = j
481
+ break
482
+ }
483
+ if (parseEncodedBlockNodeParagraph(children[j]!) !== null) break
484
+ }
485
+
486
+ out.push(marker.node)
487
+ if (end !== -1) i = end
488
+ }
489
+ return out
490
+ }
491
+
492
+ const groupMarkedParagraphs = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
493
+ const out: Array<AdfNode> = []
494
+ for (let i = 0; i < children.length; i++) {
495
+ const child = children[i]
496
+ if (!child) continue
497
+ const marks = parseParagraphMarksParagraph(child)
498
+ if (!marks) {
499
+ if (!isParagraphMarksEnd(child)) out.push(child)
500
+ continue
501
+ }
502
+
503
+ let end = -1
504
+ for (let j = i + 1; j < children.length; j++) {
505
+ if (isParagraphMarksEnd(children[j]!)) {
506
+ end = j
507
+ break
508
+ }
509
+ if (parseParagraphMarksParagraph(children[j]!) !== null) break
510
+ }
511
+
512
+ if (end === -1) {
513
+ out.push({ type: "paragraph", marks, content: [] })
514
+ continue
515
+ }
516
+
517
+ const body = children.slice(i + 1, end)
518
+ const first = body[0]
519
+ if (first?.type === "paragraph") {
520
+ out.push(marks.length > 0 ? { ...first, marks } : first)
521
+ for (const rest of body.slice(1)) out.push(rest)
522
+ } else {
523
+ out.push({ type: "paragraph", marks, content: [] })
524
+ for (const rest of body) out.push(rest)
525
+ }
526
+ i = end
527
+ }
528
+ return out
529
+ }
530
+
531
+ const groupNativeMacros = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> =>
532
+ children.map((child) => parseTocParagraph(child) ?? child)
533
+
240
534
  const transform = (node: AdfNode): AdfNode => {
241
535
  // ADF codeBlock permits only plain text children — expanding placeholder-
242
536
  // looking text inside one would inject schema-invalid nodes and corrupt
@@ -259,7 +553,10 @@ const transform = (node: AdfNode): AdfNode => {
259
553
  newContent.push(transform(child))
260
554
  }
261
555
  }
262
- return { ...node, content: groupBlockExtensions(newContent, node.type) }
556
+ const nativeMacrosRestored = groupNativeMacros(newContent)
557
+ const paragraphsRestored = groupMarkedParagraphs(nativeMacrosRestored)
558
+ const encodedBlocksRestored = groupEncodedBlockNodes(paragraphsRestored)
559
+ return { ...node, content: groupPanels(groupBlockExtensions(encodedBlocksRestored, node.type)) }
263
560
  }
264
561
 
265
562
  /** Walk the document tree and rewrite placeholder text into proper ADF nodes. */
@@ -27,9 +27,7 @@ const validate = ajv.compile(adfJsonSchema as object)
27
27
  *
28
28
  * @category Service
29
29
  */
30
- export class AdfSchemaValidator extends Context.Tag(
31
- "@knpkv/confluence-to-markdown/AdfSchemaValidator"
32
- )<
30
+ export class AdfSchemaValidator extends Context.Service<
33
31
  AdfSchemaValidator,
34
32
  {
35
33
  readonly check: (
@@ -37,7 +35,7 @@ export class AdfSchemaValidator extends Context.Tag(
37
35
  direction: "incoming" | "outgoing"
38
36
  ) => Effect.Effect<DocNode, AdfSchemaError>
39
37
  }
40
- >() {}
38
+ >()("@knpkv/confluence-to-markdown/AdfSchemaValidator") {}
41
39
 
42
40
  /**
43
41
  * Live Layer for `AdfSchemaValidator`. The Ajv validator is compiled once at
package/src/AdfWalker.ts CHANGED
@@ -93,31 +93,17 @@ const attrNum = (n: AdfNode, key: string): number | undefined => {
93
93
  const v = n.attrs?.[key]
94
94
  return typeof v === "number" ? v : undefined
95
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
+ }
96
100
 
97
- // Color-matched to GitHub's admonition palette: info/blue→NOTE, note/purple→
98
- // IMPORTANT, success/green→TIP, warning/yellow→WARNING, error/red→CAUTION.
99
- const PANEL_MAP: Record<string, string> = {
100
- info: "NOTE",
101
- note: "IMPORTANT",
102
- warning: "WARNING",
103
- success: "TIP",
104
- error: "CAUTION"
105
- }
106
-
107
- // btoa operates on byte strings; route through TextEncoder so non-ASCII attrs
108
- // survive. Web APIs only — this module is a standalone subpath export and
109
- // must not assume Node (same reasoning as internal/hashUtils' Web Crypto).
110
- const toBase64 = (s: string): string => {
111
- const bytes = new TextEncoder().encode(s)
112
- let bin = ""
113
- for (const b of bytes) bin += String.fromCharCode(b)
114
- return btoa(bin)
115
- }
116
-
117
- // Deterministic JSON for the placeholder attrs blob: object keys are sorted
118
- // recursively so the same attrs always produce the same base64, no matter
119
- // what order Confluence happens to serialize them in. Keeps pull → push →
120
- // pull a byte-level fixed point (and contentHash stable).
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).
121
107
  const stableStringify = (v: unknown): string => {
122
108
  if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`
123
109
  if (v !== null && typeof v === "object") {
@@ -163,24 +149,21 @@ const inlineNode = (n: AdfNode, ctx: Ctx): string => {
163
149
  return id ? `[${display}](confluence-mention://${encodeURIComponent(id)})` : display
164
150
  }
165
151
  case "emoji": {
166
- const short = attrStr(n, "shortName")
167
- return short ? `:${short}:` : (attrStr(n, "text") ?? "")
152
+ return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
168
153
  }
169
154
  case "inlineCard": {
170
- const url = attrStr(n, "url")
155
+ const url = cardUrl(n)
171
156
  if (!url) {
172
157
  // data-payload smart links have no URL to render — losing one must
173
158
  // at least be visible in the logs.
174
159
  ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: "inlineCard" })
175
160
  return ""
176
161
  }
177
- return `<${url}>`
162
+ const attrs = n.attrs ?? { url }
163
+ return `<!-- adf:inlineCard attrs=${stableStringify(attrs)} -->`
178
164
  }
179
165
  case "date": {
180
- const ts = attrStr(n, "timestamp")
181
- if (!ts) return ""
182
- const d = new Date(Number(ts))
183
- return Number.isNaN(d.getTime()) ? ts : d.toISOString().slice(0, 10)
166
+ return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
184
167
  }
185
168
  case "status": {
186
169
  const text = attrStr(n, "text") ?? ""
@@ -215,11 +198,55 @@ const extensionPlaceholder = (
215
198
  // so macros survive a pull → push round-trip with their configuration.
216
199
  const attrs = n.attrs ?? {}
217
200
  const attrsPart = Object.keys(attrs).length > 0
218
- ? ` attrs=${toBase64(stableStringify(attrs))}`
201
+ ? ` attrs=${stableStringify(attrs)}`
219
202
  : ""
220
203
  return `<!-- adf:${nodeType}${keyPart}${typePart}${attrsPart} -->`
221
204
  }
222
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
+
223
250
  const bodiedExtension = (n: AdfNode, ctx: Ctx): string => {
224
251
  const open = extensionPlaceholder(n, "bodiedExtension", ctx)
225
252
  // Table cells flatten newlines to <br>, which would weld the markers and
@@ -299,7 +326,7 @@ const indentLines = (s: string, indent: string): string =>
299
326
  const block = (n: AdfNode, ctx: Ctx): string => {
300
327
  switch (n.type) {
301
328
  case "paragraph":
302
- return escapeLineStarts(inline(n.content, ctx))
329
+ return paragraph(n, ctx)
303
330
  case "heading": {
304
331
  const level = Math.min(6, Math.max(1, attrNum(n, "level") ?? 1))
305
332
  return "#".repeat(level) + " " + inline(n.content, ctx)
@@ -325,12 +352,19 @@ const block = (n: AdfNode, ctx: Ctx): string => {
325
352
  return taskList(n, ctx)
326
353
  case "decisionList":
327
354
  return decisionList(n, ctx)
355
+ case "layoutSection":
356
+ return layoutSection(n, ctx)
357
+ case "layoutColumn":
358
+ return layoutColumn(n, ctx)
328
359
  case "mediaSingle":
329
360
  return mediaSingle(n, ctx)
330
361
  case "mediaGroup":
331
362
  return mediaGroup(n, ctx)
363
+ case "blockCard":
364
+ case "embedCard":
365
+ return blockCard(n, ctx)
332
366
  case "extension":
333
- return extensionPlaceholder(n, "extension", ctx)
367
+ return tocMarkdown(n) ?? extensionPlaceholder(n, "extension", ctx)
334
368
  case "bodiedExtension":
335
369
  return bodiedExtension(n, ctx)
336
370
  default:
@@ -339,6 +373,14 @@ const block = (n: AdfNode, ctx: Ctx): string => {
339
373
  }
340
374
  }
341
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
+
342
384
  const blockquote = (content: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
343
385
  const inner = (content ?? []).map((c) => block(c, ctx)).join("\n\n")
344
386
  return inner.split("\n").map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\n")
@@ -423,15 +465,25 @@ const table = (n: AdfNode, ctx: Ctx): string => {
423
465
  const separator = Array<string>(colCount).fill("---")
424
466
  const bodyRows = (firstIsHeader ? allRows.slice(1) : allRows).map(pad)
425
467
  const fmt = (cells: Array<string>): string => `| ${cells.join(" | ")} |`
426
- return [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n")
468
+ return encodedBlockNode(n, [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n"), ctx)
427
469
  }
428
470
 
429
471
  const panel = (n: AdfNode, ctx: Ctx): string => {
430
472
  const panelType = attrStr(n, "panelType") ?? "info"
431
- const tag = PANEL_MAP[panelType] ?? "NOTE"
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
432
477
  const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
433
- const lines = [`[!${tag}]`, ...inner.split("\n")]
434
- return lines.map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\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")
435
487
  }
436
488
 
437
489
  const expand = (n: AdfNode, ctx: Ctx): string => {
@@ -442,7 +494,8 @@ const expand = (n: AdfNode, ctx: Ctx): string => {
442
494
  // backslash escapes are the correct (and only working) form.
443
495
  const safeTitle = ctx.inTable ? escapeText(title) : escapeHtml(title)
444
496
  const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
445
- return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
497
+ if (ctx.inTable) return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
498
+ return encodedBlockNode(n, `${title}\n\n${inner}`, ctx)
446
499
  }
447
500
 
448
501
  const taskList = (n: AdfNode, ctx: Ctx): string => {
@@ -457,7 +510,7 @@ const taskList = (n: AdfNode, ctx: Ctx): string => {
457
510
  const text = inline(item.content, ctx)
458
511
  lines.push(`- [${checked}] ${text}`)
459
512
  }
460
- return lines.join("\n")
513
+ return encodedBlockNode(n, lines.join("\n"), ctx)
461
514
  }
462
515
 
463
516
  const decisionList = (n: AdfNode, ctx: Ctx): string => {
@@ -470,7 +523,34 @@ const decisionList = (n: AdfNode, ctx: Ctx): string => {
470
523
  }
471
524
  lines.push(`- 🔘 ${inline(item.content, ctx)}`)
472
525
  }
473
- return lines.join("\n")
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)
474
554
  }
475
555
 
476
556
  const renderMedia = (media: AdfNode | undefined, ctx: Ctx): string => {