@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
@@ -1,1338 +0,0 @@
1
- /**
2
- * Parser for Markdown to AST.
3
- *
4
- * @module
5
- */
6
- import * as Effect from "effect/Effect"
7
- import remarkGfm from "remark-gfm"
8
- import remarkParse from "remark-parse"
9
- import { unified } from "unified"
10
- import {
11
- CodeBlock,
12
- Heading,
13
- Image,
14
- Paragraph,
15
- Table,
16
- TableCell,
17
- TableRow,
18
- type TaskItem,
19
- type TaskList,
20
- ThematicBreak,
21
- UnsupportedBlock
22
- } from "../ast/BlockNode.js"
23
- import { type Document, type DocumentNode, makeDocument } from "../ast/Document.js"
24
- import {
25
- ColoredText,
26
- DateTime,
27
- Emoticon,
28
- Emphasis,
29
- Highlight,
30
- InlineCode,
31
- type InlineNode,
32
- LineBreak,
33
- Link,
34
- Strikethrough,
35
- Strong,
36
- Subscript,
37
- Superscript,
38
- Text,
39
- Underline,
40
- UnsupportedInline,
41
- UserMention
42
- } from "../ast/InlineNode.js"
43
- import { type ExpandMacro, type InfoPanel, PanelTypes, type TocMacro } from "../ast/MacroNode.js"
44
- import { ParseError } from "../SchemaConverterError.js"
45
-
46
- // Mdast types (inline to avoid dependency)
47
- interface MdastText {
48
- type: "text"
49
- value: string
50
- }
51
-
52
- interface MdastInlineCode {
53
- type: "inlineCode"
54
- value: string
55
- }
56
-
57
- interface MdastStrong {
58
- type: "strong"
59
- children: Array<MdastNode>
60
- }
61
-
62
- interface MdastEmphasis {
63
- type: "emphasis"
64
- children: Array<MdastNode>
65
- }
66
-
67
- interface MdastDelete {
68
- type: "delete"
69
- children: Array<MdastNode>
70
- }
71
-
72
- interface MdastLink {
73
- type: "link"
74
- url: string
75
- title?: string | null
76
- children: Array<MdastNode>
77
- }
78
-
79
- interface MdastBreak {
80
- type: "break"
81
- }
82
-
83
- interface MdastHeading {
84
- type: "heading"
85
- depth: 1 | 2 | 3 | 4 | 5 | 6
86
- children: Array<MdastNode>
87
- }
88
-
89
- interface MdastParagraph {
90
- type: "paragraph"
91
- children: Array<MdastNode>
92
- }
93
-
94
- interface MdastCode {
95
- type: "code"
96
- lang?: string | null
97
- meta?: string | null
98
- value: string
99
- }
100
-
101
- interface MdastThematicBreak {
102
- type: "thematicBreak"
103
- }
104
-
105
- interface MdastImage {
106
- type: "image"
107
- url: string
108
- alt?: string | null
109
- title?: string | null
110
- }
111
-
112
- interface MdastBlockquote {
113
- type: "blockquote"
114
- children: Array<MdastNode>
115
- }
116
-
117
- interface MdastList {
118
- type: "list"
119
- ordered?: boolean | null
120
- start?: number | null
121
- spread?: boolean | null
122
- children: Array<MdastListItem>
123
- }
124
-
125
- interface MdastListItem {
126
- type: "listItem"
127
- checked?: boolean | null
128
- spread?: boolean | null
129
- children: Array<MdastNode>
130
- }
131
-
132
- interface MdastTable {
133
- type: "table"
134
- align?: Array<"left" | "right" | "center" | null> | null
135
- children: Array<MdastTableRow>
136
- }
137
-
138
- interface MdastTableRow {
139
- type: "tableRow"
140
- children: Array<MdastTableCell>
141
- }
142
-
143
- interface MdastTableCell {
144
- type: "tableCell"
145
- children: Array<MdastNode>
146
- }
147
-
148
- interface MdastRoot {
149
- type: "root"
150
- children: Array<MdastNode>
151
- }
152
-
153
- interface MdastHtml {
154
- type: "html"
155
- value: string
156
- }
157
-
158
- type MdastNode =
159
- | MdastText
160
- | MdastInlineCode
161
- | MdastStrong
162
- | MdastEmphasis
163
- | MdastDelete
164
- | MdastLink
165
- | MdastBreak
166
- | MdastHeading
167
- | MdastParagraph
168
- | MdastCode
169
- | MdastThematicBreak
170
- | MdastImage
171
- | MdastBlockquote
172
- | MdastList
173
- | MdastListItem
174
- | MdastTable
175
- | MdastTableRow
176
- | MdastTableCell
177
- | MdastRoot
178
- | MdastHtml
179
- | { type: string }
180
-
181
- /**
182
- * Parse Markdown to Document AST.
183
- *
184
- * @example
185
- * ```typescript
186
- * import { parseMarkdown } from "@knpkv/confluence-to-markdown/parsers/MarkdownParser"
187
- * import { Effect } from "effect"
188
- *
189
- * Effect.gen(function* () {
190
- * const doc = yield* parseMarkdown("# Title\n\nContent")
191
- * console.log(doc.children.length) // 2
192
- * })
193
- * ```
194
- *
195
- * @category Parsers
196
- */
197
- export const parseMarkdown = (markdown: string): Effect.Effect<Document, ParseError> =>
198
- Effect.gen(function*() {
199
- // Check for embedded rawConfluence comment (for 1-to-1 roundtrip)
200
- const rawMatch = markdown.match(/<!--cf:raw:([A-Za-z0-9+/=]+)-->/)
201
- let rawConfluence: string | undefined
202
- let cleanMarkdown = markdown
203
-
204
- if (rawMatch) {
205
- // Extract and decode the raw Confluence HTML
206
- const encoded = rawMatch[1] ?? ""
207
- rawConfluence = Buffer.from(encoded, "base64").toString("utf-8")
208
- // Remove the raw comment from markdown for parsing
209
- cleanMarkdown = markdown.replace(/\n*<!--cf:raw:[A-Za-z0-9+/=]+-->\s*$/, "")
210
- }
211
-
212
- // Preprocess container syntax (:::type ... :::) to HTML comments
213
- cleanMarkdown = preprocessContainers(cleanMarkdown)
214
-
215
- // Parse Markdown to mdast
216
- const mdast = yield* Effect.try({
217
- try: () =>
218
- unified()
219
- .use(remarkParse)
220
- .use(remarkGfm)
221
- .parse(cleanMarkdown) as MdastRoot,
222
- catch: (error) =>
223
- new ParseError({
224
- source: "markdown",
225
- message: `Markdown parse error: ${error instanceof Error ? error.message : String(error)}`,
226
- rawContent: markdown.slice(0, 200)
227
- })
228
- })
229
-
230
- // Convert mdast to AST
231
- const children = yield* mdastToDocumentNodes(mdast)
232
- return makeDocument(children, rawConfluence)
233
- })
234
-
235
- /**
236
- * Preprocess :::type container syntax to HTML comments.
237
- * Converts :::info\ncontent\n::: to <!--cf:panel:info::encodedContent-->
238
- * Optionally with title: :::info Title\ncontent\n::: to <!--cf:panel:info:Title:encodedContent-->
239
- */
240
- const preprocessContainers = (markdown: string): string => {
241
- // Match :::type with optional same-line title, then content, then :::
242
- // Title must be on same line as opening :::type
243
- const containerRegex = /^:::(\w+)(?: ([^\n]+))?\n([\s\S]*?)\n:::$/gm
244
- return markdown.replace(containerRegex, (_, type, title, content) => {
245
- const panelType = type.toLowerCase()
246
- const encodedContent = encodeURIComponent(content.trim())
247
- const encodedTitle = title ? encodeURIComponent(title.trim()) : ""
248
- return `<!--cf:panel:${panelType}:${encodedTitle}:${encodedContent}-->`
249
- })
250
- }
251
-
252
- /**
253
- * Convert mdast Root to document nodes.
254
- *
255
- * Recognises `<details><summary>title</summary>...body...</details>` HTML blocks
256
- * and rebuilds them as ExpandMacro nodes so they round-trip back to Confluence.
257
- */
258
- const mdastToDocumentNodes = (root: MdastRoot): Effect.Effect<Array<DocumentNode>, ParseError> =>
259
- Effect.gen(function*() {
260
- const nodes: Array<DocumentNode> = []
261
- const children = root.children
262
- let i = 0
263
- while (i < children.length) {
264
- const child = children[i]
265
- if (!child) {
266
- i++
267
- continue
268
- }
269
- if (child.type === "html") {
270
- const detailsMatch = (child as MdastHtml).value.match(
271
- /^<details(?:\s[^>]*)?>\s*<summary(?:\s[^>]*)?>([\s\S]*?)<\/summary>/
272
- )
273
- if (detailsMatch) {
274
- const title = decodeHtmlEntities(detailsMatch[1] ?? "").trim()
275
- let j = i + 1
276
- while (j < children.length) {
277
- const c = children[j]
278
- if (c?.type === "html" && /<\/details>/.test((c as MdastHtml).value)) break
279
- j++
280
- }
281
- const bodyChildren = children.slice(i + 1, j)
282
- const bodyBlocks = yield* mdastChildrenToSimpleBlocks(bodyChildren)
283
- nodes.push(
284
- {
285
- _tag: "ExpandMacro" as const,
286
- version: 1,
287
- ...(title ? { title } : {}),
288
- children: bodyBlocks
289
- } satisfies ExpandMacro
290
- )
291
- i = j + 1
292
- continue
293
- }
294
- // Synthetic empty-header marker: drop it and tell the next table to drop
295
- // its first row (which the serializer added only to satisfy GFM).
296
- if ((child as MdastHtml).value.trim() === "<!--cf:synth-thead-->") {
297
- const next = children[i + 1]
298
- if (next?.type === "table") {
299
- const table = yield* parseTable(next as MdastTable, { dropSyntheticHeader: true })
300
- nodes.push(table)
301
- i += 2
302
- continue
303
- }
304
- i++
305
- continue
306
- }
307
- }
308
- const node = yield* mdastNodeToBlock(child)
309
- if (node !== null) nodes.push(node)
310
- i++
311
- }
312
- return nodes
313
- })
314
-
315
- const decodeHtmlEntities = (s: string): string =>
316
- s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'")
317
-
318
- /**
319
- * Convert mdast node to BlockNode or MacroNode.
320
- */
321
- const mdastNodeToBlock = (node: MdastNode): Effect.Effect<DocumentNode | null, ParseError> =>
322
- Effect.gen(function*() {
323
- switch (node.type) {
324
- case "heading": {
325
- const heading = node as MdastHeading
326
- const children = yield* mdastChildrenToInline(heading.children)
327
- return new Heading({ level: heading.depth, children })
328
- }
329
-
330
- case "paragraph": {
331
- const para = node as MdastParagraph
332
- // Check if paragraph is just [[toc]] - convert to TocMacro
333
- if (para.children.length === 1 && para.children[0]?.type === "text") {
334
- const textContent = (para.children[0] as MdastText).value.trim()
335
- if (textContent === "[[toc]]") {
336
- return { _tag: "TocMacro" as const, version: 1 } satisfies TocMacro
337
- }
338
- }
339
- const children = yield* mdastChildrenToInline(para.children)
340
- return new Paragraph({ children })
341
- }
342
-
343
- case "code": {
344
- const code = node as MdastCode
345
- return new CodeBlock({
346
- code: code.value,
347
- language: code.lang || undefined
348
- })
349
- }
350
-
351
- case "thematicBreak": {
352
- return new ThematicBreak({})
353
- }
354
-
355
- case "image": {
356
- const img = node as MdastImage
357
- return new Image({
358
- src: img.url,
359
- alt: img.alt || undefined,
360
- title: img.title || undefined
361
- })
362
- }
363
-
364
- case "blockquote": {
365
- const bq = node as MdastBlockquote
366
- const children = yield* mdastChildrenToSimpleBlocks(bq.children)
367
- return { _tag: "BlockQuote" as const, version: 1, children }
368
- }
369
-
370
- case "list": {
371
- const list = node as MdastList
372
- return yield* parseList(list)
373
- }
374
-
375
- case "table": {
376
- const table = node as MdastTable
377
- return yield* parseTable(table)
378
- }
379
-
380
- case "html": {
381
- const html = node as MdastHtml
382
- // Check for comment-encoded task list
383
- const taskListParsed = yield* parseTaskListComment(html.value)
384
- if (taskListParsed) return taskListParsed
385
- // Check for comment-encoded image
386
- const imageParsed = yield* parseImageComment(html.value)
387
- if (imageParsed) return imageParsed
388
- // Check for comment-encoded expand macro
389
- const expandParsed = yield* parseExpandMacroComment(html.value)
390
- if (expandParsed) return expandParsed
391
- // Check for comment-encoded TOC macro
392
- const tocParsed = yield* parseTocComment(html.value)
393
- if (tocParsed) return tocParsed
394
- // Check for comment-encoded status macro (wrap in paragraph)
395
- const statusParsed = yield* parseStatusComment(html.value)
396
- if (statusParsed) return statusParsed
397
- // Check for comment-encoded smart link (wrap in paragraph)
398
- const smartLinkParsed = yield* parseSmartLinkComment(html.value)
399
- if (smartLinkParsed) return smartLinkParsed
400
- // Check for comment-encoded decision list
401
- const decisionParsed = yield* parseDecisionComment(html.value)
402
- if (decisionParsed) return decisionParsed
403
- // Check for comment-encoded layout
404
- const layoutParsed = yield* parseLayoutComment(html.value)
405
- if (layoutParsed) return layoutParsed
406
- // Check for comment-encoded panel (:::type container)
407
- const panelParsed = yield* parsePanelComment(html.value)
408
- if (panelParsed) return panelParsed
409
- // Check for comment-encoded inline elements that should become paragraphs
410
- const inlineParsed = yield* parseBlockLevelInlineComment(html.value)
411
- if (inlineParsed) return inlineParsed
412
- return new UnsupportedBlock({
413
- rawMarkdown: html.value,
414
- source: "markdown"
415
- })
416
- }
417
-
418
- default:
419
- return null
420
- }
421
- })
422
-
423
- /**
424
- * Convert mdast children to inline nodes.
425
- * Handles paired HTML tags like <span style="color:...">...</span> by looking ahead.
426
- */
427
- const mdastChildrenToInline = (children: Array<MdastNode>): Effect.Effect<Array<InlineNode>, ParseError> =>
428
- Effect.gen(function*() {
429
- const nodes: Array<InlineNode> = []
430
- let i = 0
431
-
432
- while (i < children.length) {
433
- const child = children[i]
434
- if (!child) {
435
- i++
436
- continue
437
- }
438
-
439
- // Handle text nodes specially - they can contain embedded HTML comments
440
- if (child.type === "text") {
441
- const text = child as MdastText
442
- const parsed = yield* parseTextWithEmbeddedHtml(text.value)
443
- for (const p of parsed) nodes.push(p)
444
- i++
445
- continue
446
- }
447
-
448
- // Check for paired HTML tags (span with color/background)
449
- if (child.type === "html") {
450
- const html = child as MdastHtml
451
- const pairedResult = yield* tryParsePairedHtmlTag(html.value, children, i)
452
- if (pairedResult) {
453
- nodes.push(pairedResult.node)
454
- i = pairedResult.nextIndex
455
- continue
456
- }
457
- }
458
-
459
- // Regular node processing
460
- const node = yield* mdastNodeToInline(child)
461
- if (node !== null) nodes.push(node)
462
- i++
463
- }
464
- return nodes
465
- })
466
-
467
- /**
468
- * Try to parse paired HTML tags like <span style="color:...">content</span>.
469
- * Returns the parsed node and next index if successful, null otherwise.
470
- */
471
- const tryParsePairedHtmlTag = (
472
- openingTag: string,
473
- children: Array<MdastNode>,
474
- startIndex: number
475
- ): Effect.Effect<{ node: InlineNode; nextIndex: number } | null, ParseError> =>
476
- Effect.gen(function*() {
477
- // Check for color span: <span style="color: ...;">
478
- const colorMatch = openingTag.match(/^<span\s+style="color:\s*([^;]+);">$/)
479
- if (colorMatch) {
480
- const result = yield* collectUntilClosingTag(children, startIndex + 1, "</span>")
481
- if (result) {
482
- const innerNodes = yield* mdastChildrenToInline(result.innerChildren)
483
- const baseNodes = inlineNodesToBase(innerNodes)
484
- return {
485
- node: new ColoredText({ color: colorMatch[1] ?? "", children: baseNodes }),
486
- nextIndex: result.nextIndex
487
- }
488
- }
489
- }
490
-
491
- // Check for highlight span: <span style="background-color: ...;">
492
- const bgMatch = openingTag.match(/^<span\s+style="background-color:\s*([^;]+);">$/)
493
- if (bgMatch) {
494
- const result = yield* collectUntilClosingTag(children, startIndex + 1, "</span>")
495
- if (result) {
496
- const innerNodes = yield* mdastChildrenToInline(result.innerChildren)
497
- const baseNodes = inlineNodesToBase(innerNodes)
498
- return {
499
- node: new Highlight({ backgroundColor: bgMatch[1] ?? "", children: baseNodes }),
500
- nextIndex: result.nextIndex
501
- }
502
- }
503
- }
504
-
505
- // Check for underline: <u>
506
- if (openingTag === "<u>") {
507
- const result = yield* collectUntilClosingTag(children, startIndex + 1, "</u>")
508
- if (result) {
509
- const innerNodes = yield* mdastChildrenToInline(result.innerChildren)
510
- const baseNodes = inlineNodesToBase(innerNodes)
511
- return {
512
- node: new Underline({ children: baseNodes }),
513
- nextIndex: result.nextIndex
514
- }
515
- }
516
- }
517
-
518
- // Check for subscript: <sub>
519
- if (openingTag === "<sub>") {
520
- const result = yield* collectUntilClosingTag(children, startIndex + 1, "</sub>")
521
- if (result) {
522
- const innerNodes = yield* mdastChildrenToInline(result.innerChildren)
523
- const baseNodes = inlineNodesToBase(innerNodes)
524
- return {
525
- node: new Subscript({ children: baseNodes }),
526
- nextIndex: result.nextIndex
527
- }
528
- }
529
- }
530
-
531
- // Check for superscript: <sup>
532
- if (openingTag === "<sup>") {
533
- const result = yield* collectUntilClosingTag(children, startIndex + 1, "</sup>")
534
- if (result) {
535
- const innerNodes = yield* mdastChildrenToInline(result.innerChildren)
536
- const baseNodes = inlineNodesToBase(innerNodes)
537
- return {
538
- node: new Superscript({ children: baseNodes }),
539
- nextIndex: result.nextIndex
540
- }
541
- }
542
- }
543
-
544
- return null
545
- })
546
-
547
- /**
548
- * Collect mdast nodes until a closing HTML tag is found.
549
- * Returns the inner children and the index after the closing tag.
550
- */
551
- const collectUntilClosingTag = (
552
- children: Array<MdastNode>,
553
- startIndex: number,
554
- closingTag: string
555
- ): Effect.Effect<{ innerChildren: Array<MdastNode>; nextIndex: number } | null, ParseError> =>
556
- Effect.gen(function*() {
557
- const innerChildren: Array<MdastNode> = []
558
-
559
- for (let i = startIndex; i < children.length; i++) {
560
- const child = children[i]
561
- if (!child) continue
562
-
563
- if (child.type === "html") {
564
- const html = child as MdastHtml
565
- if (html.value === closingTag) {
566
- return { innerChildren, nextIndex: i + 1 }
567
- }
568
- }
569
-
570
- innerChildren.push(child)
571
- }
572
-
573
- // No closing tag found
574
- return null
575
- })
576
-
577
- /**
578
- * Convert InlineNode array to base inline nodes for nested formatting.
579
- */
580
- const inlineNodesToBase = (
581
- nodes: Array<InlineNode>
582
- ): Array<Text | InlineCode | LineBreak | UnsupportedInline> => {
583
- const result: Array<Text | InlineCode | LineBreak | UnsupportedInline> = []
584
- for (const node of nodes) {
585
- switch (node._tag) {
586
- case "Text":
587
- case "InlineCode":
588
- case "LineBreak":
589
- case "UnsupportedInline":
590
- result.push(node as Text | InlineCode | LineBreak | UnsupportedInline)
591
- break
592
- default:
593
- // For complex nodes, serialize to raw string
594
- result.push(new UnsupportedInline({ raw: JSON.stringify(node), source: "markdown" }))
595
- }
596
- }
597
- return result
598
- }
599
-
600
- /**
601
- * Convert mdast node to InlineNode.
602
- */
603
- const mdastNodeToInline = (node: MdastNode): Effect.Effect<InlineNode | null, ParseError> =>
604
- Effect.gen(function*() {
605
- switch (node.type) {
606
- case "text": {
607
- const text = node as MdastText
608
- return new Text({ value: text.value })
609
- }
610
-
611
- case "strong": {
612
- const strong = node as MdastStrong
613
- const children = yield* mdastChildrenToBaseInline(strong.children)
614
- return new Strong({ children })
615
- }
616
-
617
- case "emphasis": {
618
- const em = node as MdastEmphasis
619
- const children = yield* mdastChildrenToBaseInline(em.children)
620
- return new Emphasis({ children })
621
- }
622
-
623
- case "delete": {
624
- const del = node as MdastDelete
625
- const children = yield* mdastChildrenToBaseInline(del.children)
626
- return new Strikethrough({ children })
627
- }
628
-
629
- case "inlineCode": {
630
- const code = node as MdastInlineCode
631
- return new InlineCode({ value: code.value })
632
- }
633
-
634
- case "link": {
635
- const link = node as MdastLink
636
- // Recognise the visible round-trip carriers emitted by MarkdownSerializer
637
- // for inline macros (`#cf-user:`, `#cf-status:`, `#cf-toc:`) and rebuild
638
- // the proper AST node so push back to Confluence is lossless.
639
- const macroNode = parseInlineMacroLink(link)
640
- if (macroNode) return macroNode
641
- const children = yield* mdastChildrenToBaseInline(link.children)
642
- return new Link({
643
- href: link.url,
644
- title: link.title || undefined,
645
- children
646
- })
647
- }
648
-
649
- case "break": {
650
- return new LineBreak({})
651
- }
652
-
653
- case "image": {
654
- const img = node as MdastImage
655
- return new UnsupportedInline({
656
- raw: `![${img.alt || ""}](${img.url})`,
657
- source: "markdown"
658
- })
659
- }
660
-
661
- case "html": {
662
- const html = node as MdastHtml
663
- const parsed = yield* parseInlineHtml(html.value)
664
- if (parsed) return parsed
665
- return new UnsupportedInline({
666
- raw: html.value,
667
- source: "markdown"
668
- })
669
- }
670
-
671
- default:
672
- return null
673
- }
674
- })
675
-
676
- /**
677
- * Parse inline HTML that was preserved for roundtrip.
678
- */
679
- const parseInlineHtml = (html: string): Effect.Effect<InlineNode | null, ParseError> =>
680
- Effect.gen(function*() {
681
- // Comment-encoded Emoticon: <!--cf:emoticon:shortname|emojiId|fallback-->
682
- // Use non-greedy match since values can contain special chars
683
- const emoticonCommentMatch = html.match(/<!--cf:emoticon:([^|]*)\|([^|]*)\|(.+?)-->/)
684
- if (emoticonCommentMatch) {
685
- return new Emoticon({
686
- shortname: decodeURIComponent(emoticonCommentMatch[1] ?? ""),
687
- emojiId: decodeURIComponent(emoticonCommentMatch[2] ?? ""),
688
- fallback: decodeURIComponent(emoticonCommentMatch[3] ?? "")
689
- })
690
- }
691
-
692
- // Comment-encoded User mention: <!--cf:user:accountId-->
693
- // Account IDs can contain dashes, colons, etc. Match everything until -->
694
- const userCommentMatch = html.match(/<!--cf:user:(.+?)-->/)
695
- if (userCommentMatch) {
696
- return new UserMention({ accountId: userCommentMatch[1] ?? "" })
697
- }
698
-
699
- // Comment-encoded DateTime: <!--cf:date:datetime-->
700
- // Use non-greedy match since dates can contain dashes, allow empty datetime
701
- const dateCommentMatch = html.match(/<!--cf:date:(.*?)-->/)
702
- if (dateCommentMatch) {
703
- return new DateTime({ datetime: dateCommentMatch[1] ?? "" })
704
- }
705
-
706
- // Colored text
707
- const colorMatch = html.match(/<span style="color:\s*([^;]+);">([^<]*)<\/span>/)
708
- if (colorMatch) {
709
- return new ColoredText({
710
- color: colorMatch[1] ?? "",
711
- children: [new Text({ value: colorMatch[2] ?? "" })]
712
- })
713
- }
714
-
715
- // Highlight
716
- const bgMatch = html.match(/<span style="background-color:\s*([^;]+);">([^<]*)<\/span>/)
717
- if (bgMatch) {
718
- return new Highlight({
719
- backgroundColor: bgMatch[1] ?? "",
720
- children: [new Text({ value: bgMatch[2] ?? "" })]
721
- })
722
- }
723
-
724
- return null
725
- })
726
-
727
- /**
728
- * Recognise visible markdown links emitted as round-trip carriers for inline
729
- * Confluence macros (UserMention, StatusMacro, TocMacro). Returns the rebuilt
730
- * AST node, or null if the link is a regular link.
731
- *
732
- * The raw `<!--cf:status:title;color-->` form expects URL-encoded values;
733
- * link text is the human-readable (decoded) form, so we re-encode it here.
734
- * URL fragments are already URL-encoded, so they pass through verbatim.
735
- */
736
- const parseInlineMacroLink = (link: MdastLink): InlineNode | null => {
737
- const userMatch = link.url.match(/^#cf-user:(.+)$/)
738
- if (userMatch) {
739
- return new UserMention({ accountId: decodeURIComponent(userMatch[1] ?? "") })
740
- }
741
- const statusMatch = link.url.match(/^#cf-status:(.*)$/)
742
- if (statusMatch) {
743
- const text = link.children
744
- .filter((c): c is MdastText => c.type === "text")
745
- .map((c) => c.value)
746
- .join("")
747
- return new UnsupportedInline({
748
- raw: `<!--cf:status:${encodeURIComponent(text)};${statusMatch[1] ?? ""}-->`,
749
- source: "markdown"
750
- })
751
- }
752
- const tocMatch = link.url.match(/^#cf-toc:([^:]*):([^:]*)$/)
753
- if (tocMatch) {
754
- return new UnsupportedInline({
755
- raw: `<!--cf:toc:${tocMatch[1] ?? ""};${tocMatch[2] ?? ""}-->`,
756
- source: "markdown"
757
- })
758
- }
759
- return null
760
- }
761
-
762
- /**
763
- * Parse comment-encoded task list.
764
- * Format: <!--cf:tasklist:id|uuid|status|body;id|uuid|status|body-->
765
- */
766
- const parseTaskListComment = (html: string): Effect.Effect<TaskList | null, ParseError> =>
767
- Effect.gen(function*() {
768
- // Check if this is a task list
769
- const match = html.match(/<!--cf:tasklist:(.*)-->/)
770
- if (!match) {
771
- return null
772
- }
773
-
774
- const itemsStr = match[1] ?? ""
775
- const items: Array<TaskItem> = []
776
-
777
- for (const itemStr of itemsStr.split(";")) {
778
- const parts = itemStr.split("|")
779
- if (parts.length >= 4) {
780
- items.push({
781
- _tag: "TaskItem" as const,
782
- id: parts[0] ?? "",
783
- uuid: parts[1] ?? "",
784
- status: (parts[2] === "complete" ? "complete" : "incomplete") as "incomplete" | "complete",
785
- body: [new Text({ value: decodeURIComponent(parts[3] ?? "") })]
786
- })
787
- }
788
- }
789
-
790
- if (items.length === 0) {
791
- return null
792
- }
793
-
794
- return {
795
- _tag: "TaskList" as const,
796
- version: 1,
797
- children: items
798
- }
799
- })
800
-
801
- /**
802
- * Parse block-level HTML that contains comment-encoded inline elements.
803
- * Wraps them in a paragraph if found.
804
- */
805
- const parseBlockLevelInlineComment = (html: string): Effect.Effect<Paragraph | null, ParseError> =>
806
- Effect.gen(function*() {
807
- // Check for patterns that should be inline within a paragraph
808
- const inlinePattern = /<!--cf:(emoticon|user|date):/
809
- if (!inlinePattern.test(html)) {
810
- return null
811
- }
812
-
813
- // Parse the text which may contain multiple inline elements
814
- const parsed = yield* parseTextWithEmbeddedHtml(html)
815
- if (parsed.length === 0) {
816
- return null
817
- }
818
-
819
- // Filter out empty text nodes
820
- const nonEmpty = parsed.filter((n) => n._tag !== "Text" || (n as Text).value.trim() !== "")
821
- if (nonEmpty.length === 0) {
822
- return null
823
- }
824
-
825
- return new Paragraph({ children: parsed })
826
- })
827
-
828
- /**
829
- * Parse comment-encoded image.
830
- * Format: <!--cf:image:f=filename|v=version|s=src|a=alt|t=title|al=align|w=width-->
831
- */
832
- const parseImageComment = (html: string): Effect.Effect<Image | null, ParseError> =>
833
- Effect.gen(function*() {
834
- const match = html.match(/<!--cf:image:(.*)-->/)
835
- if (!match) {
836
- return null
837
- }
838
-
839
- const partsStr = match[1] ?? ""
840
- const props: Record<string, string> = {}
841
-
842
- for (const part of partsStr.split("|")) {
843
- const [key, ...valueParts] = part.split("=")
844
- if (key) {
845
- props[key] = valueParts.join("=")
846
- }
847
- }
848
-
849
- const attachment = props["f"]
850
- ? {
851
- filename: decodeURIComponent(props["f"]),
852
- version: props["v"] ? parseInt(props["v"], 10) : undefined
853
- }
854
- : undefined
855
-
856
- return new Image({
857
- src: props["s"] ? decodeURIComponent(props["s"]) : undefined,
858
- alt: props["a"] ? decodeURIComponent(props["a"]) : undefined,
859
- title: props["t"] ? decodeURIComponent(props["t"]) : undefined,
860
- align: props["al"] ?? undefined,
861
- width: props["w"] ? parseInt(props["w"], 10) : undefined,
862
- attachment
863
- })
864
- })
865
-
866
- /**
867
- * Parse comment-encoded expand macro.
868
- * Format: <!--cf:expand:title:content-->
869
- */
870
- type ExpandMacroResult = {
871
- _tag: "ExpandMacro"
872
- version: number
873
- title?: string
874
- children: Array<Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock>
875
- }
876
-
877
- const parseExpandMacroComment = (html: string): Effect.Effect<ExpandMacroResult | null, ParseError> =>
878
- Effect.gen(function*() {
879
- const match = html.match(/<!--cf:expand:([^:]*):(.*)-->/)
880
- if (!match) {
881
- return null
882
- }
883
-
884
- const titleStr = decodeURIComponent(match[1] ?? "")
885
- const content = decodeURIComponent(match[2] ?? "")
886
-
887
- // Parse content as simple paragraphs
888
- const children: Array<Paragraph> = content
889
- .split("\n")
890
- .filter((line) => line.trim())
891
- .map((line) => new Paragraph({ children: [new Text({ value: line })] }))
892
-
893
- const result: ExpandMacroResult = {
894
- _tag: "ExpandMacro",
895
- version: 1,
896
- children
897
- }
898
- if (titleStr) {
899
- result.title = titleStr
900
- }
901
- return result
902
- })
903
-
904
- /**
905
- * Parse comment-encoded panel (from :::type container syntax).
906
- * Format: <!--cf:panel:type:title:content-->
907
- */
908
- const parsePanelComment = (html: string): Effect.Effect<InfoPanel | null, ParseError> =>
909
- Effect.gen(function*() {
910
- const match = html.match(/<!--cf:panel:(\w+):([^:]*):(.*)-->/)
911
- if (!match) {
912
- return null
913
- }
914
-
915
- const panelType = match[1] ?? "info"
916
- const titleStr = decodeURIComponent(match[2] ?? "")
917
- const content = decodeURIComponent(match[3] ?? "")
918
-
919
- // Verify panel type is valid
920
- if (!(PanelTypes as ReadonlyArray<string>).includes(panelType)) {
921
- return null
922
- }
923
-
924
- // Parse content as simple paragraphs
925
- const children: Array<Paragraph> = content
926
- .split("\n")
927
- .filter((line) => line.trim())
928
- .map((line) => new Paragraph({ children: [new Text({ value: line })] }))
929
-
930
- return {
931
- _tag: "InfoPanel" as const,
932
- version: 1,
933
- panelType: panelType as (typeof PanelTypes)[number],
934
- ...(titleStr ? { title: titleStr } : {}),
935
- children
936
- } satisfies InfoPanel
937
- })
938
-
939
- /**
940
- * Parse comment-encoded TOC macro.
941
- * Format: <!--cf:toc:minLevel;maxLevel-->
942
- */
943
- const parseTocComment = (html: string): Effect.Effect<TocMacro | null, ParseError> =>
944
- Effect.gen(function*() {
945
- const match = html.match(/<!--cf:toc:([^;]*);([^;]*)-->/)
946
- if (!match) {
947
- return null
948
- }
949
-
950
- const minStr = match[1] ?? ""
951
- const maxStr = match[2] ?? ""
952
-
953
- return {
954
- _tag: "TocMacro" as const,
955
- version: 1,
956
- minLevel: minStr ? parseInt(minStr) : undefined,
957
- maxLevel: maxStr ? parseInt(maxStr) : undefined
958
- } satisfies TocMacro
959
- })
960
-
961
- /**
962
- * Parse comment-encoded Status macro(s).
963
- * Format: <!--cf:status:title;color-->
964
- * Returns a paragraph containing all status macros found.
965
- */
966
- const parseStatusComment = (html: string): Effect.Effect<Paragraph | null, ParseError> =>
967
- Effect.gen(function*() {
968
- // Match all status comments in the string
969
- const statusPattern = /<!--cf:status:([^;]*);([^;]*)-->/g
970
- const matches = [...html.matchAll(statusPattern)]
971
-
972
- if (matches.length === 0) {
973
- return null
974
- }
975
-
976
- // Create StatusMacro nodes wrapped in UnsupportedInline for now
977
- // (since StatusMacro isn't an InlineNode)
978
- const children: Array<InlineNode> = []
979
- let lastIndex = 0
980
-
981
- for (const match of matches) {
982
- // Add any text/whitespace between matches (preserve spaces)
983
- if (match.index !== undefined && match.index > lastIndex) {
984
- const textBetween = html.slice(lastIndex, match.index)
985
- if (textBetween) {
986
- children.push(new Text({ value: textBetween }))
987
- }
988
- }
989
-
990
- // Add status as UnsupportedInline to preserve through roundtrip
991
- children.push(
992
- new UnsupportedInline({
993
- raw: match[0],
994
- source: "markdown"
995
- })
996
- )
997
-
998
- lastIndex = (match.index ?? 0) + match[0].length
999
- }
1000
-
1001
- // Add any trailing text
1002
- if (lastIndex < html.length) {
1003
- const trailing = html.slice(lastIndex)
1004
- if (trailing.trim()) {
1005
- children.push(new Text({ value: trailing }))
1006
- }
1007
- }
1008
-
1009
- return new Paragraph({ children })
1010
- })
1011
-
1012
- /**
1013
- * Parse comment-encoded Smart link.
1014
- * Format: <!--cf:smartlink:href;appearance;datasource-->
1015
- */
1016
- const parseSmartLinkComment = (html: string): Effect.Effect<Paragraph | null, ParseError> =>
1017
- Effect.gen(function*() {
1018
- const match = html.match(/<!--cf:smartlink:([^;]*);([^;]*);(.*)-->/)
1019
- if (!match) {
1020
- return null
1021
- }
1022
-
1023
- // Preserve as UnsupportedInline for roundtrip
1024
- return new Paragraph({
1025
- children: [
1026
- new UnsupportedInline({
1027
- raw: html.trim(),
1028
- source: "markdown"
1029
- })
1030
- ]
1031
- })
1032
- })
1033
-
1034
- /**
1035
- * Parse comment-encoded Decision list.
1036
- * Format: <!--cf:decision:localId;state;content|localId;state;content-->
1037
- */
1038
- const parseDecisionComment = (html: string): Effect.Effect<UnsupportedBlock | null, ParseError> =>
1039
- Effect.gen(function*() {
1040
- const match = html.match(/<!--cf:decision:(.*)-->/)
1041
- if (!match) {
1042
- return null
1043
- }
1044
-
1045
- // Preserve as UnsupportedBlock with the raw comment for roundtrip
1046
- return new UnsupportedBlock({
1047
- rawHtml: html.trim(),
1048
- source: "markdown"
1049
- })
1050
- })
1051
-
1052
- /**
1053
- * Parse layout marker comments.
1054
- * Markers:
1055
- * - <!--cf:layout-start-->
1056
- * - <!--cf:section:index;type;breakoutMode;breakoutWidth;cellCount-->
1057
- * - <!--cf:cell:sectionIndex;cellIndex-->
1058
- * - <!--cf:section-end:index-->
1059
- * - <!--cf:layout-end-->
1060
- */
1061
- const parseLayoutComment = (html: string): Effect.Effect<UnsupportedBlock | null, ParseError> =>
1062
- Effect.gen(function*() {
1063
- // Check for any layout marker pattern
1064
- if (
1065
- html.trim() === "<!--cf:layout-start-->" ||
1066
- html.trim() === "<!--cf:layout-end-->" ||
1067
- /<!--cf:section:\d+;[^;]*;[^;]*;[^;]*;\d+-->/.test(html) ||
1068
- /<!--cf:section-end:\d+-->/.test(html) ||
1069
- /<!--cf:cell:\d+;\d+-->/.test(html)
1070
- ) {
1071
- // Preserve as UnsupportedBlock with the raw comment for roundtrip
1072
- return new UnsupportedBlock({
1073
- rawHtml: html.trim(),
1074
- source: "markdown"
1075
- })
1076
- }
1077
-
1078
- return null
1079
- })
1080
-
1081
- /**
1082
- * Parse text that may contain embedded HTML patterns not recognized by remark.
1083
- * This handles ac: and ri: namespaced elements that remark treats as text.
1084
- */
1085
- const parseTextWithEmbeddedHtml = (text: string): Effect.Effect<Array<InlineNode>, ParseError> =>
1086
- Effect.gen(function*() {
1087
- const nodes: Array<InlineNode> = []
1088
-
1089
- // Pattern to match all embedded HTML we care about (comment-encoded)
1090
- // Use non-greedy match for content since account IDs can contain dashes
1091
- // Date can be empty, so use .*? instead of .+?
1092
- const htmlPattern = /<!--cf:emoticon:.+?-->|<!--cf:user:.+?-->|<!--cf:date:.*?-->/g
1093
-
1094
- let lastIndex = 0
1095
- let match: RegExpExecArray | null
1096
-
1097
- while ((match = htmlPattern.exec(text)) !== null) {
1098
- // Add text before the match
1099
- if (match.index > lastIndex) {
1100
- nodes.push(new Text({ value: text.slice(lastIndex, match.index) }))
1101
- }
1102
-
1103
- // Parse the HTML match
1104
- const parsed = yield* parseInlineHtml(match[0])
1105
- if (parsed) {
1106
- nodes.push(parsed)
1107
- } else {
1108
- // If we can't parse it, keep as text
1109
- nodes.push(new Text({ value: match[0] }))
1110
- }
1111
-
1112
- lastIndex = match.index + match[0].length
1113
- }
1114
-
1115
- // Add remaining text
1116
- if (lastIndex < text.length) {
1117
- nodes.push(new Text({ value: text.slice(lastIndex) }))
1118
- }
1119
-
1120
- // If no matches, return original text
1121
- if (nodes.length === 0) {
1122
- nodes.push(new Text({ value: text }))
1123
- }
1124
-
1125
- return nodes
1126
- })
1127
-
1128
- /**
1129
- * Convert mdast children to base inline nodes.
1130
- */
1131
- const mdastChildrenToBaseInline = (
1132
- children: Array<MdastNode>
1133
- ): Effect.Effect<Array<Text | InlineCode | LineBreak | UnsupportedInline>, ParseError> =>
1134
- Effect.gen(function*() {
1135
- const nodes: Array<Text | InlineCode | LineBreak | UnsupportedInline> = []
1136
- for (const child of children) {
1137
- switch (child.type) {
1138
- case "text": {
1139
- const text = child as MdastText
1140
- nodes.push(new Text({ value: text.value }))
1141
- break
1142
- }
1143
- case "inlineCode": {
1144
- const code = child as MdastInlineCode
1145
- nodes.push(new InlineCode({ value: code.value }))
1146
- break
1147
- }
1148
- case "break": {
1149
- nodes.push(new LineBreak({}))
1150
- break
1151
- }
1152
- default: {
1153
- nodes.push(new UnsupportedInline({ raw: JSON.stringify(child), source: "markdown" }))
1154
- }
1155
- }
1156
- }
1157
- return nodes
1158
- })
1159
-
1160
- /**
1161
- * Convert mdast children to block nodes for list items.
1162
- *
1163
- * Allows nested {@link NestedList} children — recurses on `list` mdast nodes so
1164
- * second-level bullets become proper nested markdown lists rather than raw HTML.
1165
- */
1166
- const mdastChildrenToListItemBlocks = (
1167
- children: Array<MdastNode>
1168
- ): Effect.Effect<Array<SimpleBlock>, ParseError> =>
1169
- Effect.gen(function*() {
1170
- const blocks: Array<SimpleBlock> = []
1171
- for (const child of children) {
1172
- switch (child.type) {
1173
- case "heading": {
1174
- const heading = child as MdastHeading
1175
- const inlineChildren = yield* mdastChildrenToInline(heading.children)
1176
- blocks.push(new Heading({ level: heading.depth, children: inlineChildren }))
1177
- break
1178
- }
1179
- case "paragraph": {
1180
- const para = child as MdastParagraph
1181
- const inlineChildren = yield* mdastChildrenToInline(para.children)
1182
- blocks.push(new Paragraph({ children: inlineChildren }))
1183
- break
1184
- }
1185
- case "code": {
1186
- const code = child as MdastCode
1187
- blocks.push(new CodeBlock({ code: code.value, language: code.lang || undefined }))
1188
- break
1189
- }
1190
- case "thematicBreak": {
1191
- blocks.push(new ThematicBreak({}))
1192
- break
1193
- }
1194
- case "image": {
1195
- const img = child as MdastImage
1196
- blocks.push(new Image({ src: img.url, alt: img.alt || undefined }))
1197
- break
1198
- }
1199
- case "table": {
1200
- const table = child as MdastTable
1201
- blocks.push(yield* parseTable(table))
1202
- break
1203
- }
1204
- case "html": {
1205
- // HTML nodes in list items - preserve as-is for roundtrip
1206
- // Trim leading/trailing whitespace that remark may add
1207
- const html = child as MdastHtml
1208
- blocks.push(new UnsupportedBlock({ rawHtml: html.value.trim(), source: "markdown" }))
1209
- break
1210
- }
1211
- case "list": {
1212
- blocks.push(yield* parseList(child as MdastList))
1213
- break
1214
- }
1215
- default: {
1216
- blocks.push(new UnsupportedBlock({ rawMarkdown: JSON.stringify(child), source: "markdown" }))
1217
- }
1218
- }
1219
- }
1220
- return blocks
1221
- })
1222
-
1223
- /**
1224
- * Convert mdast children to simple block nodes (no nested lists).
1225
- *
1226
- * Used for AST nodes whose schema does not permit nested lists (BlockQuote etc.).
1227
- */
1228
- const mdastChildrenToSimpleBlocks = (
1229
- children: Array<MdastNode>
1230
- ): Effect.Effect<
1231
- Array<Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock>,
1232
- ParseError
1233
- > =>
1234
- Effect.gen(function*() {
1235
- const blocks = yield* mdastChildrenToListItemBlocks(children)
1236
- return blocks.map((block) =>
1237
- block._tag === "List"
1238
- ? new UnsupportedBlock({ rawMarkdown: "", source: "markdown" })
1239
- : block
1240
- )
1241
- })
1242
-
1243
- // Type for simple blocks used in lists. Recursive: a list item may contain nested Lists.
1244
- type SimpleBlock =
1245
- | Heading
1246
- | Paragraph
1247
- | CodeBlock
1248
- | ThematicBreak
1249
- | Image
1250
- | Table
1251
- | UnsupportedBlock
1252
- | NestedList
1253
-
1254
- type NestedList = {
1255
- _tag: "List"
1256
- version: number
1257
- ordered: boolean
1258
- start?: number
1259
- children: Array<{ _tag: "ListItem"; checked?: boolean; children: Array<SimpleBlock> }>
1260
- }
1261
-
1262
- /**
1263
- * Parse mdast list.
1264
- */
1265
- const parseList = (
1266
- list: MdastList
1267
- ): Effect.Effect<
1268
- {
1269
- _tag: "List"
1270
- version: number
1271
- ordered: boolean
1272
- start?: number
1273
- children: Array<{ _tag: "ListItem"; checked?: boolean; children: Array<SimpleBlock> }>
1274
- },
1275
- ParseError
1276
- > =>
1277
- Effect.gen(function*() {
1278
- const items: Array<{ _tag: "ListItem"; checked?: boolean; children: Array<SimpleBlock> }> = []
1279
- const ordered = list.ordered === true
1280
- const start = ordered && list.start != null ? list.start : undefined
1281
-
1282
- for (const item of list.children) {
1283
- const children = yield* mdastChildrenToListItemBlocks(item.children)
1284
- if (item.checked != null) {
1285
- items.push({ _tag: "ListItem", checked: item.checked, children })
1286
- } else {
1287
- items.push({ _tag: "ListItem", children })
1288
- }
1289
- }
1290
-
1291
- if (start !== undefined) {
1292
- return { _tag: "List" as const, version: 1, ordered, start, children: items }
1293
- }
1294
- return { _tag: "List" as const, version: 1, ordered, children: items }
1295
- })
1296
-
1297
- /**
1298
- * Parse mdast table.
1299
- *
1300
- * `dropSyntheticHeader` is set by `mdastToDocumentNodes` when the table was
1301
- * preceded by a `<!--cf:synth-thead-->` marker — only then do we discard the
1302
- * first row (which the serializer added solely to satisfy GFM's required
1303
- * divider line). A legitimate empty `<thead>` from real Confluence storage is
1304
- * always preserved.
1305
- */
1306
- const parseTable = (
1307
- table: MdastTable,
1308
- options: { dropSyntheticHeader?: boolean } = {}
1309
- ): Effect.Effect<Table, ParseError> =>
1310
- Effect.gen(function*() {
1311
- let header: TableRow | undefined
1312
- const rows: Array<TableRow> = []
1313
-
1314
- for (let i = 0; i < table.children.length; i++) {
1315
- const row = table.children[i]
1316
- if (!row) continue
1317
- const cells: Array<TableCell> = []
1318
-
1319
- for (const cell of row.children) {
1320
- const children = yield* mdastChildrenToInline(cell.children)
1321
- const isHeader = i === 0
1322
- cells.push(new TableCell({ isHeader, children }))
1323
- }
1324
-
1325
- const tableRow = new TableRow({ cells })
1326
- if (i === 0) {
1327
- header = tableRow
1328
- } else {
1329
- rows.push(tableRow)
1330
- }
1331
- }
1332
-
1333
- if (options.dropSyntheticHeader) {
1334
- header = undefined
1335
- }
1336
-
1337
- return new Table({ header, rows })
1338
- })