@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,956 +0,0 @@
1
- /**
2
- * Parser for Confluence storage format (HTML) to AST.
3
- *
4
- * @module
5
- */
6
- import * as Effect from "effect/Effect"
7
- import * as Schema from "effect/Schema"
8
- import rehypeParse from "rehype-parse"
9
- import { unified } from "unified"
10
- import {
11
- CodeBlock,
12
- Heading,
13
- Image,
14
- type List,
15
- Paragraph,
16
- Table,
17
- TableCell,
18
- TableRow,
19
- type TaskItem,
20
- type TaskList,
21
- ThematicBreak,
22
- UnsupportedBlock
23
- } from "../ast/BlockNode.js"
24
- import { type Document, type DocumentNode, makeDocument } from "../ast/Document.js"
25
- import {
26
- ColoredText,
27
- DateTime,
28
- Emoticon,
29
- Emphasis,
30
- Highlight,
31
- InlineCode,
32
- type InlineNode,
33
- LineBreak,
34
- Link,
35
- Strikethrough,
36
- Strong,
37
- Subscript,
38
- Superscript,
39
- Text,
40
- Underline,
41
- UnsupportedInline,
42
- UserMention
43
- } from "../ast/InlineNode.js"
44
- import { type ExpandMacro, type InfoPanel, PanelTypes, type TocMacro } from "../ast/MacroNode.js"
45
- import { ParseError } from "../SchemaConverterError.js"
46
- import { PreprocessedHtmlFromConfluence } from "../schemas/preprocessing/index.js"
47
-
48
- // Hast types (inline to avoid dependency)
49
- interface HastText {
50
- type: "text"
51
- value: string
52
- }
53
-
54
- interface HastElement {
55
- type: "element"
56
- tagName: string
57
- properties?: Record<string, unknown>
58
- children: Array<HastNode>
59
- }
60
-
61
- interface HastRoot {
62
- type: "root"
63
- children: Array<HastNode>
64
- }
65
-
66
- type HastNode = HastText | HastElement | HastRoot | { type: string }
67
-
68
- /**
69
- * Parse Confluence storage format HTML to Document AST.
70
- *
71
- * @example
72
- * ```typescript
73
- * import { parseConfluenceHtml } from "@knpkv/confluence-to-markdown/parsers/ConfluenceParser"
74
- * import { Effect } from "effect"
75
- *
76
- * Effect.gen(function* () {
77
- * const doc = yield* parseConfluenceHtml("<h1>Title</h1><p>Content</p>")
78
- * console.log(doc.children.length) // 2
79
- * })
80
- * ```
81
- *
82
- * @category Parsers
83
- */
84
- export const parseConfluenceHtml = (html: string): Effect.Effect<Document, ParseError> =>
85
- Effect.gen(function*() {
86
- // Pre-process Confluence macros (includes size validation)
87
- const preprocessed = yield* Schema.decode(PreprocessedHtmlFromConfluence)(html).pipe(
88
- Effect.mapError((error) =>
89
- new ParseError({
90
- source: "confluence",
91
- message: `Preprocessing error: ${error.message}`,
92
- rawContent: html.slice(0, 200)
93
- })
94
- )
95
- )
96
-
97
- // Parse HTML to hast
98
- const hast = yield* Effect.try({
99
- try: () => unified().use(rehypeParse, { fragment: true }).parse(preprocessed) as HastRoot,
100
- catch: (error) =>
101
- new ParseError({
102
- source: "confluence",
103
- message: `HTML parse error: ${error instanceof Error ? error.message : String(error)}`,
104
- rawContent: html.slice(0, 200)
105
- })
106
- })
107
-
108
- // Convert hast to AST
109
- const children = yield* hastToDocumentNodes(hast)
110
- // Store original HTML for 1-to-1 roundtrip
111
- return makeDocument(children, html)
112
- })
113
-
114
- /**
115
- * Convert hast Root to document nodes.
116
- */
117
- const hastToDocumentNodes = (root: HastRoot): Effect.Effect<Array<DocumentNode>, ParseError> =>
118
- Effect.gen(function*() {
119
- const nodes: Array<DocumentNode> = []
120
- for (const child of root.children) {
121
- if (child.type === "element") {
122
- const el = child as HastElement
123
- // Check for marker div containing a cf: comment
124
- if (el.tagName === "div" && el.properties?.["dataCfMarker"] !== undefined) {
125
- const commentChild = el.children.find((c) => c.type === "comment")
126
- if (commentChild) {
127
- const comment = (commentChild as { type: "comment"; value: string }).value
128
- const node = yield* parseCommentNode(comment)
129
- if (node !== null) nodes.push(node)
130
- }
131
- } else {
132
- const node = yield* hastElementToNode(el)
133
- if (node !== null) nodes.push(node)
134
- }
135
- } else if (child.type === "comment") {
136
- // Handle cf: comment-encoded elements at root level
137
- const comment = (child as { type: "comment"; value: string }).value
138
- const node = yield* parseCommentNode(comment)
139
- if (node !== null) nodes.push(node)
140
- }
141
- }
142
- return nodes
143
- })
144
-
145
- /**
146
- * Parse comment-encoded elements (decision lists, layout markers, etc).
147
- */
148
- const parseCommentNode = (comment: string): Effect.Effect<DocumentNode | null, ParseError> =>
149
- Effect.gen(function*() {
150
- // Decision list: cf:decision:localId;state;content|localId;state;content
151
- const decisionMatch = comment.match(/^cf:decision:(.*)$/)
152
- if (decisionMatch) {
153
- const itemsStr = decisionMatch[1] ?? ""
154
- // Return as UnsupportedBlock with the encoded comment for roundtrip
155
- return new UnsupportedBlock({
156
- rawHtml: `<!--cf:decision:${itemsStr}-->`,
157
- source: "confluence"
158
- })
159
- }
160
-
161
- // Layout markers - these are structural markers, preserve for roundtrip
162
- // cf:layout-start, cf:layout-end
163
- if (comment === "cf:layout-start" || comment === "cf:layout-end") {
164
- return new UnsupportedBlock({
165
- rawHtml: `<!--${comment}-->`,
166
- source: "confluence"
167
- })
168
- }
169
-
170
- // cf:section:index;type;breakoutMode;breakoutWidth;cellCount
171
- const sectionMatch = comment.match(/^cf:section:(\d+);([^;]*);([^;]*);([^;]*);(\d+)$/)
172
- if (sectionMatch) {
173
- return new UnsupportedBlock({
174
- rawHtml: `<!--${comment}-->`,
175
- source: "confluence"
176
- })
177
- }
178
-
179
- // cf:section-end:index
180
- if (comment.startsWith("cf:section-end:")) {
181
- return new UnsupportedBlock({
182
- rawHtml: `<!--${comment}-->`,
183
- source: "confluence"
184
- })
185
- }
186
-
187
- // cf:cell:sectionIndex;cellIndex
188
- const cellMatch = comment.match(/^cf:cell:(\d+);(\d+)$/)
189
- if (cellMatch) {
190
- return new UnsupportedBlock({
191
- rawHtml: `<!--${comment}-->`,
192
- source: "confluence"
193
- })
194
- }
195
-
196
- return null
197
- })
198
-
199
- /**
200
- * Convert hast Element to BlockNode or MacroNode.
201
- */
202
- const hastElementToNode = (element: HastElement): Effect.Effect<DocumentNode | null, ParseError> =>
203
- Effect.gen(function*() {
204
- const tagName = element.tagName.toLowerCase()
205
-
206
- // Heading
207
- if (/^h[1-6]$/.test(tagName)) {
208
- const levelStr = tagName[1]
209
- if (!levelStr) return null
210
- const level = parseInt(levelStr) as 1 | 2 | 3 | 4 | 5 | 6
211
- const children = yield* hastChildrenToInline(element.children)
212
- return new Heading({ level, children })
213
- }
214
-
215
- // Paragraph (with optional alignment and indent)
216
- if (tagName === "p") {
217
- const children = yield* hastChildrenToInline(element.children)
218
- const style = element.properties?.style as string | undefined
219
- let alignment: "left" | "center" | "right" | undefined
220
- let indent: number | undefined
221
-
222
- if (style) {
223
- const alignMatch = style.match(/text-align:\s*(left|center|right)/)
224
- if (alignMatch?.[1]) {
225
- alignment = alignMatch[1] as "left" | "center" | "right"
226
- }
227
- const marginMatch = style.match(/margin-left:\s*(\d+(?:\.\d+)?)\s*px/)
228
- if (marginMatch?.[1]) {
229
- indent = parseFloat(marginMatch[1])
230
- }
231
- }
232
-
233
- if (alignment !== undefined || indent !== undefined) {
234
- return new Paragraph({
235
- children,
236
- ...(alignment !== undefined ? { alignment } : {}),
237
- ...(indent !== undefined ? { indent } : {})
238
- })
239
- }
240
- return new Paragraph({ children })
241
- }
242
-
243
- // Code block
244
- if (tagName === "pre") {
245
- const codeEl = element.children.find(
246
- (c): c is HastElement => c.type === "element" && (c as HastElement).tagName === "code"
247
- )
248
- const code = codeEl ? getTextContent(codeEl) : getTextContent(element)
249
- const language = (element.properties?.["dataLanguage"] as string) || undefined
250
- return new CodeBlock({ code, language })
251
- }
252
-
253
- // Thematic break
254
- if (tagName === "hr") {
255
- return new ThematicBreak({})
256
- }
257
-
258
- // Image (supports both URL and Confluence attachments from preprocessed data)
259
- if (tagName === "img") {
260
- const src = element.properties?.src as string | undefined
261
- const dataAttachment = element.properties?.["dataAttachment"] as string | undefined
262
- const dataAlign = element.properties?.["dataAlign"] as string | undefined
263
- const dataWidth = element.properties?.["dataWidth"] as string | undefined
264
- const alt = (element.properties?.alt as string) || undefined
265
-
266
- // Confluence attachment (preprocessed)
267
- if (dataAttachment) {
268
- return new Image({
269
- attachment: { filename: dataAttachment },
270
- alt,
271
- ...(dataAlign ? { align: dataAlign } : {}),
272
- ...(dataWidth ? { width: parseInt(dataWidth) } : {})
273
- })
274
- }
275
-
276
- // URL-based image
277
- if (!src) return null
278
- return new Image({
279
- src,
280
- alt,
281
- title: (element.properties?.title as string) || undefined
282
- })
283
- }
284
-
285
- // Table
286
- if (tagName === "table") {
287
- return yield* parseTable(element)
288
- }
289
-
290
- // Task list (from preprocessed data)
291
- if (tagName === "ul" && element.properties?.["dataMacro"] === "task-list") {
292
- return yield* parseTaskList(element)
293
- }
294
-
295
- // Lists
296
- if (tagName === "ul" || tagName === "ol") {
297
- return yield* parseList(element, tagName === "ol")
298
- }
299
-
300
- // Block quote
301
- if (tagName === "blockquote") {
302
- const children = yield* hastChildrenToSimpleBlocks(element.children)
303
- return { _tag: "BlockQuote" as const, version: 1, children }
304
- }
305
-
306
- // Macro divs
307
- if (tagName === "div" && element.properties?.["dataMacro"]) {
308
- const macro = element.properties["dataMacro"] as string
309
- if ((PanelTypes as ReadonlyArray<string>).includes(macro)) {
310
- const children = yield* hastChildrenToSimpleBlocks(element.children)
311
- return {
312
- _tag: "InfoPanel" as const,
313
- version: 1,
314
- panelType: macro as (typeof PanelTypes)[number],
315
- title: (element.properties["dataTitle"] as string) || undefined,
316
- children
317
- } satisfies InfoPanel
318
- }
319
- }
320
-
321
- // Expand/details
322
- if (tagName === "details") {
323
- const summary = element.children.find(
324
- (c): c is HastElement => c.type === "element" && (c as HastElement).tagName === "summary"
325
- )
326
- const title = summary ? getTextContent(summary) : undefined
327
- const contentChildren = element.children.filter(
328
- (c) => !(c.type === "element" && (c as HastElement).tagName === "summary")
329
- )
330
- const children = yield* hastChildrenToSimpleBlocks(contentChildren)
331
- return {
332
- _tag: "ExpandMacro" as const,
333
- version: 1,
334
- title,
335
- children
336
- } satisfies ExpandMacro
337
- }
338
-
339
- // TOC
340
- if (tagName === "nav" && element.properties?.["dataMacro"] === "toc") {
341
- const minStr = element.properties["dataMin"] as string | undefined
342
- const maxStr = element.properties["dataMax"] as string | undefined
343
- return {
344
- _tag: "TocMacro" as const,
345
- version: 1,
346
- minLevel: minStr ? parseInt(minStr) : undefined,
347
- maxLevel: maxStr ? parseInt(maxStr) : undefined
348
- } satisfies TocMacro
349
- }
350
-
351
- // Unsupported macro
352
- if (element.properties?.["dataUnsupportedMacro"]) {
353
- return new UnsupportedBlock({
354
- rawHtml: hastToHtml(element),
355
- source: "confluence"
356
- })
357
- }
358
-
359
- // Generic div - recurse into children
360
- if (tagName === "div" || tagName === "section" || tagName === "article") {
361
- const children: Array<DocumentNode> = []
362
- for (const child of element.children) {
363
- if (child.type === "element") {
364
- const node = yield* hastElementToNode(child as HastElement)
365
- if (node !== null) children.push(node)
366
- }
367
- }
368
- if (children.length >= 1) {
369
- const first = children[0]
370
- return first !== undefined ? first : null
371
- }
372
- return null
373
- }
374
-
375
- // Ignore common layout elements
376
- if (["br", "html", "head", "body"].includes(tagName)) {
377
- return null
378
- }
379
-
380
- // Inline elements at block level - wrap in paragraph
381
- if (["a", "strong", "em", "b", "i", "u", "code", "del", "sub", "sup", "span"].includes(tagName)) {
382
- const inlineNode = yield* hastElementToInline(element)
383
- if (inlineNode) {
384
- return new Paragraph({ children: [inlineNode] })
385
- }
386
- return null
387
- }
388
-
389
- // Unknown block element
390
- return new UnsupportedBlock({
391
- rawHtml: hastToHtml(element),
392
- source: "confluence"
393
- })
394
- })
395
-
396
- /**
397
- * Convert hast children to inline nodes.
398
- */
399
- const hastChildrenToInline = (
400
- children: Array<HastNode>
401
- ): Effect.Effect<Array<InlineNode>, ParseError> =>
402
- Effect.gen(function*() {
403
- const nodes: Array<InlineNode> = []
404
- for (const child of children) {
405
- if (child.type === "text") {
406
- const textNode = child as HastText
407
- if (textNode.value.trim() || nodes.length > 0) {
408
- nodes.push(new Text({ value: textNode.value }))
409
- }
410
- } else if (child.type === "element") {
411
- const node = yield* hastElementToInline(child as HastElement)
412
- if (node !== null) nodes.push(node)
413
- }
414
- }
415
- return nodes
416
- })
417
-
418
- /**
419
- * Convert hast Element to InlineNode.
420
- */
421
- const hastElementToInline = (element: HastElement): Effect.Effect<InlineNode | null, ParseError> =>
422
- Effect.gen(function*() {
423
- const tagName = element.tagName.toLowerCase()
424
-
425
- // Strong/bold
426
- if (tagName === "strong" || tagName === "b") {
427
- const children = yield* hastChildrenToBaseInline(element.children)
428
- return new Strong({ children })
429
- }
430
-
431
- // Emphasis/italic
432
- if (tagName === "em" || tagName === "i") {
433
- const children = yield* hastChildrenToBaseInline(element.children)
434
- return new Emphasis({ children })
435
- }
436
-
437
- // Underline
438
- if (tagName === "u") {
439
- const children = yield* hastChildrenToBaseInline(element.children)
440
- return new Underline({ children })
441
- }
442
-
443
- // Strikethrough
444
- if (tagName === "del" || tagName === "s") {
445
- const children = yield* hastChildrenToBaseInline(element.children)
446
- return new Strikethrough({ children })
447
- }
448
-
449
- // Subscript
450
- if (tagName === "sub") {
451
- const children = yield* hastChildrenToBaseInline(element.children)
452
- return new Subscript({ children })
453
- }
454
-
455
- // Superscript
456
- if (tagName === "sup") {
457
- const children = yield* hastChildrenToBaseInline(element.children)
458
- return new Superscript({ children })
459
- }
460
-
461
- // Inline code
462
- if (tagName === "code") {
463
- return new InlineCode({ value: getTextContent(element) })
464
- }
465
-
466
- // Smart link (Jira, Confluence search, etc.) - preserve datasource for roundtrip
467
- if (tagName === "a" && element.properties?.["dataDatasource"]) {
468
- const href = element.properties?.href as string | undefined
469
- const appearance = (element.properties?.["dataCardAppearance"] as string) || "inline"
470
- const datasource = element.properties["dataDatasource"] as string
471
- return new UnsupportedInline({
472
- raw: `<!--cf:smartlink:${encodeURIComponent(href ?? "")};${encodeURIComponent(appearance)};${
473
- encodeURIComponent(datasource)
474
- }-->`,
475
- source: "confluence"
476
- })
477
- }
478
-
479
- // Link
480
- if (tagName === "a") {
481
- const href = element.properties?.href as string | undefined
482
- if (!href) return null
483
- const children = yield* hastChildrenToBaseInline(element.children)
484
- return new Link({
485
- href,
486
- title: (element.properties?.title as string) || undefined,
487
- children
488
- })
489
- }
490
-
491
- // Line break
492
- if (tagName === "br") {
493
- return new LineBreak({})
494
- }
495
-
496
- // Date/time (rehype converts datetime attr to camelCase dateTime)
497
- if (tagName === "time") {
498
- const datetime = (element.properties?.dateTime as string) || ""
499
- return new DateTime({ datetime })
500
- }
501
-
502
- // Emoticon (preprocessed from ac:emoticon)
503
- if (tagName === "span" && element.properties?.["dataEmoji"]) {
504
- const shortname = (element.properties["dataEmoji"] as string) || ""
505
- const emojiId = (element.properties["dataEmojiId"] as string) || ""
506
- const fallback = getTextContent(element)
507
- return new Emoticon({ shortname, emojiId, fallback })
508
- }
509
-
510
- // User mention (preprocessed from ac:link > ri:user)
511
- if (tagName === "span" && element.properties?.["dataUserMention"]) {
512
- const accountId = (element.properties["dataUserMention"] as string) || ""
513
- return new UserMention({ accountId })
514
- }
515
-
516
- // Confluence link with link-body (preprocessed from ac:link > ac:link-body)
517
- if (tagName === "span" && element.properties?.["dataConfluenceLink"] !== undefined) {
518
- const linkText = getTextContent(element)
519
- return new UnsupportedInline({
520
- raw: `<!--cf:link:${encodeURIComponent(linkText)}-->`,
521
- source: "confluence"
522
- })
523
- }
524
-
525
- // Status macro (inline) - use comment encoding for roundtrip
526
- if (tagName === "span" && element.properties?.["dataMacro"] === "status") {
527
- const color = (element.properties["dataColor"] as string) || ""
528
- const title = getTextContent(element)
529
- return new UnsupportedInline({
530
- raw: `<!--cf:status:${encodeURIComponent(title)};${encodeURIComponent(color)}-->`,
531
- source: "confluence"
532
- })
533
- }
534
-
535
- // TOC macro in inline context (e.g., inside table cell) - use comment encoding
536
- // Use ; as separator (not | which breaks markdown tables)
537
- if (tagName === "nav" && element.properties?.["dataMacro"] === "toc") {
538
- const minStr = element.properties["dataMin"] as string | undefined
539
- const maxStr = element.properties["dataMax"] as string | undefined
540
- return new UnsupportedInline({
541
- raw: `<!--cf:toc:${minStr ?? ""};${maxStr ?? ""}-->`,
542
- source: "confluence"
543
- })
544
- }
545
-
546
- // Colored text (span with color style)
547
- if (tagName === "span") {
548
- const style = element.properties?.style as string | undefined
549
- if (style) {
550
- const colorMatch = style.match(/(?:^|;)\s*color:\s*([^;]+)/)
551
- const bgMatch = style.match(/(?:^|;)\s*background-color:\s*([^;]+)/)
552
-
553
- if (colorMatch?.[1]) {
554
- const children = yield* hastChildrenToBaseInline(element.children)
555
- return new ColoredText({ color: colorMatch[1].trim(), children })
556
- }
557
-
558
- if (bgMatch?.[1]) {
559
- const children = yield* hastChildrenToBaseInline(element.children)
560
- return new Highlight({ backgroundColor: bgMatch[1].trim(), children })
561
- }
562
- }
563
-
564
- // Nested inline elements - extract content
565
- const children = yield* hastChildrenToInline(element.children)
566
- if (children.length === 1) {
567
- const first = children[0]
568
- return first !== undefined ? first : null
569
- }
570
- return null
571
- }
572
-
573
- // Images can be inline too
574
- if (tagName === "img") {
575
- return new UnsupportedInline({
576
- raw: hastToHtml(element),
577
- source: "confluence"
578
- })
579
- }
580
-
581
- // Unknown inline element
582
- return new UnsupportedInline({
583
- raw: hastToHtml(element),
584
- source: "confluence"
585
- })
586
- })
587
-
588
- /**
589
- * Convert hast children to base inline nodes (for Strong/Emphasis/Link children).
590
- */
591
- const hastChildrenToBaseInline = (
592
- children: Array<HastNode>
593
- ): Effect.Effect<Array<Text | InlineCode | LineBreak | UnsupportedInline>, ParseError> =>
594
- Effect.gen(function*() {
595
- const nodes: Array<Text | InlineCode | LineBreak | UnsupportedInline> = []
596
- for (const child of children) {
597
- if (child.type === "text") {
598
- const textNode = child as HastText
599
- nodes.push(new Text({ value: textNode.value }))
600
- } else if (child.type === "element") {
601
- const el = child as HastElement
602
- const tagName = el.tagName.toLowerCase()
603
- if (tagName === "code") {
604
- nodes.push(new InlineCode({ value: getTextContent(el) }))
605
- } else if (tagName === "br") {
606
- nodes.push(new LineBreak({}))
607
- } else {
608
- nodes.push(new UnsupportedInline({ raw: hastToHtml(el), source: "confluence" }))
609
- }
610
- }
611
- }
612
- return nodes
613
- })
614
-
615
- /**
616
- * Convert hast children to simple block nodes (non-recursive).
617
- */
618
- const hastChildrenToSimpleBlocks = (
619
- children: Array<HastNode>
620
- ): Effect.Effect<
621
- Array<Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock>,
622
- ParseError
623
- > =>
624
- Effect.gen(function*() {
625
- const blocks: Array<Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock> = []
626
- for (const child of children) {
627
- if (child.type === "element") {
628
- const el = child as HastElement
629
- const tagName = el.tagName.toLowerCase()
630
-
631
- if (/^h[1-6]$/.test(tagName)) {
632
- const levelStr = tagName[1]
633
- if (levelStr) {
634
- const level = parseInt(levelStr) as 1 | 2 | 3 | 4 | 5 | 6
635
- const inlineChildren = yield* hastChildrenToInline(el.children)
636
- blocks.push(new Heading({ level, children: inlineChildren }))
637
- }
638
- } else if (tagName === "p") {
639
- const inlineChildren = yield* hastChildrenToInline(el.children)
640
- blocks.push(new Paragraph({ children: inlineChildren }))
641
- } else if (tagName === "pre") {
642
- const codeEl = el.children.find(
643
- (c): c is HastElement => c.type === "element" && (c as HastElement).tagName === "code"
644
- )
645
- const code = codeEl ? getTextContent(codeEl) : getTextContent(el)
646
- blocks.push(new CodeBlock({ code }))
647
- } else if (tagName === "hr") {
648
- blocks.push(new ThematicBreak({}))
649
- } else if (tagName === "img") {
650
- const src = el.properties?.src as string | undefined
651
- if (src) blocks.push(new Image({ src }))
652
- } else if (tagName === "table") {
653
- blocks.push(yield* parseTable(el))
654
- } else {
655
- blocks.push(new UnsupportedBlock({ rawHtml: hastToHtml(el), source: "confluence" }))
656
- }
657
- }
658
- }
659
- return blocks
660
- })
661
-
662
- /**
663
- * Parse table element.
664
- */
665
- const parseTable = (element: HastElement): Effect.Effect<Table, ParseError> =>
666
- Effect.gen(function*() {
667
- let header: TableRow | undefined
668
- const rows: Array<TableRow> = []
669
-
670
- for (const child of element.children) {
671
- if (child.type !== "element") continue
672
- const el = child as HastElement
673
-
674
- if (el.tagName === "thead") {
675
- const tr = el.children.find(
676
- (c): c is HastElement => c.type === "element" && (c as HastElement).tagName === "tr"
677
- )
678
- if (tr) {
679
- header = yield* parseTableRow(tr, true)
680
- }
681
- } else if (el.tagName === "tbody") {
682
- for (const row of el.children) {
683
- if (row.type === "element" && (row as HastElement).tagName === "tr") {
684
- const tr = row as HastElement
685
- // Check if this row has all <th> cells - treat as header if no header yet
686
- const allTh = tr.children
687
- .filter((c) => c.type === "element")
688
- .every((c) => (c as HastElement).tagName === "th")
689
- if (allTh && !header && rows.length === 0) {
690
- header = yield* parseTableRow(tr, true)
691
- } else {
692
- rows.push(yield* parseTableRow(tr, false))
693
- }
694
- }
695
- }
696
- } else if (el.tagName === "tr") {
697
- rows.push(yield* parseTableRow(el, false))
698
- }
699
- }
700
-
701
- return new Table({ header, rows })
702
- })
703
-
704
- /**
705
- * Parse table row.
706
- */
707
- const parseTableRow = (element: HastElement, isHeader: boolean): Effect.Effect<TableRow, ParseError> =>
708
- Effect.gen(function*() {
709
- const cells: Array<TableCell> = []
710
- for (const child of element.children) {
711
- if (child.type === "element") {
712
- const el = child as HastElement
713
- if (el.tagName === "td" || el.tagName === "th") {
714
- const cellIsHeader = isHeader || el.tagName === "th"
715
- // Unwrap single <p> elements inside cells
716
- const children = yield* parseCellContent(el.children)
717
- cells.push(new TableCell({ isHeader: cellIsHeader, children }))
718
- }
719
- }
720
- }
721
- return new TableRow({ cells })
722
- })
723
-
724
- /**
725
- * Parse cell content, unwrapping single <p> elements.
726
- *
727
- * Confluence's editor frequently emits empty `<p>` placeholders alongside the
728
- * real content (e.g. `<p/><p>Must</p>` for a styled cell). Skip those so the
729
- * single-real-paragraph case still hits the unwrap path.
730
- */
731
- const parseCellContent = (children: Array<HastNode>): Effect.Effect<Array<InlineNode>, ParseError> =>
732
- Effect.gen(function*() {
733
- const isEmptyParagraph = (el: HastElement): boolean =>
734
- el.tagName.toLowerCase() === "p" &&
735
- el.children.every((c) => c.type === "text" && !(c as HastText).value.trim())
736
-
737
- // Find actual element children (skip whitespace text and empty <p> placeholders)
738
- const elementChildren = children.filter((c) => {
739
- if (c.type === "element") return !isEmptyParagraph(c as HastElement)
740
- if (c.type === "text" && (c as HastText).value.trim()) return true
741
- return false
742
- })
743
-
744
- // If single <p> element, unwrap it
745
- if (elementChildren.length === 1) {
746
- const first = elementChildren[0]
747
- if (first && first.type === "element" && (first as HastElement).tagName === "p") {
748
- return yield* hastChildrenToInline((first as HastElement).children)
749
- }
750
- }
751
-
752
- // Otherwise parse normally
753
- return yield* hastChildrenToInline(children)
754
- })
755
-
756
- // Type for blocks allowed inside list items: simple blocks plus nested Lists.
757
- type SimpleBlock = Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock | List
758
-
759
- /**
760
- * Parse task list element (preprocessed from ac:task-list).
761
- */
762
- const parseTaskList = (
763
- element: HastElement
764
- ): Effect.Effect<TaskList, ParseError> =>
765
- Effect.gen(function*() {
766
- const items: Array<TaskItem> = []
767
-
768
- for (const child of element.children) {
769
- if (child.type === "element" && (child as HastElement).tagName === "li") {
770
- const li = child as HastElement
771
- const id = (li.properties?.["dataTaskId"] as string) || ""
772
- const uuid = (li.properties?.["dataTaskUuid"] as string) || ""
773
- const status = (li.properties?.["dataTaskStatus"] as string) === "complete"
774
- ? "complete" as const
775
- : "incomplete" as const
776
- const body = yield* hastChildrenToInline(li.children)
777
-
778
- items.push({
779
- _tag: "TaskItem",
780
- id,
781
- uuid,
782
- status,
783
- body
784
- })
785
- }
786
- }
787
-
788
- return {
789
- _tag: "TaskList" as const,
790
- version: 1,
791
- children: items
792
- }
793
- })
794
-
795
- /**
796
- * Parse list element.
797
- */
798
- const parseList = (
799
- element: HastElement,
800
- ordered: boolean
801
- ): Effect.Effect<
802
- {
803
- _tag: "List"
804
- version: number
805
- ordered: boolean
806
- start?: number
807
- children: Array<{ _tag: "ListItem"; checked?: boolean; children: Array<SimpleBlock> }>
808
- },
809
- ParseError
810
- > =>
811
- Effect.gen(function*() {
812
- const items: Array<{ _tag: "ListItem"; checked?: boolean; children: Array<SimpleBlock> }> = []
813
- const startProp = element.properties?.start
814
- const start = ordered && startProp ? parseInt(String(startProp)) : undefined
815
-
816
- for (const child of element.children) {
817
- if (child.type === "element" && (child as HastElement).tagName === "li") {
818
- const li = child as HastElement
819
- const children = yield* parseListItemContent(li.children)
820
- // Check for task list items
821
- const checkbox = li.children.find(
822
- (c): c is HastElement =>
823
- c.type === "element" &&
824
- (c as HastElement).tagName === "input" &&
825
- (c as HastElement).properties?.type === "checkbox"
826
- )
827
- const checked = checkbox ? (checkbox.properties?.checked === true) : undefined
828
- if (checked !== undefined) {
829
- items.push({ _tag: "ListItem", checked, children })
830
- } else {
831
- items.push({ _tag: "ListItem", children })
832
- }
833
- }
834
- }
835
-
836
- if (start !== undefined) {
837
- return { _tag: "List" as const, version: 1, ordered, start, children: items }
838
- }
839
- return { _tag: "List" as const, version: 1, ordered, children: items }
840
- })
841
-
842
- /**
843
- * Parse list item content, handling nested lists and unwrapping single <p>.
844
- * Also handles direct text/inline content without wrapper elements.
845
- */
846
- const parseListItemContent = (
847
- children: Array<HastNode>
848
- ): Effect.Effect<Array<SimpleBlock>, ParseError> =>
849
- Effect.gen(function*() {
850
- const blocks: Array<SimpleBlock> = []
851
-
852
- // Check if there's any direct text/inline content (not wrapped in <p>)
853
- const hasDirectInlineContent = children.some((child) => {
854
- if (child.type === "text") {
855
- return (child as HastText).value.trim() !== ""
856
- }
857
- if (child.type === "element") {
858
- const tagName = (child as HastElement).tagName.toLowerCase()
859
- // Inline elements that should be wrapped in a paragraph
860
- return ["a", "strong", "em", "b", "i", "u", "code", "span", "del", "sub", "sup"].includes(tagName)
861
- }
862
- return false
863
- })
864
-
865
- // If there's direct inline content, wrap it all in a paragraph
866
- if (hasDirectInlineContent) {
867
- const inlineChildren = yield* hastChildrenToInline(children)
868
- if (inlineChildren.length > 0) {
869
- blocks.push(new Paragraph({ children: inlineChildren }))
870
- }
871
- // Also check for nested lists after the inline content
872
- for (const child of children) {
873
- if (child.type === "element") {
874
- const el = child as HastElement
875
- const tagName = el.tagName.toLowerCase()
876
- if (tagName === "ul" || tagName === "ol") {
877
- blocks.push(yield* parseList(el, tagName === "ol"))
878
- }
879
- }
880
- }
881
- return blocks
882
- }
883
-
884
- for (const child of children) {
885
- if (child.type !== "element") continue
886
- const el = child as HastElement
887
- const tagName = el.tagName.toLowerCase()
888
-
889
- // Single <p> inside list item - extract inline content as paragraph
890
- if (tagName === "p") {
891
- const inlineChildren = yield* hastChildrenToInline(el.children)
892
- blocks.push(new Paragraph({ children: inlineChildren }))
893
- } else if (tagName === "ul" || tagName === "ol") {
894
- blocks.push(yield* parseList(el, tagName === "ol"))
895
- } else if (tagName === "pre") {
896
- const codeEl = el.children.find(
897
- (c): c is HastElement => c.type === "element" && (c as HastElement).tagName === "code"
898
- )
899
- const code = codeEl ? getTextContent(codeEl) : getTextContent(el)
900
- blocks.push(new CodeBlock({ code }))
901
- } else if (tagName === "hr") {
902
- blocks.push(new ThematicBreak({}))
903
- } else if (tagName === "img") {
904
- const src = el.properties?.src as string | undefined
905
- if (src) blocks.push(new Image({ src }))
906
- } else if (tagName === "table") {
907
- blocks.push(yield* parseTable(el))
908
- } else if (/^h[1-6]$/.test(tagName)) {
909
- const levelStr = tagName[1]
910
- if (levelStr) {
911
- const level = parseInt(levelStr) as 1 | 2 | 3 | 4 | 5 | 6
912
- const inlineChildren = yield* hastChildrenToInline(el.children)
913
- blocks.push(new Heading({ level, children: inlineChildren }))
914
- }
915
- }
916
- }
917
-
918
- return blocks
919
- })
920
-
921
- /**
922
- * Get text content from hast node.
923
- */
924
- const getTextContent = (element: HastElement): string => {
925
- let text = ""
926
- for (const child of element.children) {
927
- if (child.type === "text") {
928
- text += (child as HastText).value
929
- } else if (child.type === "element") {
930
- text += getTextContent(child as HastElement)
931
- }
932
- }
933
- return text
934
- }
935
-
936
- /**
937
- * Convert hast element back to HTML string (for unsupported elements).
938
- */
939
- const hastToHtml = (element: HastElement): string => {
940
- const props = Object.entries(element.properties || {})
941
- .map(([k, v]) => {
942
- const attrName = k.replace(/([A-Z])/g, "-$1").toLowerCase()
943
- return `${attrName}="${String(v)}"`
944
- })
945
- .join(" ")
946
- const openTag = props ? `<${element.tagName} ${props}>` : `<${element.tagName}>`
947
- const closeTag = `</${element.tagName}>`
948
- const content = element.children
949
- .map((c) => {
950
- if (c.type === "text") return (c as HastText).value
951
- if (c.type === "element") return hastToHtml(c as HastElement)
952
- return ""
953
- })
954
- .join("")
955
- return `${openTag}${content}${closeTag}`
956
- }