@knpkv/confluence-to-markdown 0.2.0 → 0.4.2

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 +73 -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 +366 -0
  7. package/dist/ConfluenceAuth.js.map +1 -0
  8. package/dist/ConfluenceClient.d.ts +26 -12
  9. package/dist/ConfluenceClient.d.ts.map +1 -1
  10. package/dist/ConfluenceClient.js +139 -97
  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 +99 -16
  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 +32 -21
  255. package/src/ConfluenceAuth.ts +581 -0
  256. package/src/ConfluenceClient.ts +230 -165
  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
@@ -12,10 +12,10 @@ import type { ContentHash } from "./Brand.js"
12
12
  import type { FrontMatterError } from "./ConfluenceError.js"
13
13
  import { FileSystemError } from "./ConfluenceError.js"
14
14
  import type { ParsedMarkdown } from "./internal/frontmatter.js"
15
- import { parseMarkdown, serializeMarkdown } from "./internal/frontmatter.js"
16
- import { computeHash } from "./internal/hashUtils.js"
15
+ import { parseMarkdown, serializeMarkdown, serializeNewPageMarkdown } from "./internal/frontmatter.js"
16
+ import { computeHash, HashServiceLive } from "./internal/hashUtils.js"
17
17
  import { pageToDir, pageToPath } from "./internal/pathUtils.js"
18
- import type { PageFrontMatter } from "./Schemas.js"
18
+ import type { NewPageFrontMatter, PageFrontMatter } from "./Schemas.js"
19
19
 
20
20
  /**
21
21
  * Local markdown file representation.
@@ -28,6 +28,20 @@ export interface LocalFile {
28
28
  readonly isNew: boolean
29
29
  }
30
30
 
31
+ /**
32
+ * Page tree node for directory structure.
33
+ */
34
+ export interface PageTreeNode {
35
+ /** File path relative to docsPath */
36
+ readonly path: string
37
+ /** Page title */
38
+ readonly title: string
39
+ /** Page ID (null if new page) */
40
+ readonly pageId: string | null
41
+ /** Child pages */
42
+ readonly children: ReadonlyArray<PageTreeNode>
43
+ }
44
+
31
45
  /**
32
46
  * Local file system service for markdown operations.
33
47
  *
@@ -38,7 +52,7 @@ export interface LocalFile {
38
52
  *
39
53
  * const program = Effect.gen(function* () {
40
54
  * const fs = yield* LocalFileSystem
41
- * const files = yield* fs.listMarkdownFiles(".docs/confluence")
55
+ * const files = yield* fs.listMarkdownFiles(".confluence/docs")
42
56
  * console.log(files)
43
57
  * })
44
58
  * ```
@@ -91,12 +105,35 @@ export class LocalFileSystem extends Context.Tag(
91
105
  title: string,
92
106
  hasChildren: boolean,
93
107
  parentPath: string
94
- ) => string
108
+ ) => Effect.Effect<string>
95
109
 
96
110
  /**
97
111
  * Get the directory path for a page's children.
98
112
  */
99
- readonly getPageDir: (title: string, parentPath: string) => string
113
+ readonly getPageDir: (title: string, parentPath: string) => Effect.Effect<string>
114
+
115
+ /**
116
+ * Write a raw file (e.g., source HTML).
117
+ */
118
+ readonly writeFile: (filePath: string, content: string) => Effect.Effect<void, FileSystemError>
119
+
120
+ /**
121
+ * Build a tree of pages from directory structure.
122
+ */
123
+ readonly buildPageTree: (
124
+ docsPath: string,
125
+ rootPageId: string,
126
+ rootTitle: string
127
+ ) => Effect.Effect<PageTreeNode, FileSystemError | FrontMatterError>
128
+
129
+ /**
130
+ * Write a new page file with minimal front-matter.
131
+ */
132
+ readonly writeNewPageFile: (
133
+ filePath: string,
134
+ frontMatter: NewPageFrontMatter,
135
+ content: string
136
+ ) => Effect.Effect<void, FileSystemError>
100
137
  }
