@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,717 @@
1
+ /**
2
+ * Serializer for AST to Confluence storage format (HTML).
3
+ *
4
+ * @module
5
+ */
6
+ import * as Effect from "effect/Effect"
7
+ import type { CodeBlock, Heading, Image, Paragraph, Table, ThematicBreak, UnsupportedBlock } from "../ast/BlockNode.js"
8
+ import type { Document, DocumentNode } from "../ast/Document.js"
9
+ import type { InlineNode } from "../ast/InlineNode.js"
10
+ import type { SerializeError } from "../SchemaConverterError.js"
11
+
12
+ /**
13
+ * Serialize Document AST to Confluence storage format HTML.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { serializeToConfluence } from "@knpkv/confluence-to-markdown/serializers/ConfluenceSerializer"
18
+ * import { makeDocument, Heading, Text } from "@knpkv/confluence-to-markdown/ast"
19
+ * import { Effect } from "effect"
20
+ *
21
+ * Effect.gen(function* () {
22
+ * const doc = makeDocument([
23
+ * new Heading({ level: 1, children: [new Text({ value: "Title" })] })
24
+ * ])
25
+ * const html = yield* serializeToConfluence(doc)
26
+ * console.log(html) // <h1>Title</h1>
27
+ * })
28
+ * ```
29
+ *
30
+ * @category Serializers
31
+ */
32
+ export const serializeToConfluence = (doc: Document): Effect.Effect<string, SerializeError> =>
33
+ Effect.gen(function*() {
34
+ // 1-to-1 roundtrip: if rawConfluence is available, return it as-is
35
+ if (doc.rawConfluence !== undefined) {
36
+ return doc.rawConfluence
37
+ }
38
+
39
+ const parts: Array<string> = []
40
+ for (const node of doc.children) {
41
+ const serialized = yield* serializeDocumentNode(node)
42
+ parts.push(serialized)
43
+ }
44
+ const raw = parts.join("\n")
45
+ // Post-process to reconstruct layouts from markers
46
+ return reconstructLayouts(raw)
47
+ })
48
+
49
+ /**
50
+ * Reconstruct layouts from marker comments.
51
+ *
52
+ * Markers:
53
+ * - <!--cf:layout-start-->
54
+ * - <!--cf:section:index;type;breakoutMode;breakoutWidth;cellCount-->
55
+ * - <!--cf:cell:sectionIndex;cellIndex-->
56
+ * - <!--cf:section-end:index-->
57
+ * - <!--cf:layout-end-->
58
+ */
59
+ const reconstructLayouts = (html: string): string => {
60
+ // Check if there are any layout markers
61
+ if (!html.includes("<!--cf:layout-start-->")) {
62
+ return html
63
+ }
64
+
65
+ let result = html
66
+
67
+ // Process each layout block
68
+ const layoutRegex = /<!--cf:layout-start-->([\s\S]*?)<!--cf:layout-end-->/g
69
+ result = result.replace(layoutRegex, (_, layoutContent: string) => {
70
+ // Parse sections from the content
71
+ const sections: Array<{
72
+ type: string
73
+ breakoutMode: string
74
+ breakoutWidth: string
75
+ cells: Array<string>
76
+ }> = []
77
+
78
+ // Find all section markers
79
+ const sectionRegex = /<!--cf:section:(\d+);([^;]*);([^;]*);([^;]*);(\d+)-->/g
80
+ let sectionMatch
81
+ const sectionMeta: Array<
82
+ { index: number; type: string; breakoutMode: string; breakoutWidth: string; cellCount: number }
83
+ > = []
84
+
85
+ while ((sectionMatch = sectionRegex.exec(layoutContent)) !== null) {
86
+ sectionMeta.push({
87
+ index: parseInt(sectionMatch[1] ?? "0"),
88
+ type: decodeURIComponent(sectionMatch[2] ?? "fixed-width"),
89
+ breakoutMode: decodeURIComponent(sectionMatch[3] ?? ""),
90
+ breakoutWidth: decodeURIComponent(sectionMatch[4] ?? ""),
91
+ cellCount: parseInt(sectionMatch[5] ?? "0")
92
+ })
93
+ }
94
+
95
+ // For each section, extract cell content
96
+ for (const meta of sectionMeta) {
97
+ const cells: Array<string> = []
98
+
99
+ for (let cellIndex = 0; cellIndex < meta.cellCount; cellIndex++) {
100
+ const cellStartMarker = `<!--cf:cell:${meta.index};${cellIndex}-->`
101
+ const nextCellMarker = `<!--cf:cell:${meta.index};${cellIndex + 1}-->`
102
+ const sectionEndMarker = `<!--cf:section-end:${meta.index}-->`
103
+ const nextSectionMarker = `<!--cf:section:${meta.index + 1};`
104
+
105
+ const cellStart = layoutContent.indexOf(cellStartMarker)
106
+ if (cellStart === -1) continue
107
+
108
+ const contentStart = cellStart + cellStartMarker.length
109
+
110
+ // Find where this cell ends - either next cell, section end, or next section
111
+ let cellEnd = layoutContent.length
112
+ const nextCell = layoutContent.indexOf(nextCellMarker, contentStart)
113
+ const secEnd = layoutContent.indexOf(sectionEndMarker, contentStart)
114
+ const nextSec = layoutContent.indexOf(nextSectionMarker, contentStart)
115
+
116
+ if (nextCell !== -1 && nextCell < cellEnd) cellEnd = nextCell
117
+ if (secEnd !== -1 && secEnd < cellEnd) cellEnd = secEnd
118
+ if (nextSec !== -1 && nextSec < cellEnd) cellEnd = nextSec
119
+
120
+ const cellContent = layoutContent.slice(contentStart, cellEnd).trim()
121
+ cells.push(cellContent)
122
+ }
123
+
124
+ sections.push({
125
+ type: meta.type,
126
+ breakoutMode: meta.breakoutMode,
127
+ breakoutWidth: meta.breakoutWidth,
128
+ cells
129
+ })
130
+ }
131
+
132
+ // Build the layout HTML
133
+ const sectionHtml = sections.map((section) => {
134
+ const typeAttr = ` ac:type="${escapeHtml(section.type)}"`
135
+ const breakoutModeAttr = section.breakoutMode ? ` ac:breakout-mode="${escapeHtml(section.breakoutMode)}"` : ""
136
+ const breakoutWidthAttr = section.breakoutWidth ? ` ac:breakout-width="${escapeHtml(section.breakoutWidth)}"` : ""
137
+ const cellsHtml = section.cells.map((c) => `<ac:layout-cell>${c}</ac:layout-cell>`).join("")
138
+ return `<ac:layout-section${typeAttr}${breakoutModeAttr}${breakoutWidthAttr}>${cellsHtml}</ac:layout-section>`
139
+ }).join("")
140
+
141
+ return `<ac:layout>${sectionHtml}</ac:layout>`
142
+ })
143
+
144
+ return result
145
+ }
146
+
147
+ /**
148
+ * Serialize a document node to Confluence HTML.
149
+ */
150
+ const serializeDocumentNode = (node: DocumentNode): Effect.Effect<string, SerializeError> =>
151
+ Effect.gen(function*() {
152
+ switch (node._tag) {
153
+ // Block nodes
154
+ case "Heading":
155
+ return yield* serializeHeading({ level: node.level, children: node.children })
156
+ case "Paragraph":
157
+ return yield* serializeParagraph({
158
+ children: node.children,
159
+ alignment: node.alignment,
160
+ indent: node.indent
161
+ })
162
+ case "CodeBlock":
163
+ return serializeCodeBlock({ code: node.code, language: node.language })
164
+ case "ThematicBreak":
165
+ return "<hr/>"
166
+ case "Image":
167
+ return serializeImage({
168
+ src: node.src,
169
+ attachment: node.attachment,
170
+ alt: node.alt,
171
+ title: node.title,
172
+ align: node.align,
173
+ width: node.width
174
+ })
175
+ case "Table":
176
+ return yield* serializeTable({ header: node.header, rows: node.rows })
177
+ case "List":
178
+ return yield* serializeList({
179
+ ordered: node.ordered,
180
+ start: node.start,
181
+ children: node.children as unknown as Array<ListItemType>
182
+ })
183
+ case "BlockQuote":
184
+ return yield* serializeBlockQuote({ children: node.children as unknown as Array<SimpleBlock> })
185
+ case "UnsupportedBlock": {
186
+ const raw = node.rawHtml || node.rawMarkdown || ""
187
+ // Check for comment-encoded decision list
188
+ const decisionMatch = raw.match(/<!--cf:decision:(.*)-->/)
189
+ if (decisionMatch) {
190
+ const itemsStr = decisionMatch[1] ?? ""
191
+ const items = itemsStr.split("|").map((item) => {
192
+ const parts = item.split(";")
193
+ return {
194
+ localId: decodeURIComponent(parts[0] ?? ""),
195
+ state: decodeURIComponent(parts[1] ?? ""),
196
+ content: decodeURIComponent(parts[2] ?? "")
197
+ }
198
+ })
199
+ const decisionItems = items.map((item) =>
200
+ `<ac:adf-node type="decision-item"><ac:adf-attribute key="local-id">${
201
+ escapeHtml(item.localId)
202
+ }</ac:adf-attribute><ac:adf-attribute key="state">${
203
+ escapeHtml(item.state)
204
+ }</ac:adf-attribute><ac:adf-content>${escapeHtml(item.content)}</ac:adf-content></ac:adf-node>`
205
+ ).join("")
206
+ const fallbackItems = items.map((item) => `<li>${escapeHtml(item.content)}</li>`).join("")
207
+ return `<ac:adf-extension><ac:adf-node type="decision-list">${decisionItems}</ac:adf-node><ac:adf-fallback><ul class="decision-list">${fallbackItems}</ul></ac:adf-fallback></ac:adf-extension>`
208
+ }
209
+ // Layout markers are passed through - reconstructLayouts will process them
210
+ return raw
211
+ }
212
+
213
+ // Macro nodes - serialize to Confluence macros
214
+ case "InfoPanel":
215
+ return yield* serializeInfoPanel({
216
+ panelType: node.panelType,
217
+ title: node.title,
218
+ children: node.children as unknown as Array<SimpleBlock>
219
+ })
220
+ case "ExpandMacro":
221
+ return yield* serializeExpandMacro({
222
+ title: node.title,
223
+ children: node.children as unknown as Array<SimpleBlock>
224
+ })
225
+ case "TocMacro":
226
+ return serializeTocMacro({ minLevel: node.minLevel, maxLevel: node.maxLevel })
227
+ case "CodeMacro":
228
+ return serializeCodeMacro({
229
+ language: node.language,
230
+ title: node.title,
231
+ code: node.code,
232
+ lineNumbers: node.lineNumbers,
233
+ collapse: node.collapse,
234
+ firstLine: node.firstLine
235
+ })
236
+ case "StatusMacro":
237
+ return serializeStatusMacro({ text: node.text, color: node.color })
238
+ case "TaskList":
239
+ return yield* serializeTaskList(node.children)
240
+
241
+ default:
242
+ return ""
243
+ }
244
+ })
245
+
246
+ /**
247
+ * Serialize heading.
248
+ */
249
+ const serializeHeading = (
250
+ node: { level: 1 | 2 | 3 | 4 | 5 | 6; children: ReadonlyArray<InlineNode> }
251
+ ): Effect.Effect<string, SerializeError> =>
252
+ Effect.gen(function*() {
253
+ const content = yield* serializeInlineNodes(node.children)
254
+ return `<h${node.level}>${content}</h${node.level}>`
255
+ })
256
+
257
+ /**
258
+ * Serialize paragraph (with optional alignment and indent).
259
+ */
260
+ const serializeParagraph = (
261
+ node: {
262
+ children: ReadonlyArray<InlineNode>
263
+ alignment?: "left" | "center" | "right" | undefined
264
+ indent?: number | undefined
265
+ }
266
+ ): Effect.Effect<string, SerializeError> =>
267
+ Effect.gen(function*() {
268
+ const content = yield* serializeInlineNodes(node.children)
269
+ const styles: Array<string> = []
270
+ if (node.alignment) {
271
+ styles.push(`text-align: ${node.alignment};`)
272
+ }
273
+ if (node.indent) {
274
+ styles.push(`margin-left: ${node.indent}px;`)
275
+ }
276
+ const styleAttr = styles.length > 0 ? ` style="${styles.join(" ")}"` : ""
277
+ return `<p${styleAttr}>${content}</p>`
278
+ })
279
+
280
+ /**
281
+ * Serialize code block as Confluence code macro.
282
+ */
283
+ const serializeCodeBlock = (node: { code: string; language?: string | undefined }): string => {
284
+ const lang = node.language ? `<ac:parameter ac:name="language">${escapeHtml(node.language)}</ac:parameter>` : ""
285
+ return `<ac:structured-macro ac:name="code">${lang}<ac:plain-text-body><![CDATA[${node.code}]]></ac:plain-text-body></ac:structured-macro>`
286
+ }
287
+
288
+ /**
289
+ * Serialize image (supports both URL and Confluence attachments).
290
+ */
291
+ const serializeImage = (node: {
292
+ src?: string | undefined
293
+ attachment?: { filename: string; version?: number | undefined } | undefined
294
+ alt?: string | undefined
295
+ title?: string | undefined
296
+ align?: string | undefined
297
+ width?: number | undefined
298
+ }): string => {
299
+ // Confluence attachment
300
+ if (node.attachment) {
301
+ const alignAttr = node.align ? ` ac:align="${node.align}"` : ""
302
+ const widthAttr = node.width ? ` ac:width="${node.width}"` : ""
303
+ const altAttr = node.alt ? ` ac:alt="${escapeHtml(node.alt)}"` : ""
304
+ const versionAttr = node.attachment.version ? ` ri:version-at-save="${node.attachment.version}"` : ""
305
+ return `<ac:image${alignAttr}${widthAttr}${altAttr}><ri:attachment ri:filename="${
306
+ escapeHtml(node.attachment.filename)
307
+ }"${versionAttr}/></ac:image>`
308
+ }
309
+
310
+ // URL-based image
311
+ const src = node.src ?? ""
312
+ const alt = node.alt ? ` alt="${escapeHtml(node.alt)}"` : ""
313
+ const title = node.title ? ` title="${escapeHtml(node.title)}"` : ""
314
+ return `<img src="${escapeHtml(src)}"${alt}${title}/>`
315
+ }
316
+
317
+ /**
318
+ * Serialize table.
319
+ */
320
+ const serializeTable = (
321
+ node: {
322
+ header?:
323
+ | { cells: ReadonlyArray<{ isHeader?: boolean | undefined; children: ReadonlyArray<InlineNode> }> }
324
+ | undefined
325
+ rows: ReadonlyArray<
326
+ { cells: ReadonlyArray<{ isHeader?: boolean | undefined; children: ReadonlyArray<InlineNode> }> }
327
+ >
328
+ }
329
+ ): Effect.Effect<string, SerializeError> =>
330
+ Effect.gen(function*() {
331
+ const parts: Array<string> = ["<table>"]
332
+
333
+ // Header
334
+ if (node.header) {
335
+ parts.push("<thead><tr>")
336
+ for (const cell of node.header.cells) {
337
+ const content = yield* serializeInlineNodes(cell.children)
338
+ parts.push(`<th>${content}</th>`)
339
+ }
340
+ parts.push("</tr></thead>")
341
+ }
342
+
343
+ // Body
344
+ if (node.rows.length > 0) {
345
+ parts.push("<tbody>")
346
+ for (const row of node.rows) {
347
+ parts.push("<tr>")
348
+ for (const cell of row.cells) {
349
+ const tag = cell.isHeader ? "th" : "td"
350
+ const content = yield* serializeInlineNodes(cell.children)
351
+ parts.push(`<${tag}>${content}</${tag}>`)
352
+ }
353
+ parts.push("</tr>")
354
+ }
355
+ parts.push("</tbody>")
356
+ }
357
+
358
+ parts.push("</table>")
359
+ return parts.join("")
360
+ })
361
+
362
+ // Simple block type for list items
363
+ type SimpleBlock =
364
+ | Heading
365
+ | Paragraph
366
+ | CodeBlock
367
+ | ThematicBreak
368
+ | Image
369
+ | Table
370
+ | UnsupportedBlock
371
+
372
+ // List item type
373
+ type ListItemType = {
374
+ readonly _tag: "ListItem"
375
+ readonly checked?: boolean | undefined
376
+ readonly children: ReadonlyArray<SimpleBlock>
377
+ }
378
+
379
+ /**
380
+ * Serialize list.
381
+ */
382
+ const serializeList = (
383
+ node: { ordered: boolean; start?: number | undefined; children: ReadonlyArray<ListItemType> }
384
+ ): Effect.Effect<string, SerializeError> =>
385
+ Effect.gen(function*() {
386
+ const tag = node.ordered ? "ol" : "ul"
387
+ const startAttr = node.ordered && node.start && node.start !== 1 ? ` start="${node.start}"` : ""
388
+ const parts: Array<string> = [`<${tag}${startAttr}>`]
389
+
390
+ for (const item of node.children) {
391
+ parts.push("<li>")
392
+ if (item.checked !== undefined) {
393
+ const checked = item.checked ? " checked" : ""
394
+ parts.push(`<input type="checkbox"${checked}/>`)
395
+ }
396
+ for (const child of item.children) {
397
+ parts.push(yield* serializeSimpleBlock(child))
398
+ }
399
+ parts.push("</li>")
400
+ }
401
+
402
+ parts.push(`</${tag}>`)
403
+ return parts.join("")
404
+ })
405
+
406
+ /**
407
+ * Serialize simple block.
408
+ */
409
+ const serializeSimpleBlock = (node: SimpleBlock): Effect.Effect<string, SerializeError> =>
410
+ Effect.gen(function*() {
411
+ switch (node._tag) {
412
+ case "Heading":
413
+ return yield* serializeHeading(
414
+ node as unknown as { level: 1 | 2 | 3 | 4 | 5 | 6; children: ReadonlyArray<InlineNode> }
415
+ )
416
+ case "Paragraph":
417
+ return yield* serializeParagraph(node as unknown as { children: ReadonlyArray<InlineNode> })
418
+ case "CodeBlock":
419
+ return serializeCodeBlock(node as unknown as { code: string; language?: string | undefined })
420
+ case "ThematicBreak":
421
+ return "<hr/>"
422
+ case "Image":
423
+ return serializeImage(node as unknown as { src: string; alt?: string | undefined; title?: string | undefined })
424
+ case "Table":
425
+ return yield* serializeTable(
426
+ node as unknown as {
427
+ header?:
428
+ | { cells: ReadonlyArray<{ isHeader?: boolean | undefined; children: ReadonlyArray<InlineNode> }> }
429
+ | undefined
430
+ rows: ReadonlyArray<
431
+ { cells: ReadonlyArray<{ isHeader?: boolean | undefined; children: ReadonlyArray<InlineNode> }> }
432
+ >
433
+ }
434
+ )
435
+ case "UnsupportedBlock": {
436
+ const unsupported = node as unknown as { rawHtml?: string; rawMarkdown?: string }
437
+ return unsupported.rawHtml || unsupported.rawMarkdown || ""
438
+ }
439
+ default:
440
+ return ""
441
+ }
442
+ })
443
+
444
+ /**
445
+ * Serialize block quote.
446
+ */
447
+ const serializeBlockQuote = (
448
+ node: { children: ReadonlyArray<SimpleBlock> }
449
+ ): Effect.Effect<string, SerializeError> =>
450
+ Effect.gen(function*() {
451
+ const parts: Array<string> = ["<blockquote>"]
452
+ for (const child of node.children) {
453
+ parts.push(yield* serializeSimpleBlock(child as SimpleBlock))
454
+ }
455
+ parts.push("</blockquote>")
456
+ return parts.join("")
457
+ })
458
+
459
+ /**
460
+ * Serialize info panel as Confluence macro.
461
+ */
462
+ const serializeInfoPanel = (
463
+ node: { panelType: string; title?: string | undefined; children: ReadonlyArray<SimpleBlock> }
464
+ ): Effect.Effect<string, SerializeError> =>
465
+ Effect.gen(function*() {
466
+ const titleParam = node.title
467
+ ? `<ac:parameter ac:name="title">${escapeHtml(node.title)}</ac:parameter>`
468
+ : ""
469
+
470
+ const parts: Array<string> = [
471
+ `<ac:structured-macro ac:name="${node.panelType}">`,
472
+ titleParam,
473
+ "<ac:rich-text-body>"
474
+ ]
475
+
476
+ for (const child of node.children) {
477
+ parts.push(yield* serializeSimpleBlock(child as SimpleBlock))
478
+ }
479
+
480
+ parts.push("</ac:rich-text-body>")
481
+ parts.push("</ac:structured-macro>")
482
+ return parts.join("")
483
+ })
484
+
485
+ /**
486
+ * Serialize expand macro as Confluence macro.
487
+ */
488
+ const serializeExpandMacro = (
489
+ node: { title?: string | undefined; children: ReadonlyArray<SimpleBlock> }
490
+ ): Effect.Effect<string, SerializeError> =>
491
+ Effect.gen(function*() {
492
+ const titleParam = node.title
493
+ ? `<ac:parameter ac:name="title">${escapeHtml(node.title)}</ac:parameter>`
494
+ : ""
495
+
496
+ const parts: Array<string> = [
497
+ `<ac:structured-macro ac:name="expand">`,
498
+ titleParam,
499
+ "<ac:rich-text-body>"
500
+ ]
501
+
502
+ for (const child of node.children) {
503
+ parts.push(yield* serializeSimpleBlock(child as SimpleBlock))
504
+ }
505
+
506
+ parts.push("</ac:rich-text-body>")
507
+ parts.push("</ac:structured-macro>")
508
+ return parts.join("")
509
+ })
510
+
511
+ /**
512
+ * Serialize TOC macro.
513
+ */
514
+ const serializeTocMacro = (node: { minLevel?: number | undefined; maxLevel?: number | undefined }): string => {
515
+ const params: Array<string> = []
516
+ if (node.minLevel) {
517
+ params.push(`<ac:parameter ac:name="minLevel">${node.minLevel}</ac:parameter>`)
518
+ }
519
+ if (node.maxLevel) {
520
+ params.push(`<ac:parameter ac:name="maxLevel">${node.maxLevel}</ac:parameter>`)
521
+ }
522
+ return `<ac:structured-macro ac:name="toc">${params.join("")}</ac:structured-macro>`
523
+ }
524
+
525
+ /**
526
+ * Serialize code macro with full options.
527
+ */
528
+ const serializeCodeMacro = (
529
+ node: {
530
+ language?: string | undefined
531
+ title?: string | undefined
532
+ code: string
533
+ lineNumbers?: boolean | undefined
534
+ collapse?: boolean | undefined
535
+ firstLine?: number | undefined
536
+ }
537
+ ): string => {
538
+ const params: Array<string> = []
539
+ if (node.language) {
540
+ params.push(`<ac:parameter ac:name="language">${escapeHtml(node.language)}</ac:parameter>`)
541
+ }
542
+ if (node.title) {
543
+ params.push(`<ac:parameter ac:name="title">${escapeHtml(node.title)}</ac:parameter>`)
544
+ }
545
+ if (node.lineNumbers) {
546
+ params.push(`<ac:parameter ac:name="linenumbers">true</ac:parameter>`)
547
+ }
548
+ if (node.collapse) {
549
+ params.push(`<ac:parameter ac:name="collapse">true</ac:parameter>`)
550
+ }
551
+ if (node.firstLine) {
552
+ params.push(`<ac:parameter ac:name="firstline">${node.firstLine}</ac:parameter>`)
553
+ }
554
+
555
+ return `<ac:structured-macro ac:name="code">${
556
+ params.join("")
557
+ }<ac:plain-text-body><![CDATA[${node.code}]]></ac:plain-text-body></ac:structured-macro>`
558
+ }
559
+
560
+ /**
561
+ * Serialize status macro.
562
+ */
563
+ const serializeStatusMacro = (node: { text: string; color: string }): string => {
564
+ return `<ac:structured-macro ac:name="status"><ac:parameter ac:name="colour">${
565
+ escapeHtml(node.color)
566
+ }</ac:parameter><ac:parameter ac:name="title">${escapeHtml(node.text)}</ac:parameter></ac:structured-macro>`
567
+ }
568
+
569
+ /**
570
+ * Serialize task list to Confluence storage format.
571
+ */
572
+ const serializeTaskList = (
573
+ children: ReadonlyArray<{
574
+ _tag: "TaskItem"
575
+ id: string
576
+ uuid: string
577
+ status: "incomplete" | "complete"
578
+ body: ReadonlyArray<InlineNode>
579
+ }>
580
+ ): Effect.Effect<string, SerializeError> =>
581
+ Effect.gen(function*() {
582
+ const parts: Array<string> = [`<ac:task-list>`]
583
+
584
+ for (const item of children) {
585
+ const body = yield* serializeInlineNodes(item.body)
586
+ parts.push(
587
+ `<ac:task>` +
588
+ `<ac:task-id>${item.id}</ac:task-id>` +
589
+ `<ac:task-uuid>${item.uuid}</ac:task-uuid>` +
590
+ `<ac:task-status>${item.status}</ac:task-status>` +
591
+ `<ac:task-body><span class="placeholder-inline-tasks">${body}</span></ac:task-body>` +
592
+ `</ac:task>`
593
+ )
594
+ }
595
+
596
+ parts.push(`</ac:task-list>`)
597
+ return parts.join("\n")
598
+ })
599
+
600
+ /**
601
+ * Serialize inline nodes to HTML.
602
+ */
603
+ const serializeInlineNodes = (
604
+ nodes: ReadonlyArray<InlineNode>
605
+ ): Effect.Effect<string, SerializeError> =>
606
+ Effect.gen(function*() {
607
+ const parts: Array<string> = []
608
+ for (const node of nodes) {
609
+ parts.push(yield* serializeInlineNode(node))
610
+ }
611
+ return parts.join("")
612
+ })
613
+
614
+ /**
615
+ * Serialize inline node to HTML.
616
+ */
617
+ const serializeInlineNode = (node: InlineNode): Effect.Effect<string, SerializeError> =>
618
+ Effect.gen(function*() {
619
+ switch (node._tag) {
620
+ case "Text":
621
+ return escapeHtml(node.value)
622
+ case "Strong": {
623
+ const content = yield* serializeInlineNodes(node.children)
624
+ return `<strong>${content}</strong>`
625
+ }
626
+ case "Emphasis": {
627
+ const content = yield* serializeInlineNodes(node.children)
628
+ return `<em>${content}</em>`
629
+ }
630
+ case "Underline": {
631
+ const content = yield* serializeInlineNodes(node.children)
632
+ return `<u>${content}</u>`
633
+ }
634
+ case "Strikethrough": {
635
+ const content = yield* serializeInlineNodes(node.children)
636
+ return `<del>${content}</del>`
637
+ }
638
+ case "Subscript": {
639
+ const content = yield* serializeInlineNodes(node.children)
640
+ return `<sub>${content}</sub>`
641
+ }
642
+ case "Superscript": {
643
+ const content = yield* serializeInlineNodes(node.children)
644
+ return `<sup>${content}</sup>`
645
+ }
646
+ case "InlineCode":
647
+ return `<code>${escapeHtml(node.value)}</code>`
648
+ case "Link": {
649
+ const content = yield* serializeInlineNodes(node.children)
650
+ const title = node.title ? ` title="${escapeHtml(node.title)}"` : ""
651
+ return `<a href="${escapeHtml(node.href)}"${title}>${content}</a>`
652
+ }
653
+ case "LineBreak":
654
+ return "<br/>"
655
+ case "Emoticon":
656
+ return `<ac:emoticon ac:emoji-shortname="${escapeHtml(node.shortname)}" ac:emoji-id="${
657
+ escapeHtml(node.emojiId)
658
+ }" ac:emoji-fallback="${escapeHtml(node.fallback)}"/>`
659
+ case "UserMention":
660
+ return `<ac:link><ri:user ri:account-id="${escapeHtml(node.accountId)}"/></ac:link>`
661
+ case "DateTime":
662
+ return `<time datetime="${escapeHtml(node.datetime)}"/>`
663
+ case "ColoredText": {
664
+ const content = yield* serializeInlineNodes(node.children)
665
+ return `<span style="color: ${escapeHtml(node.color)};">${content}</span>`
666
+ }
667
+ case "Highlight": {
668
+ const content = yield* serializeInlineNodes(node.children)
669
+ return `<span style="background-color: ${escapeHtml(node.backgroundColor)};">${content}</span>`
670
+ }
671
+ case "UnsupportedInline": {
672
+ // Check for comment-encoded TOC and convert back to Confluence macro
673
+ const tocMatch = node.raw.match(/<!--cf:toc:([^;]*);([^;]*)-->/)
674
+ if (tocMatch) {
675
+ const minLevel = tocMatch[1]
676
+ const maxLevel = tocMatch[2]
677
+ let params = ""
678
+ if (minLevel) params += `<ac:parameter ac:name="minLevel">${minLevel}</ac:parameter>`
679
+ if (maxLevel) params += `<ac:parameter ac:name="maxLevel">${maxLevel}</ac:parameter>`
680
+ return `<ac:structured-macro ac:name="toc">${params}</ac:structured-macro>`
681
+ }
682
+ // Check for comment-encoded Status macro
683
+ const statusMatch = node.raw.match(/<!--cf:status:([^;]*);([^;]*)-->/)
684
+ if (statusMatch) {
685
+ const title = decodeURIComponent(statusMatch[1] ?? "")
686
+ const color = decodeURIComponent(statusMatch[2] ?? "")
687
+ let params = ""
688
+ if (title) params += `<ac:parameter ac:name="title">${escapeHtml(title)}</ac:parameter>`
689
+ if (color) params += `<ac:parameter ac:name="colour">${escapeHtml(color)}</ac:parameter>`
690
+ return `<ac:structured-macro ac:name="status">${params}</ac:structured-macro>`
691
+ }
692
+ // Check for comment-encoded Smart link (Jira, etc.)
693
+ const smartLinkMatch = node.raw.match(/<!--cf:smartlink:([^;]*);([^;]*);(.*)-->/)
694
+ if (smartLinkMatch) {
695
+ const href = decodeURIComponent(smartLinkMatch[1] ?? "")
696
+ const appearance = decodeURIComponent(smartLinkMatch[2] ?? "")
697
+ const datasource = decodeURIComponent(smartLinkMatch[3] ?? "")
698
+ return `<a href="${escapeHtml(href)}" data-card-appearance="${escapeHtml(appearance)}" data-datasource="${
699
+ escapeHtml(datasource)
700
+ }">${escapeHtml(href)}</a>`
701
+ }
702
+ return node.raw
703
+ }
704
+ default:
705
+ return ""
706
+ }
707
+ })
708
+
709
+ /**
710
+ * Escape HTML special characters.
711
+ */
712
+ const escapeHtml = (str: string): string =>
713
+ str
714
+ .replace(/&/g, "&amp;")
715
+ .replace(/</g, "&lt;")
716
+ .replace(/>/g, "&gt;")
717
+ .replace(/"/g, "&quot;")