@knpkv/confluence-to-markdown 0.2.0 → 0.4.1

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