101
138
  >() {}
102
139
 
@@ -120,7 +157,9 @@ export const layer: Layer.Layer<LocalFileSystem, never, FileSystem.FileSystem |
120
157
  )
121
158
 
122
159
  const parsed: ParsedMarkdown = yield* parseMarkdown(filePath, content)
123
- const contentHash = computeHash(parsed.content)
160
+ const contentHash = yield* computeHash(parsed.content).pipe(
161
+ Effect.provide(HashServiceLive)
162
+ )
124
163
 
125
164
  return {
126
165
  path: filePath,
@@ -211,6 +250,134 @@ export const layer: Layer.Layer<LocalFileSystem, never, FileSystem.FileSystem |
211
250
  Effect.mapError((cause) => new FileSystemError({ operation: "read", path: filePath, cause }))
212
251
  )
213
252
 
253
+ const writeFile = (
254
+ filePath: string,
255
+ content: string
256
+ ): Effect.Effect<void, FileSystemError> =>
257
+ Effect.gen(function*() {
258
+ const dir = pathService.dirname(filePath)
259
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(
260
+ Effect.catchAll(() => Effect.void)
261
+ )
262
+
263
+ // Atomic write: write to temp file, then rename
264
+ const tempPath = `${filePath}.tmp.${Date.now()}`
265
+ yield* fs.writeFileString(tempPath, content).pipe(
266
+ Effect.mapError((cause) => new FileSystemError({ operation: "write", path: filePath, cause }))
267
+ )
268
+ yield* fs.rename(tempPath, filePath).pipe(
269
+ Effect.mapError((cause) => new FileSystemError({ operation: "rename", path: filePath, cause }))
270
+ )
271
+ })
272
+
273
+ // Wrap path functions to provide Path service
274
+ const getPagePath = (title: string, hasChildren: boolean, parentPath: string) =>
275
+ pageToPath(title, hasChildren, parentPath).pipe(
276
+ Effect.provide(Layer.succeed(Path.Path, pathService))
277
+ )
278
+
279
+ const getPageDir = (title: string, parentPath: string) =>
280
+ pageToDir(title, parentPath).pipe(
281
+ Effect.provide(Layer.succeed(Path.Path, pathService))
282
+ )
283
+
284
+ const writeNewPageFile = (
285
+ filePath: string,
286
+ frontMatter: NewPageFrontMatter,
287
+ content: string
288
+ ): Effect.Effect<void, FileSystemError> =>
289
+ Effect.gen(function*() {
290
+ const dir = pathService.dirname(filePath)
291
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(
292
+ Effect.catchAll(() => Effect.void)
293
+ )
294
+
295
+ const serialized = serializeNewPageMarkdown(frontMatter, content)
296
+
297
+ // Atomic write: write to temp file, then rename
298
+ const tempPath = `${filePath}.tmp.${Date.now()}`
299
+ yield* fs.writeFileString(tempPath, serialized).pipe(
300
+ Effect.mapError((cause) => new FileSystemError({ operation: "write", path: filePath, cause }))
301
+ )
302
+ yield* fs.rename(tempPath, filePath).pipe(
303
+ Effect.mapError((cause) => new FileSystemError({ operation: "rename", path: filePath, cause }))
304
+ )
305
+ })
306
+
307
+ const buildPageTree = (
308
+ docsPath: string,
309
+ rootPageId: string,
310
+ rootTitle: string
311
+ ): Effect.Effect<PageTreeNode, FileSystemError | FrontMatterError> =>
312
+ Effect.gen(function*() {
313
+ // Build a map of directory -> page info
314
+ const pagesByDir = new Map<string, { path: string; title: string; pageId: string | null }>()
315
+
316
+ // Get all markdown files
317
+ const files = yield* listMarkdownFiles(docsPath)
318
+
319
+ // Parse each file and map to its directory
320
+ for (const filePath of files) {
321
+ const localFile = yield* readMarkdownFile(filePath)
322
+ const relativePath = pathService.relative(docsPath, filePath)
323
+ const dirPath = pathService.dirname(relativePath)
324
+ const baseName = pathService.basename(filePath, ".md")
325
+
326
+ // Determine the page's "key" - the directory it owns children in
327
+ // e.g., "foo.md" owns "foo/" directory
328
+ const pageKey = dirPath === "." ? baseName : pathService.join(dirPath, baseName)
329
+
330
+ pagesByDir.set(pageKey, {
331
+ path: relativePath,
332
+ title: localFile.frontMatter?.title ?? baseName,
333
+ pageId: localFile.frontMatter?.pageId ?? null
334
+ })
335
+ }
336
+
337
+ // Build tree recursively
338
+ const buildNode = (
339
+ key: string,
340
+ path: string,
341
+ title: string,
342
+ pageId: string | null
343
+ ): PageTreeNode => {
344
+ // Find children: pages whose parent directory matches this key
345
+ const children: Array<PageTreeNode> = []
346
+
347
+ for (const [childKey, childInfo] of pagesByDir.entries()) {
348
+ const childDir = pathService.dirname(childKey)
349
+ if (childDir === key) {
350
+ children.push(buildNode(childKey, childInfo.path, childInfo.title, childInfo.pageId))
351
+ }
352
+ }
353
+
354
+ // Sort children by title
355
+ children.sort((a, b) => a.title.localeCompare(b.title))
356
+
357
+ return { path, title, pageId, children }
358
+ }
359
+
360
+ // Find root-level pages (those in docsPath root, or the root page itself)
361
+ const rootChildren: Array<PageTreeNode> = []
362
+
363
+ for (const [key, info] of pagesByDir.entries()) {
364
+ const dir = pathService.dirname(key)
365
+ if (dir === ".") {
366
+ rootChildren.push(buildNode(key, info.path, info.title, info.pageId))
367
+ }
368
+ }
369
+
370
+ // Sort children by title
371
+ rootChildren.sort((a, b) => a.title.localeCompare(b.title))
372
+
373
+ return {
374
+ path: "",
375
+ title: rootTitle,
376
+ pageId: rootPageId,
377
+ children: rootChildren
378
+ }
379
+ })
380
+
214
381
  return LocalFileSystem.of({
215
382
  readMarkdownFile,
216
383
  writeMarkdownFile,
@@ -218,8 +385,11 @@ export const layer: Layer.Layer<LocalFileSystem, never, FileSystem.FileSystem |
218
385
  ensureDir,
219
386
  deleteFile,
220
387
  exists,
221
- getPagePath: pageToPath,
222
- getPageDir: pageToDir
388
+ getPagePath,
389
+ getPageDir,
390
+ writeFile,
391
+ buildPageTree,
392
+ writeNewPageFile
223
393
  })
224
394
  })
225
395
  )
@@ -1,20 +1,23 @@
1
1
  /**
2
- * HTML to Markdown conversion service using unified/remark/rehype.
2
+ * HTML to Markdown conversion service using AST-based approach.
3
3
  *
4
4
  * @module
5
5
  */
6
6
  import * as Context from "effect/Context"
7
7
  import * as Effect from "effect/Effect"
8
8
  import * as Layer from "effect/Layer"
9
- import rehypeParse from "rehype-parse"
10
- import rehypeRemark from "rehype-remark"
11
- import rehypeStringify from "rehype-stringify"
12
- import remarkGfm from "remark-gfm"
13
- import remarkParse from "remark-parse"
14
- import remarkRehype from "remark-rehype"
15
- import remarkStringify from "remark-stringify"
16
- import { unified } from "unified"
9
+ import * as Schema from "effect/Schema"
10
+ import type { Document } from "./ast/Document.js"
17
11
  import { ConversionError } from "./ConfluenceError.js"
12
+ import { parseConfluenceHtml } from "./parsers/ConfluenceParser.js"
13
+ import { parseMarkdown } from "./parsers/MarkdownParser.js"
14
+ import { ParseError, type SerializeError } from "./SchemaConverterError.js"
15
+ import { ConfluenceToMarkdown, DocumentFromHast, DocumentFromMdast } from "./schemas/ConversionSchema.js"
16
+ import { HastFromHtml } from "./schemas/hast/index.js"
17
+ import { MdastFromMarkdown } from "./schemas/mdast/index.js"
18
+ import { PreprocessedHtmlFromConfluence } from "./schemas/preprocessing/index.js"
19
+ import { serializeToConfluence } from "./serializers/ConfluenceSerializer.js"
20
+ import { type SerializeOptions, serializeToMarkdown } from "./serializers/MarkdownSerializer.js"
18
21
 
19
22
  /**
20
23
  * Markdown conversion service for HTML <-> GFM conversion.
@@ -45,143 +48,144 @@ export class MarkdownConverter extends Context.Tag(
45
48
  /**
46
49
  * Convert Confluence storage format (HTML) to GitHub Flavored Markdown.
47
50
  */
48
- readonly htmlToMarkdown: (html: string) => Effect.Effect<string, ConversionError>
51
+ readonly htmlToMarkdown: (
52
+ html: string,
53
+ options?: SerializeOptions
54
+ ) => Effect.Effect<string, ConversionError>
49
55
 
50
56
  /**
51
57
  * Convert GitHub Flavored Markdown to HTML (Confluence storage format).
52
58
  */
53
59
  readonly markdownToHtml: (markdown: string) => Effect.Effect<string, ConversionError>
60
+
61
+ /**
62
+ * Parse Confluence HTML to Document AST.
63
+ */
64
+ readonly htmlToAst: (html: string) => Effect.Effect<Document, ParseError>
65
+
66
+ /**
67
+ * Parse Markdown to Document AST.
68
+ */
69
+ readonly markdownToAst: (markdown: string) => Effect.Effect<Document, ParseError>
70
+
71
+ /**
72
+ * Serialize Document AST to Confluence HTML.
73
+ */
74
+ readonly astToHtml: (doc: Document) => Effect.Effect<string, SerializeError>
75
+
76
+ /**
77
+ * Serialize Document AST to Markdown.
78
+ */
79
+ readonly astToMarkdown: (doc: Document) => Effect.Effect<string, SerializeError>
54
80
  }
55
81
  >() {}
56
82
 
57
- /** Maximum HTML input size (1MB) to prevent ReDoS attacks */
58
- const MAX_HTML_SIZE = 1024 * 1024
59
-
60
83
  /**
61
- * Strip Confluence-specific macros while preserving text content.
62
- * Uses iterative approach to avoid ReDoS with nested content.
84
+ * Layer that provides the MarkdownConverter service.
85
+ *
86
+ * @category Layers
63
87
  */
64
- const stripConfluenceMacros = (html: string): Effect.Effect<string, ConversionError> =>
65
- Effect.gen(function*() {
66
- // Limit input size to prevent ReDoS
67
- if (html.length > MAX_HTML_SIZE) {
68
- return yield* Effect.fail(
69
- new ConversionError({
70
- direction: "htmlToMarkdown",
71
- cause: `HTML input too large: ${html.length} bytes (max ${MAX_HTML_SIZE})`
72
- })
73
- )
74
- }
75
-
76
- let result = html
77
-
78
- // Process structured macros iteratively to handle nesting safely
79
- let iterations = 0
80
- const maxIterations = 100 // Prevent infinite loops
81
-
82
- while (iterations < maxIterations) {
83
- const macroStart = result.indexOf("<ac:structured-macro")
84
- if (macroStart === -1) break
85
-
86
- // Find matching closing tag by counting nesting
87
- let depth = 1
88
- let pos = macroStart + 20 // Skip past opening tag start
89
- let endPos = -1
90
-
91
- while (pos < result.length && depth > 0) {
92
- if (result.slice(pos, pos + 20) === "<ac:structured-macro") {
93
- depth++
94
- pos += 20
95
- } else if (result.slice(pos, pos + 21) === "</ac:structured-macro") {
96
- depth--
97
- if (depth === 0) {
98
- endPos = result.indexOf(">", pos) + 1
99
- }
100
- pos += 21
101
- } else {
102
- pos++
103
- }
104
- }
105
-
106
- if (endPos === -1) break // Malformed HTML, stop processing
107
-
108
- const macroContent = result.slice(macroStart, endPos)
109
-
110
- // Extract content from macro
111
- let replacement = ""
112
- const plainBodyStart = macroContent.indexOf("<ac:plain-text-body><![CDATA[")
113
- const plainBodyEnd = macroContent.indexOf("]]></ac:plain-text-body>")
114
- if (plainBodyStart !== -1 && plainBodyEnd !== -1) {
115
- const content = macroContent.slice(plainBodyStart + 29, plainBodyEnd)
116
- replacement = `<pre><code>${content}</code></pre>`
117
- } else {
118
- const richBodyStart = macroContent.indexOf("<ac:rich-text-body>")
119
- const richBodyEnd = macroContent.indexOf("</ac:rich-text-body>")
120
- if (richBodyStart !== -1 && richBodyEnd !== -1) {
121
- replacement = macroContent.slice(richBodyStart + 19, richBodyEnd)
122
- }
123
- }
88
+ export const layer: Layer.Layer<MarkdownConverter> = Layer.succeed(
89
+ MarkdownConverter,
90
+ MarkdownConverter.of({
91
+ htmlToMarkdown: (html, options) =>
92
+ Effect.gen(function*() {
93
+ // Use AST-based approach to preserve colors, underlines, etc.
94
+ const doc = yield* parseConfluenceHtml(html).pipe(
95
+ Effect.mapError((e) => new ConversionError({ direction: "htmlToMarkdown", cause: e.message }))
96
+ )
97
+ return yield* serializeToMarkdown(doc, options).pipe(
98
+ Effect.mapError((e) => new ConversionError({ direction: "htmlToMarkdown", cause: e.message }))
99
+ )
100
+ }),
124
101
 
125
- result = result.slice(0, macroStart) + replacement + result.slice(endPos)
126
- iterations++
127
- }
102
+ markdownToHtml: (markdown) =>
103
+ Effect.gen(function*() {
104
+ // Use AST-based approach for consistency
105
+ const doc = yield* parseMarkdown(markdown).pipe(
106
+ Effect.mapError((e) => new ConversionError({ direction: "markdownToHtml", cause: e.message }))
107
+ )
108
+ return yield* serializeToConfluence(doc).pipe(
109
+ Effect.mapError((e) => new ConversionError({ direction: "markdownToHtml", cause: e.message }))
110
+ )
111
+ }),
128
112
 
129
- // Remove remaining simple tags with non-greedy bounded patterns
130
- result = result
131
- .replace(/<ac:parameter[^>]{0,1000}>[^<]{0,10000}<\/ac:parameter>/gi, "")
132
- .replace(/<\/?ac:[a-z-]{1,50}[^>]{0,1000}>/gi, "")
133
- .replace(/<\/?ri:[a-z-]{1,50}[^>]{0,1000}\/?>/gi, "")
113
+ htmlToAst: (html) => parseConfluenceHtml(html),
134
114
 
135
- return result
136
- })
115
+ markdownToAst: (markdown) => parseMarkdown(markdown),
137
116
 
138
- /**
139
- * Create the markdown converter processor for HTML -> Markdown.
140
- */
141
- const createHtmlToMdProcessor = () =>
142
- unified()
143
- .use(rehypeParse, { fragment: true })
144
- .use(rehypeRemark)
145
- .use(remarkGfm)
146
- .use(remarkStringify)
117
+ astToHtml: (doc) => serializeToConfluence(doc),
147
118
 
148
- /**
149
- * Create the markdown converter processor for Markdown -> HTML.
150
- */
151
- const createMdToHtmlProcessor = () =>
152
- unified()
153
- .use(remarkParse)
154
- .use(remarkGfm)
155
- .use(remarkRehype)
156
- .use(rehypeStringify)
119
+ astToMarkdown: (doc) => serializeToMarkdown(doc)
120
+ })
121
+ )
157
122
 
158
123
  /**
159
- * Layer that provides the MarkdownConverter service.
124
+ * Schema-based layer for MarkdownConverter using Effect Schema transforms.
125
+ *
126
+ * This is an alternative implementation that uses the new Schema-based
127
+ * conversion pipeline. It provides the same API as the default layer.
128
+ *
129
+ * Note: For full fidelity, continue to use the default layer. This schema-based
130
+ * layer is useful for simpler use cases or when you want to leverage Schema
131
+ * composition.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * import { MarkdownConverter, schemaBasedLayer } from "@knpkv/confluence-to-markdown/MarkdownConverter"
136
+ * import { Effect } from "effect"
137
+ *
138
+ * const program = Effect.gen(function* () {
139
+ * const converter = yield* MarkdownConverter
140
+ * const md = yield* converter.htmlToMarkdown("<h1>Hello</h1>")
141
+ * })
142
+ *
143
+ * Effect.runPromise(
144
+ * program.pipe(Effect.provide(schemaBasedLayer))
145
+ * )
146
+ * ```
160
147
  *
161
148
  * @category Layers
162
149
  */
163
- export const layer: Layer.Layer<MarkdownConverter> = Layer.succeed(
150
+ export const schemaBasedLayer: Layer.Layer<MarkdownConverter> = Layer.succeed(
164
151
  MarkdownConverter,
165
152
  MarkdownConverter.of({
166
- htmlToMarkdown: (html) =>
153
+ // Note: Schema-based layer doesn't support includeRawSource option yet
154
+ htmlToMarkdown: (html, options) =>
167
155
  Effect.gen(function*() {
168
- const cleaned = yield* stripConfluenceMacros(html)
169
- return yield* Effect.try({
170
- try: () => {
171
- const result = createHtmlToMdProcessor().processSync(cleaned)
172
- return String(result).trim()
173
- },
174
- catch: (cause) => new ConversionError({ direction: "htmlToMarkdown", cause })
175
- })
176
- }),
156
+ if (options?.includeRawSource !== undefined) {
157
+ yield* Effect.logWarning("schemaBasedLayer: includeRawSource option is not supported, use default layer")
158
+ }
159
+ return yield* Schema.decode(ConfluenceToMarkdown)(html)
160
+ }).pipe(
161
+ Effect.mapError((e) => new ConversionError({ direction: "htmlToMarkdown", cause: e.message }))
162
+ ),
177
163
 
178
164
  markdownToHtml: (markdown) =>
179
- Effect.try({
180
- try: () => {
181
- const result = createMdToHtmlProcessor().processSync(markdown)
182
- return String(result).trim()
183
- },
184
- catch: (cause) => new ConversionError({ direction: "markdownToHtml", cause })
185
- })
165
+ Schema.encode(ConfluenceToMarkdown)(markdown).pipe(
166
+ Effect.mapError((e) => new ConversionError({ direction: "markdownToHtml", cause: e.message }))
167
+ ),
168
+
169
+ htmlToAst: (html) =>
170
+ Effect.gen(function*() {
171
+ const preprocessed = yield* Schema.decode(PreprocessedHtmlFromConfluence)(html)
172
+ const hast = yield* Schema.decode(HastFromHtml)(preprocessed)
173
+ return yield* Schema.decode(DocumentFromHast)(hast)
174
+ }).pipe(
175
+ Effect.mapError((e) => new ParseError({ source: "confluence", message: e.message }))
176
+ ),
177
+
178
+ markdownToAst: (markdown) =>
179
+ Effect.gen(function*() {
180
+ const mdast = yield* Schema.decode(MdastFromMarkdown)(markdown)
181
+ return yield* Schema.decode(DocumentFromMdast)(mdast)
182
+ }).pipe(
183
+ Effect.mapError((e) => new ParseError({ source: "markdown", message: e.message }))
184
+ ),
185
+
186
+ // For serialization, continue using the existing serializers for full fidelity
187
+ astToHtml: (doc) => serializeToConfluence(doc),
188
+
189
+ astToMarkdown: (doc) => serializeToMarkdown(doc)
186
190
  })
187
191
  )
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Error types for schema-based conversion.
3
+ *
4
+ * @module
5
+ */
6
+ import * as Data from "effect/Data"
7
+
8
+ /**
9
+ * Error thrown when parsing HTML or Markdown fails.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { Effect } from "effect"
14
+ * import { ParseError } from "@knpkv/confluence-to-markdown/SchemaConverterError"
15
+ *
16
+ * Effect.gen(function* () {
17
+ * // ... parsing operation
18
+ * }).pipe(
19
+ * Effect.catchTag("ParseError", (error) =>
20
+ * Effect.sync(() => console.error(`Parse error: ${error.message}`))
21
+ * )
22
+ * )
23
+ * ```
24
+ *
25
+ * @category Errors
26
+ */
27
+ export class ParseError extends Data.TaggedError("ParseError")<{
28
+ readonly source: "confluence" | "markdown"
29
+ readonly message: string
30
+ readonly position?: { readonly line: number; readonly column: number }
31
+ readonly rawContent?: string
32
+ }> {}
33
+
34
+ /**
35
+ * Error thrown when serializing AST to HTML or Markdown fails.
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * import { Effect } from "effect"
40
+ * import { SerializeError } from "@knpkv/confluence-to-markdown/SchemaConverterError"
41
+ *
42
+ * Effect.gen(function* () {
43
+ * // ... serialization operation
44
+ * }).pipe(
45
+ * Effect.catchTag("SerializeError", (error) =>
46
+ * Effect.sync(() => console.error(`Serialize error: ${error.message}`))
47
+ * )
48
+ * )
49
+ * ```
50
+ *
51
+ * @category Errors
52
+ */
53
+ export class SerializeError extends Data.TaggedError("SerializeError")<{
54
+ readonly target: "confluence" | "markdown"
55
+ readonly nodeType: string
56
+ readonly message: string
57
+ }> {}
58
+
59
+ /**
60
+ * Error thrown when migrating between schema versions fails.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * import { Effect } from "effect"
65
+ * import { MigrationError } from "@knpkv/confluence-to-markdown/SchemaConverterError"
66
+ *
67
+ * Effect.gen(function* () {
68
+ * // ... migration operation
69
+ * }).pipe(
70
+ * Effect.catchTag("MigrationError", (error) =>
71
+ * Effect.sync(() =>
72
+ * console.error(`Migration error: ${error.nodeType} v${error.fromVersion} -> v${error.toVersion}`)
73
+ * )
74
+ * )
75
+ * )
76
+ * ```
77
+ *
78
+ * @category Errors
79
+ */
80
+ export class MigrationError extends Data.TaggedError("MigrationError")<{
81
+ readonly nodeType: string
82
+ readonly fromVersion: number
83
+ readonly toVersion: number
84
+ readonly message: string
85
+ }> {}
86
+
87
+ /**
88
+ * Union of all schema converter errors.
89
+ *
90
+ * @category Errors
91
+ */
92
+ export type SchemaConverterError = ParseError | SerializeError | MigrationError
93
+
94
+ /**
95
+ * Type guard to check if error is a SchemaConverterError.
96
+ *
97
+ * @param error - The error to check
98
+ * @returns True if error is a SchemaConverterError
99
+ *
100
+ * @category Utilities
101
+ */
102
+ export const isSchemaConverterError = (error: unknown): error is SchemaConverterError =>
103
+ typeof error === "object" &&
104
+ error !== null &&
105
+ "_tag" in error &&
106
+ ["ParseError", "SerializeError", "MigrationError"].includes(
107
+ (error as { _tag: string })._tag
108
+ )