@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.
- package/CHANGELOG.md +73 -0
- package/LICENSE +21 -0
- package/README.md +282 -14
- package/dist/ConfluenceAuth.d.ts +76 -0
- package/dist/ConfluenceAuth.d.ts.map +1 -0
- package/dist/ConfluenceAuth.js +366 -0
- package/dist/ConfluenceAuth.js.map +1 -0
- package/dist/ConfluenceClient.d.ts +26 -12
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +139 -97
- package/dist/ConfluenceClient.js.map +1 -1
- package/dist/ConfluenceConfig.d.ts +4 -24
- package/dist/ConfluenceConfig.d.ts.map +1 -1
- package/dist/ConfluenceConfig.js +45 -7
- package/dist/ConfluenceConfig.js.map +1 -1
- package/dist/ConfluenceError.d.ts +99 -16
- package/dist/ConfluenceError.d.ts.map +1 -1
- package/dist/ConfluenceError.js +88 -5
- package/dist/ConfluenceError.js.map +1 -1
- package/dist/GitError.d.ts +103 -0
- package/dist/GitError.d.ts.map +1 -0
- package/dist/GitError.js +85 -0
- package/dist/GitError.js.map +1 -0
- package/dist/GitService.d.ts +175 -0
- package/dist/GitService.d.ts.map +1 -0
- package/dist/GitService.js +431 -0
- package/dist/GitService.js.map +1 -0
- package/dist/LocalFileSystem.d.ts +29 -4
- package/dist/LocalFileSystem.d.ts.map +1 -1
- package/dist/LocalFileSystem.js +80 -6
- package/dist/LocalFileSystem.js.map +1 -1
- package/dist/MarkdownConverter.d.ts +49 -2
- package/dist/MarkdownConverter.d.ts.map +1 -1
- package/dist/MarkdownConverter.js +73 -111
- package/dist/MarkdownConverter.js.map +1 -1
- package/dist/SchemaConverterError.d.ts +108 -0
- package/dist/SchemaConverterError.d.ts.map +1 -0
- package/dist/SchemaConverterError.js +84 -0
- package/dist/SchemaConverterError.js.map +1 -0
- package/dist/Schemas.d.ts +225 -1
- package/dist/Schemas.d.ts.map +1 -1
- package/dist/Schemas.js +155 -6
- package/dist/Schemas.js.map +1 -1
- package/dist/SyncEngine.d.ts +30 -20
- package/dist/SyncEngine.d.ts.map +1 -1
- package/dist/SyncEngine.js +566 -117
- package/dist/SyncEngine.js.map +1 -1
- package/dist/ast/BlockNode.d.ts +468 -0
- package/dist/ast/BlockNode.d.ts.map +1 -0
- package/dist/ast/BlockNode.js +319 -0
- package/dist/ast/BlockNode.js.map +1 -0
- package/dist/ast/Document.d.ts +244 -0
- package/dist/ast/Document.d.ts.map +1 -0
- package/dist/ast/Document.js +69 -0
- package/dist/ast/Document.js.map +1 -0
- package/dist/ast/InlineNode.d.ts +477 -0
- package/dist/ast/InlineNode.d.ts.map +1 -0
- package/dist/ast/InlineNode.js +263 -0
- package/dist/ast/InlineNode.js.map +1 -0
- package/dist/ast/MacroNode.d.ts +267 -0
- package/dist/ast/MacroNode.d.ts.map +1 -0
- package/dist/ast/MacroNode.js +164 -0
- package/dist/ast/MacroNode.js.map +1 -0
- package/dist/ast/index.d.ts +10 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +14 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/bin.js +33 -149
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +15 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +86 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/clone.d.ts +12 -0
- package/dist/commands/clone.d.ts.map +1 -0
- package/dist/commands/clone.js +93 -0
- package/dist/commands/clone.js.map +1 -0
- package/dist/commands/delete.d.ts +13 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +48 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/errorHandler.d.ts +14 -0
- package/dist/commands/errorHandler.d.ts.map +1 -0
- package/dist/commands/errorHandler.js +33 -0
- package/dist/commands/errorHandler.js.map +1 -0
- package/dist/commands/git.d.ts +22 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +72 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/index.d.ts +11 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +11 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/layers.d.ts +31 -0
- package/dist/commands/layers.d.ts.map +1 -0
- package/dist/commands/layers.js +137 -0
- package/dist/commands/layers.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +80 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/pageTree.d.ts +18 -0
- package/dist/commands/pageTree.d.ts.map +1 -0
- package/dist/commands/pageTree.js +20 -0
- package/dist/commands/pageTree.js.map +1 -0
- package/dist/commands/shared.d.ts +15 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +27 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +101 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.d.ts +7 -0
- package/dist/internal/NodeLayers.d.ts.map +1 -0
- package/dist/internal/NodeLayers.js +19 -0
- package/dist/internal/NodeLayers.js.map +1 -0
- package/dist/internal/frontmatter.d.ts +10 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +16 -0
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/gitCommands.d.ts +78 -0
- package/dist/internal/gitCommands.d.ts.map +1 -0
- package/dist/internal/gitCommands.js +156 -0
- package/dist/internal/gitCommands.js.map +1 -0
- package/dist/internal/hashUtils.d.ts +42 -1
- package/dist/internal/hashUtils.d.ts.map +1 -1
- package/dist/internal/hashUtils.js +38 -2
- package/dist/internal/hashUtils.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +55 -0
- package/dist/internal/oauthServer.d.ts.map +1 -0
- package/dist/internal/oauthServer.js +110 -0
- package/dist/internal/oauthServer.js.map +1 -0
- package/dist/internal/pathUtils.d.ts +21 -4
- package/dist/internal/pathUtils.d.ts.map +1 -1
- package/dist/internal/pathUtils.js +24 -13
- package/dist/internal/pathUtils.js.map +1 -1
- package/dist/internal/tokenStorage.d.ts +75 -0
- package/dist/internal/tokenStorage.d.ts.map +1 -0
- package/dist/internal/tokenStorage.js +149 -0
- package/dist/internal/tokenStorage.js.map +1 -0
- package/dist/internal/userCache.d.ts +42 -0
- package/dist/internal/userCache.d.ts.map +1 -0
- package/dist/internal/userCache.js +51 -0
- package/dist/internal/userCache.js.map +1 -0
- package/dist/parsers/ConfluenceParser.d.ts +26 -0
- package/dist/parsers/ConfluenceParser.d.ts.map +1 -0
- package/dist/parsers/ConfluenceParser.js +792 -0
- package/dist/parsers/ConfluenceParser.js.map +1 -0
- package/dist/parsers/MarkdownParser.d.ts +26 -0
- package/dist/parsers/MarkdownParser.d.ts.map +1 -0
- package/dist/parsers/MarkdownParser.js +873 -0
- package/dist/parsers/MarkdownParser.js.map +1 -0
- package/dist/parsers/index.d.ts +8 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +8 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/schemas/ConfluenceSchema.d.ts +21 -0
- package/dist/schemas/ConfluenceSchema.d.ts.map +1 -0
- package/dist/schemas/ConfluenceSchema.js +38 -0
- package/dist/schemas/ConfluenceSchema.js.map +1 -0
- package/dist/schemas/ConversionSchema.d.ts +35 -0
- package/dist/schemas/ConversionSchema.d.ts.map +1 -0
- package/dist/schemas/ConversionSchema.js +208 -0
- package/dist/schemas/ConversionSchema.js.map +1 -0
- package/dist/schemas/MarkdownSchema.d.ts +21 -0
- package/dist/schemas/MarkdownSchema.d.ts.map +1 -0
- package/dist/schemas/MarkdownSchema.js +38 -0
- package/dist/schemas/MarkdownSchema.js.map +1 -0
- package/dist/schemas/hast/HastFromHtml.d.ts +27 -0
- package/dist/schemas/hast/HastFromHtml.d.ts.map +1 -0
- package/dist/schemas/hast/HastFromHtml.js +107 -0
- package/dist/schemas/hast/HastFromHtml.js.map +1 -0
- package/dist/schemas/hast/HastSchema.d.ts +195 -0
- package/dist/schemas/hast/HastSchema.d.ts.map +1 -0
- package/dist/schemas/hast/HastSchema.js +183 -0
- package/dist/schemas/hast/HastSchema.js.map +1 -0
- package/dist/schemas/hast/index.d.ts +9 -0
- package/dist/schemas/hast/index.d.ts.map +1 -0
- package/dist/schemas/hast/index.js +3 -0
- package/dist/schemas/hast/index.js.map +1 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +16 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts +30 -0
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +1 -0
- package/dist/schemas/mdast/MdastFromMarkdown.js +79 -0
- package/dist/schemas/mdast/MdastFromMarkdown.js.map +1 -0
- package/dist/schemas/mdast/MdastSchema.d.ts +385 -0
- package/dist/schemas/mdast/MdastSchema.d.ts.map +1 -0
- package/dist/schemas/mdast/MdastSchema.js +266 -0
- package/dist/schemas/mdast/MdastSchema.js.map +1 -0
- package/dist/schemas/mdast/index.d.ts +10 -0
- package/dist/schemas/mdast/index.d.ts.map +1 -0
- package/dist/schemas/mdast/index.js +4 -0
- package/dist/schemas/mdast/index.js.map +1 -0
- package/dist/schemas/mdast/mdastToString.d.ts +13 -0
- package/dist/schemas/mdast/mdastToString.d.ts.map +1 -0
- package/dist/schemas/mdast/mdastToString.js +85 -0
- package/dist/schemas/mdast/mdastToString.js.map +1 -0
- package/dist/schemas/nodes/block/BlockSchema.d.ts +43 -0
- package/dist/schemas/nodes/block/BlockSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/block/BlockSchema.js +634 -0
- package/dist/schemas/nodes/block/BlockSchema.js.map +1 -0
- package/dist/schemas/nodes/block/index.d.ts +7 -0
- package/dist/schemas/nodes/block/index.d.ts.map +1 -0
- package/dist/schemas/nodes/block/index.js +7 -0
- package/dist/schemas/nodes/block/index.js.map +1 -0
- package/dist/schemas/nodes/index.d.ts +9 -0
- package/dist/schemas/nodes/index.d.ts.map +1 -0
- package/dist/schemas/nodes/index.js +12 -0
- package/dist/schemas/nodes/index.js.map +1 -0
- package/dist/schemas/nodes/inline/InlineSchema.d.ts +48 -0
- package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/inline/InlineSchema.js +436 -0
- package/dist/schemas/nodes/inline/InlineSchema.js.map +1 -0
- package/dist/schemas/nodes/inline/index.d.ts +7 -0
- package/dist/schemas/nodes/inline/index.d.ts.map +1 -0
- package/dist/schemas/nodes/inline/index.js +7 -0
- package/dist/schemas/nodes/inline/index.js.map +1 -0
- package/dist/schemas/nodes/macro/MacroSchema.d.ts +27 -0
- package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/macro/MacroSchema.js +162 -0
- package/dist/schemas/nodes/macro/MacroSchema.js.map +1 -0
- package/dist/schemas/nodes/macro/index.d.ts +7 -0
- package/dist/schemas/nodes/macro/index.d.ts.map +1 -0
- package/dist/schemas/nodes/macro/index.js +7 -0
- package/dist/schemas/nodes/macro/index.js.map +1 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +24 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js +351 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -0
- package/dist/schemas/preprocessing/index.d.ts +8 -0
- package/dist/schemas/preprocessing/index.d.ts.map +1 -0
- package/dist/schemas/preprocessing/index.js +2 -0
- package/dist/schemas/preprocessing/index.js.map +1 -0
- package/dist/serializers/ConfluenceSerializer.d.ts +30 -0
- package/dist/serializers/ConfluenceSerializer.d.ts.map +1 -0
- package/dist/serializers/ConfluenceSerializer.js +551 -0
- package/dist/serializers/ConfluenceSerializer.js.map +1 -0
- package/dist/serializers/MarkdownSerializer.d.ts +34 -0
- package/dist/serializers/MarkdownSerializer.d.ts.map +1 -0
- package/dist/serializers/MarkdownSerializer.js +355 -0
- package/dist/serializers/MarkdownSerializer.js.map +1 -0
- package/dist/serializers/index.d.ts +8 -0
- package/dist/serializers/index.d.ts.map +1 -0
- package/dist/serializers/index.js +8 -0
- package/dist/serializers/index.js.map +1 -0
- package/package.json +32 -21
- package/src/ConfluenceAuth.ts +581 -0
- package/src/ConfluenceClient.ts +230 -165
- package/src/ConfluenceConfig.ts +63 -7
- package/src/ConfluenceError.ts +110 -14
- package/src/GitError.ts +92 -0
- package/src/GitService.ts +859 -0
- package/src/LocalFileSystem.ts +179 -9
- package/src/MarkdownConverter.ts +126 -122
- package/src/SchemaConverterError.ts +108 -0
- package/src/Schemas.ts +223 -6
- package/src/SyncEngine.ts +745 -162
- package/src/ast/BlockNode.ts +425 -0
- package/src/ast/Document.ts +90 -0
- package/src/ast/InlineNode.ts +323 -0
- package/src/ast/MacroNode.ts +245 -0
- package/src/ast/index.ts +83 -0
- package/src/bin.ts +50 -249
- package/src/commands/auth.ts +117 -0
- package/src/commands/clone.ts +145 -0
- package/src/commands/delete.ts +57 -0
- package/src/commands/errorHandler.ts +32 -0
- package/src/commands/git.ts +114 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/layers.ts +211 -0
- package/src/commands/new.ts +99 -0
- package/src/commands/pageTree.ts +40 -0
- package/src/commands/shared.ts +35 -0
- package/src/commands/sync.ts +129 -0
- package/src/index.ts +21 -1
- package/src/internal/NodeLayers.ts +21 -0
- package/src/internal/frontmatter.ts +21 -0
- package/src/internal/gitCommands.ts +229 -0
- package/src/internal/hashUtils.ts +65 -3
- package/src/internal/oauthServer.ts +199 -0
- package/src/internal/pathUtils.ts +34 -17
- package/src/internal/tokenStorage.ts +240 -0
- package/src/internal/userCache.ts +90 -0
- package/src/parsers/ConfluenceParser.ts +950 -0
- package/src/parsers/MarkdownParser.ts +1198 -0
- package/src/parsers/index.ts +8 -0
- package/src/schemas/ConfluenceSchema.ts +56 -0
- package/src/schemas/ConversionSchema.ts +318 -0
- package/src/schemas/MarkdownSchema.ts +56 -0
- package/src/schemas/hast/HastFromHtml.ts +153 -0
- package/src/schemas/hast/HastSchema.ts +274 -0
- package/src/schemas/hast/index.ts +35 -0
- package/src/schemas/index.ts +20 -0
- package/src/schemas/mdast/MdastFromMarkdown.ts +118 -0
- package/src/schemas/mdast/MdastSchema.ts +566 -0
- package/src/schemas/mdast/index.ts +59 -0
- package/src/schemas/mdast/mdastToString.ts +102 -0
- package/src/schemas/nodes/block/BlockSchema.ts +773 -0
- package/src/schemas/nodes/block/index.ts +13 -0
- package/src/schemas/nodes/index.ts +20 -0
- package/src/schemas/nodes/inline/InlineSchema.ts +523 -0
- package/src/schemas/nodes/inline/index.ts +14 -0
- package/src/schemas/nodes/macro/MacroSchema.ts +226 -0
- package/src/schemas/nodes/macro/index.ts +6 -0
- package/src/schemas/preprocessing/ConfluencePreprocessor.ts +446 -0
- package/src/schemas/preprocessing/index.ts +8 -0
- package/src/serializers/ConfluenceSerializer.ts +717 -0
- package/src/serializers/MarkdownSerializer.ts +493 -0
- package/src/serializers/index.ts +8 -0
- package/test/GitService.test.ts +209 -0
- package/test/MarkdownConverter.test.ts +37 -3
- package/test/Schemas.test.ts +97 -2
- package/test/ast/BlockNode.test.ts +265 -0
- package/test/ast/Document.test.ts +126 -0
- package/test/ast/InlineNode.test.ts +161 -0
- package/test/fixtures/integration-test.html.fixture +103 -0
- package/test/fixtures/integration-test.md.expected +257 -0
- package/test/integration.test.ts +269 -0
- package/test/oauthServer.test.ts +50 -0
- package/test/parsers/ConfluenceParser.test.ts +283 -0
- package/test/schemas/ConfluencePreprocessor.test.ts +180 -0
- package/test/schemas/ConversionSchema.test.ts +159 -0
- package/test/schemas/HastSchema.test.ts +138 -0
- package/test/schemas/MdastSchema.test.ts +145 -0
- package/test/schemas/nodes/block/BlockSchema.test.ts +173 -0
- package/test/schemas/nodes/inline/InlineSchema.test.ts +198 -0
- package/test/schemas/nodes/macro/MacroSchema.test.ts +142 -0
- package/test/tokenStorage.test.ts +99 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for confluence-to-markdown CLI.
|
|
3
|
+
*
|
|
4
|
+
* Tests full cycle: clone -> create page -> push -> pull -> modify -> push -> re-clone -> delete -> verify
|
|
5
|
+
*
|
|
6
|
+
* Requires:
|
|
7
|
+
* - CONFLUENCE_BASE_URL: Confluence base URL
|
|
8
|
+
* - CONFLUENCE_ROOT_PAGE_ID: Test page ID
|
|
9
|
+
* - OAuth tokens in ~/.confluence/ or CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from "node:child_process"
|
|
12
|
+
import * as fs from "node:fs"
|
|
13
|
+
import * as os from "node:os"
|
|
14
|
+
import * as path from "node:path"
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
|
16
|
+
|
|
17
|
+
const CLI_PATH = path.resolve(__dirname, "../dist/bin.js")
|
|
18
|
+
const BASE_URL = process.env.CONFLUENCE_BASE_URL
|
|
19
|
+
const ROOT_PAGE_ID = process.env.CONFLUENCE_ROOT_PAGE_ID
|
|
20
|
+
|
|
21
|
+
// Test state
|
|
22
|
+
interface TestState {
|
|
23
|
+
testDir: string
|
|
24
|
+
pageFile: string | null
|
|
25
|
+
pageSlug: string | null
|
|
26
|
+
pageId: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const state: TestState = {
|
|
30
|
+
testDir: "",
|
|
31
|
+
pageFile: null,
|
|
32
|
+
pageSlug: null,
|
|
33
|
+
pageId: null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// === Helper Functions ===
|
|
37
|
+
|
|
38
|
+
const runCli = (args: ReadonlyArray<string>, options?: { timeout?: number }): string => {
|
|
39
|
+
return execFileSync("node", [CLI_PATH, ...args], {
|
|
40
|
+
cwd: state.testDir,
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: options?.timeout ?? 60000
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const findFile = (dir: string, predicate: (name: string) => boolean): string | null => {
|
|
47
|
+
if (!fs.existsSync(dir)) return null
|
|
48
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const fullPath = path.join(dir, entry.name)
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
const found = findFile(fullPath, predicate)
|
|
53
|
+
if (found) return found
|
|
54
|
+
} else if (predicate(entry.name)) {
|
|
55
|
+
return fullPath
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const findTemplate = (): string | null => {
|
|
62
|
+
const docsDir = path.join(state.testDir, ".confluence/docs")
|
|
63
|
+
return findFile(docsDir, (name) => name.toLowerCase().includes("template") && name.endsWith(".md"))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const findPageBySlug = (slug: string): string | null => {
|
|
67
|
+
const docsDir = path.join(state.testDir, ".confluence/docs")
|
|
68
|
+
return findFile(docsDir, (name) => name === `${slug}.md`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// === Step Functions ===
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clone pages from Confluence with full history.
|
|
75
|
+
*/
|
|
76
|
+
const clonePages = (): string => {
|
|
77
|
+
const output = runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
|
|
78
|
+
|
|
79
|
+
expect(output).toContain("Cloning pages from Confluence")
|
|
80
|
+
expect(output).toMatch(/Cloned \d+ pages with \d+ commits/)
|
|
81
|
+
expect(fs.existsSync(path.join(state.testDir, ".confluence"))).toBe(true)
|
|
82
|
+
expect(fs.existsSync(path.join(state.testDir, ".confluence/config.json"))).toBe(true)
|
|
83
|
+
expect(fs.existsSync(path.join(state.testDir, ".confluence/.git"))).toBe(true)
|
|
84
|
+
|
|
85
|
+
return output
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Remove .confluence directory for fresh clone.
|
|
90
|
+
*/
|
|
91
|
+
const removeConfluenceDir = (): void => {
|
|
92
|
+
fs.rmSync(path.join(state.testDir, ".confluence"), { recursive: true, force: true })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a new page by copying template content.
|
|
97
|
+
*/
|
|
98
|
+
const createPageFromTemplate = (): { file: string; slug: string } => {
|
|
99
|
+
const templatePath = findTemplate()
|
|
100
|
+
expect(templatePath).not.toBeNull()
|
|
101
|
+
|
|
102
|
+
const templateContent = fs.readFileSync(templatePath!, "utf-8")
|
|
103
|
+
const contentMatch = templateContent.match(/^---[\s\S]*?---\s*([\s\S]*)$/)
|
|
104
|
+
const bodyContent = contentMatch ? contentMatch[1]!.trim() : templateContent
|
|
105
|
+
|
|
106
|
+
const templateDir = path.dirname(templatePath!)
|
|
107
|
+
const timestamp = Date.now()
|
|
108
|
+
const slug = `integration-test-${timestamp}`
|
|
109
|
+
const file = path.join(templateDir, `${slug}.md`)
|
|
110
|
+
|
|
111
|
+
const newPageContent = `---
|
|
112
|
+
title: "Integration Test ${timestamp}"
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
${bodyContent}
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
Created by integration test at ${new Date().toISOString()}
|
|
119
|
+
`
|
|
120
|
+
fs.writeFileSync(file, newPageContent)
|
|
121
|
+
expect(fs.existsSync(file)).toBe(true)
|
|
122
|
+
|
|
123
|
+
state.pageFile = file
|
|
124
|
+
state.pageSlug = slug
|
|
125
|
+
|
|
126
|
+
return { file, slug }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Commit current changes.
|
|
131
|
+
*/
|
|
132
|
+
const commitChanges = (message: string): string => {
|
|
133
|
+
const output = runCli(["commit", "-m", message])
|
|
134
|
+
expect(output).toContain("Committed:")
|
|
135
|
+
return output
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Push changes to Confluence.
|
|
140
|
+
*/
|
|
141
|
+
const pushChanges = (): { pushed: number; created: number; deleted: number } => {
|
|
142
|
+
const output = runCli(["push"], { timeout: 90000 })
|
|
143
|
+
|
|
144
|
+
const pushedMatch = output.match(/Pushed:\s*(\d+)/)
|
|
145
|
+
const createdMatch = output.match(/Created:\s*(\d+)/)
|
|
146
|
+
const deletedMatch = output.match(/Deleted:\s*(\d+)/)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
pushed: pushedMatch ? parseInt(pushedMatch[1]!, 10) : 0,
|
|
150
|
+
created: createdMatch ? parseInt(createdMatch[1]!, 10) : 0,
|
|
151
|
+
deleted: deletedMatch ? parseInt(deletedMatch[1]!, 10) : 0
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pull changes from Confluence.
|
|
157
|
+
*/
|
|
158
|
+
const pullChanges = (): string => {
|
|
159
|
+
return runCli(["pull"])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract pageId from file front-matter.
|
|
164
|
+
*/
|
|
165
|
+
const extractPageId = (filePath: string): string | null => {
|
|
166
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
167
|
+
const match = content.match(/pageId:\s*["']?(\d+)/)
|
|
168
|
+
return match ? match[1]! : null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Modify page content.
|
|
173
|
+
*/
|
|
174
|
+
const modifyPage = (filePath: string, marker: string): void => {
|
|
175
|
+
const content = fs.readFileSync(filePath, "utf-8")
|
|
176
|
+
fs.writeFileSync(filePath, content + `\n\n${marker}\n`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete a local file.
|
|
181
|
+
*/
|
|
182
|
+
const deleteLocalFile = (filePath: string): void => {
|
|
183
|
+
fs.unlinkSync(filePath)
|
|
184
|
+
expect(fs.existsSync(filePath)).toBe(false)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// === Tests ===
|
|
188
|
+
|
|
189
|
+
describe("CLI Integration - Page Creation Flow", () => {
|
|
190
|
+
beforeAll(() => {
|
|
191
|
+
state.testDir = fs.mkdtempSync(path.join(os.tmpdir(), "confluence-test-"))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
afterAll(() => {
|
|
195
|
+
if (state.testDir && fs.existsSync(state.testDir)) {
|
|
196
|
+
fs.rmSync(state.testDir, { recursive: true, force: true })
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("full cycle: clone -> create -> push -> pull -> modify -> push -> re-clone -> delete -> verify", () => {
|
|
201
|
+
// 1. Clone pages from Confluence
|
|
202
|
+
clonePages()
|
|
203
|
+
|
|
204
|
+
// 2. Create new page from template
|
|
205
|
+
const { file, slug } = createPageFromTemplate()
|
|
206
|
+
|
|
207
|
+
// 3. Commit and push new page
|
|
208
|
+
commitChanges("Add integration test page")
|
|
209
|
+
|
|
210
|
+
const statusBefore = runCli(["status"])
|
|
211
|
+
expect(statusBefore).toContain("Local Only:")
|
|
212
|
+
|
|
213
|
+
const pushResult1 = pushChanges()
|
|
214
|
+
expect(pushResult1.created).toBe(1)
|
|
215
|
+
|
|
216
|
+
// Verify file has pageId after push
|
|
217
|
+
const contentAfterPush = fs.readFileSync(file, "utf-8")
|
|
218
|
+
expect(contentAfterPush).toMatch(/pageId:\s*["']?\d+/)
|
|
219
|
+
expect(contentAfterPush).toMatch(/version:\s*\d+/)
|
|
220
|
+
expect(contentAfterPush).toMatch(/contentHash:/)
|
|
221
|
+
|
|
222
|
+
const pageId = extractPageId(file)
|
|
223
|
+
expect(pageId).not.toBeNull()
|
|
224
|
+
state.pageId = pageId
|
|
225
|
+
|
|
226
|
+
// 4. Pull should be no-op (already in sync)
|
|
227
|
+
const contentBeforePull = fs.readFileSync(file, "utf-8")
|
|
228
|
+
pullChanges()
|
|
229
|
+
const contentAfterPull = fs.readFileSync(file, "utf-8")
|
|
230
|
+
expect(contentAfterPull).toBe(contentBeforePull)
|
|
231
|
+
|
|
232
|
+
// 5. Modify page, commit, and push
|
|
233
|
+
const modifyMarker = `Modified at ${Date.now()}`
|
|
234
|
+
modifyPage(file, modifyMarker)
|
|
235
|
+
commitChanges("Modify integration test page")
|
|
236
|
+
|
|
237
|
+
const pushResult2 = pushChanges()
|
|
238
|
+
expect(pushResult2.pushed).toBe(1)
|
|
239
|
+
|
|
240
|
+
const contentAfterModify = fs.readFileSync(file, "utf-8")
|
|
241
|
+
expect(contentAfterModify).toContain(modifyMarker)
|
|
242
|
+
|
|
243
|
+
// 6. Remove and re-clone - verify idempotency
|
|
244
|
+
const contentBeforeReclone = fs.readFileSync(file, "utf-8")
|
|
245
|
+
removeConfluenceDir()
|
|
246
|
+
clonePages()
|
|
247
|
+
|
|
248
|
+
const reclonedFile = findPageBySlug(slug)
|
|
249
|
+
expect(reclonedFile).not.toBeNull()
|
|
250
|
+
expect(fs.existsSync(reclonedFile!)).toBe(true)
|
|
251
|
+
|
|
252
|
+
const contentAfterReclone = fs.readFileSync(reclonedFile!, "utf-8")
|
|
253
|
+
expect(contentAfterReclone).toBe(contentBeforeReclone)
|
|
254
|
+
|
|
255
|
+
// 7. Delete page via git workflow
|
|
256
|
+
deleteLocalFile(reclonedFile!)
|
|
257
|
+
commitChanges("Delete integration test page")
|
|
258
|
+
|
|
259
|
+
const pushResult3 = pushChanges()
|
|
260
|
+
expect(pushResult3.deleted).toBe(1)
|
|
261
|
+
|
|
262
|
+
// 8. Verify deletion - re-clone should not include the page
|
|
263
|
+
removeConfluenceDir()
|
|
264
|
+
clonePages()
|
|
265
|
+
|
|
266
|
+
const deletedFile = findPageBySlug(slug)
|
|
267
|
+
expect(deletedFile).toBeNull()
|
|
268
|
+
})
|
|
269
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
|
|
2
|
+
import * as HttpClient from "@effect/platform/HttpClient"
|
|
3
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
|
|
4
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Fiber from "effect/Fiber"
|
|
7
|
+
import * as Layer from "effect/Layer"
|
|
8
|
+
import { HttpServerFactoryLive } from "../src/internal/NodeLayers.js"
|
|
9
|
+
import { startCallbackServer } from "../src/internal/oauthServer.js"
|
|
10
|
+
|
|
11
|
+
const HttpClientLive = NodeHttpClient.layer
|
|
12
|
+
|
|
13
|
+
describe("oauthServer", () => {
|
|
14
|
+
describe("startCallbackServer", () => {
|
|
15
|
+
it.effect("starts server and returns port", () =>
|
|
16
|
+
Effect.gen(function*() {
|
|
17
|
+
const expectedState = "test-state-123"
|
|
18
|
+
const result = yield* startCallbackServer(expectedState)
|
|
19
|
+
|
|
20
|
+
expect(result.port).toBeGreaterThanOrEqual(8585)
|
|
21
|
+
expect(typeof result.codePromise).toBe("object")
|
|
22
|
+
expect(typeof result.shutdown).toBe("object")
|
|
23
|
+
|
|
24
|
+
// Clean up
|
|
25
|
+
yield* result.shutdown
|
|
26
|
+
}).pipe(Effect.provide(HttpServerFactoryLive)))
|
|
27
|
+
|
|
28
|
+
it.effect("handles successful callback with code", () =>
|
|
29
|
+
Effect.gen(function*() {
|
|
30
|
+
const expectedState = "test-state-456"
|
|
31
|
+
const { codePromise, port, shutdown } = yield* startCallbackServer(expectedState)
|
|
32
|
+
|
|
33
|
+
// Make callback request in background
|
|
34
|
+
const codeReceiver = yield* Effect.fork(codePromise)
|
|
35
|
+
|
|
36
|
+
// Simulate OAuth callback using Effect HttpClient
|
|
37
|
+
const client = yield* HttpClient.HttpClient
|
|
38
|
+
const request = HttpClientRequest.get(`http://localhost:${port}/callback`).pipe(
|
|
39
|
+
HttpClientRequest.setUrlParam("code", "auth_code_123"),
|
|
40
|
+
HttpClientRequest.setUrlParam("state", expectedState)
|
|
41
|
+
)
|
|
42
|
+
yield* client.execute(request)
|
|
43
|
+
|
|
44
|
+
const code = yield* Fiber.join(codeReceiver)
|
|
45
|
+
expect(code).toBe("auth_code_123")
|
|
46
|
+
|
|
47
|
+
yield* shutdown
|
|
48
|
+
}).pipe(Effect.provide(Layer.mergeAll(HttpServerFactoryLive, HttpClientLive))))
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
|
|
2
|
+
import * as NodePath from "@effect/platform-node/NodePath"
|
|
3
|
+
import * as FileSystem from "@effect/platform/FileSystem"
|
|
4
|
+
import * as Path from "@effect/platform/Path"
|
|
5
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Layer from "effect/Layer"
|
|
8
|
+
import { parseConfluenceHtml } from "../../src/parsers/ConfluenceParser.js"
|
|
9
|
+
import { parseMarkdown } from "../../src/parsers/MarkdownParser.js"
|
|
10
|
+
import { serializeToConfluence } from "../../src/serializers/ConfluenceSerializer.js"
|
|
11
|
+
import { serializeToMarkdown } from "../../src/serializers/MarkdownSerializer.js"
|
|
12
|
+
|
|
13
|
+
const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)
|
|
14
|
+
|
|
15
|
+
const getFixturesDir = Effect.gen(function*() {
|
|
16
|
+
const path = yield* Path.Path
|
|
17
|
+
return path.join(import.meta.dirname, "../fixtures")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const readFixture = (filename: string) =>
|
|
21
|
+
Effect.gen(function*() {
|
|
22
|
+
const fs = yield* FileSystem.FileSystem
|
|
23
|
+
const path = yield* Path.Path
|
|
24
|
+
const fixturesDir = yield* getFixturesDir
|
|
25
|
+
return yield* fs.readFileString(path.join(fixturesDir, filename))
|
|
26
|
+
}).pipe(Effect.provide(PlatformLive))
|
|
27
|
+
|
|
28
|
+
describe("ConfluenceParser", () => {
|
|
29
|
+
describe("parseConfluenceHtml", () => {
|
|
30
|
+
it.effect("parses basic headings", () =>
|
|
31
|
+
Effect.gen(function*() {
|
|
32
|
+
const html = "<h1>Title</h1><h2>Subtitle</h2>"
|
|
33
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
34
|
+
expect(doc.children.length).toBe(2)
|
|
35
|
+
expect(doc.children[0]?._tag).toBe("Heading")
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
it.effect("parses paragraphs", () =>
|
|
39
|
+
Effect.gen(function*() {
|
|
40
|
+
const html = "<p>Hello world</p>"
|
|
41
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
42
|
+
expect(doc.children.length).toBe(1)
|
|
43
|
+
expect(doc.children[0]?._tag).toBe("Paragraph")
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
it.effect("parses task lists", () =>
|
|
47
|
+
Effect.gen(function*() {
|
|
48
|
+
const html = `<ac:task-list ac:task-list-id="test">
|
|
49
|
+
<ac:task>
|
|
50
|
+
<ac:task-id>1</ac:task-id>
|
|
51
|
+
<ac:task-uuid>uuid-1</ac:task-uuid>
|
|
52
|
+
<ac:task-status>incomplete</ac:task-status>
|
|
53
|
+
<ac:task-body><span>Task 1</span></ac:task-body>
|
|
54
|
+
</ac:task>
|
|
55
|
+
<ac:task>
|
|
56
|
+
<ac:task-id>2</ac:task-id>
|
|
57
|
+
<ac:task-uuid>uuid-2</ac:task-uuid>
|
|
58
|
+
<ac:task-status>complete</ac:task-status>
|
|
59
|
+
<ac:task-body><span>Task 2</span></ac:task-body>
|
|
60
|
+
</ac:task>
|
|
61
|
+
</ac:task-list>`
|
|
62
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
63
|
+
expect(doc.children.length).toBe(1)
|
|
64
|
+
const taskList = doc.children[0]
|
|
65
|
+
expect(taskList?._tag).toBe("TaskList")
|
|
66
|
+
if (taskList?._tag === "TaskList") {
|
|
67
|
+
expect(taskList.children.length).toBe(2)
|
|
68
|
+
expect(taskList.children[0]?.status).toBe("incomplete")
|
|
69
|
+
expect(taskList.children[0]?.body[0]?._tag).toBe("Text")
|
|
70
|
+
expect(taskList.children[1]?.status).toBe("complete")
|
|
71
|
+
}
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
it.effect("parses images with attachments", () =>
|
|
75
|
+
Effect.gen(function*() {
|
|
76
|
+
const html = `<ac:image ac:align="center" ac:width="250" ac:alt="logo">
|
|
77
|
+
<ri:attachment ri:filename="logo.svg"/>
|
|
78
|
+
</ac:image>`
|
|
79
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
80
|
+
expect(doc.children.length).toBe(1)
|
|
81
|
+
const image = doc.children[0]
|
|
82
|
+
expect(image?._tag).toBe("Image")
|
|
83
|
+
if (image?._tag === "Image") {
|
|
84
|
+
expect(image.attachment?.filename).toBe("logo.svg")
|
|
85
|
+
expect(image.align).toBe("center")
|
|
86
|
+
expect(image.width).toBe(250)
|
|
87
|
+
expect(image.alt).toBe("logo")
|
|
88
|
+
}
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
it.effect("parses emoticons", () =>
|
|
92
|
+
Effect.gen(function*() {
|
|
93
|
+
const html =
|
|
94
|
+
`<p><ac:emoticon ac:emoji-shortname=":grinning:" ac:emoji-id="1f600" ac:emoji-fallback="smile"/></p>`
|
|
95
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
96
|
+
expect(doc.children.length).toBe(1)
|
|
97
|
+
const para = doc.children[0]
|
|
98
|
+
expect(para?._tag).toBe("Paragraph")
|
|
99
|
+
if (para?._tag === "Paragraph") {
|
|
100
|
+
const emoticon = para.children[0]
|
|
101
|
+
expect(emoticon?._tag).toBe("Emoticon")
|
|
102
|
+
if (emoticon?._tag === "Emoticon") {
|
|
103
|
+
expect(emoticon.shortname).toBe(":grinning:")
|
|
104
|
+
expect(emoticon.emojiId).toBe("1f600")
|
|
105
|
+
expect(emoticon.fallback).toBe("smile")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
it.effect("parses user mentions", () =>
|
|
111
|
+
Effect.gen(function*() {
|
|
112
|
+
const html = `<p><ac:link><ri:user ri:account-id="557058:abc123"/></ac:link></p>`
|
|
113
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
114
|
+
expect(doc.children.length).toBe(1)
|
|
115
|
+
const para = doc.children[0]
|
|
116
|
+
if (para?._tag === "Paragraph") {
|
|
117
|
+
const mention = para.children[0]
|
|
118
|
+
expect(mention?._tag).toBe("UserMention")
|
|
119
|
+
if (mention?._tag === "UserMention") {
|
|
120
|
+
expect(mention.accountId).toBe("557058:abc123")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
it.effect("parses colored text", () =>
|
|
126
|
+
Effect.gen(function*() {
|
|
127
|
+
const html = `<p><span style="color: rgb(255,0,0);">Red text</span></p>`
|
|
128
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
129
|
+
const para = doc.children[0]
|
|
130
|
+
if (para?._tag === "Paragraph") {
|
|
131
|
+
const colored = para.children[0]
|
|
132
|
+
expect(colored?._tag).toBe("ColoredText")
|
|
133
|
+
if (colored?._tag === "ColoredText") {
|
|
134
|
+
expect(colored.color).toBe("rgb(255,0,0)")
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}))
|
|
138
|
+
|
|
139
|
+
it.effect("parses highlighted text", () =>
|
|
140
|
+
Effect.gen(function*() {
|
|
141
|
+
const html = `<p><span style="background-color: rgb(255,255,0);">Highlighted</span></p>`
|
|
142
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
143
|
+
const para = doc.children[0]
|
|
144
|
+
if (para?._tag === "Paragraph") {
|
|
145
|
+
const highlight = para.children[0]
|
|
146
|
+
expect(highlight?._tag).toBe("Highlight")
|
|
147
|
+
if (highlight?._tag === "Highlight") {
|
|
148
|
+
expect(highlight.backgroundColor).toBe("rgb(255,255,0)")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}))
|
|
152
|
+
|
|
153
|
+
it.effect("parses paragraph with alignment", () =>
|
|
154
|
+
Effect.gen(function*() {
|
|
155
|
+
const html = `<p style="text-align: center;">Centered</p>`
|
|
156
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
157
|
+
const para = doc.children[0]
|
|
158
|
+
expect(para?._tag).toBe("Paragraph")
|
|
159
|
+
if (para?._tag === "Paragraph") {
|
|
160
|
+
expect(para.alignment).toBe("center")
|
|
161
|
+
}
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
it.effect("parses paragraph with indent", () =>
|
|
165
|
+
Effect.gen(function*() {
|
|
166
|
+
const html = `<p style="margin-left: 30px;">Indented</p>`
|
|
167
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
168
|
+
const para = doc.children[0]
|
|
169
|
+
expect(para?._tag === "Paragraph").toBe(true)
|
|
170
|
+
if (para?._tag === "Paragraph") {
|
|
171
|
+
expect(para.indent).toBe(30)
|
|
172
|
+
}
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
it.effect("parses underline, subscript, superscript", () =>
|
|
176
|
+
Effect.gen(function*() {
|
|
177
|
+
const html = `<p><u>Underline</u> <sub>Sub</sub> <sup>Sup</sup></p>`
|
|
178
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
179
|
+
const para = doc.children[0]
|
|
180
|
+
if (para?._tag === "Paragraph") {
|
|
181
|
+
const [u, , sub, , sup] = para.children
|
|
182
|
+
expect(u?._tag).toBe("Underline")
|
|
183
|
+
expect(sub?._tag).toBe("Subscript")
|
|
184
|
+
expect(sup?._tag).toBe("Superscript")
|
|
185
|
+
}
|
|
186
|
+
}))
|
|
187
|
+
|
|
188
|
+
it.effect("parses tables with proper cell content", () =>
|
|
189
|
+
Effect.gen(function*() {
|
|
190
|
+
const html = `<table>
|
|
191
|
+
<thead><tr><th><p>Header 1</p></th><th><p>Header 2</p></th></tr></thead>
|
|
192
|
+
<tbody><tr><td><p>Cell 1</p></td><td><p>Cell 2</p></td></tr></tbody>
|
|
193
|
+
</table>`
|
|
194
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
195
|
+
const table = doc.children[0]
|
|
196
|
+
expect(table?._tag).toBe("Table")
|
|
197
|
+
if (table?._tag === "Table") {
|
|
198
|
+
// Header should have 2 cells
|
|
199
|
+
expect(table.header?.cells.length).toBe(2)
|
|
200
|
+
// First header cell should have text "Header 1"
|
|
201
|
+
const headerCell = table.header?.cells[0]
|
|
202
|
+
if (headerCell) {
|
|
203
|
+
expect(headerCell.children[0]?._tag).toBe("Text")
|
|
204
|
+
if (headerCell.children[0]?._tag === "Text") {
|
|
205
|
+
expect(headerCell.children[0].value).toBe("Header 1")
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Body rows
|
|
209
|
+
expect(table.rows.length).toBe(1)
|
|
210
|
+
const bodyCell = table.rows[0]?.cells[0]
|
|
211
|
+
if (bodyCell) {
|
|
212
|
+
expect(bodyCell.children[0]?._tag).toBe("Text")
|
|
213
|
+
if (bodyCell.children[0]?._tag === "Text") {
|
|
214
|
+
expect(bodyCell.children[0].value).toBe("Cell 1")
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}))
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe("integration test fixture", () => {
|
|
222
|
+
it.effect("parses and serializes integration test fixture", () =>
|
|
223
|
+
Effect.gen(function*() {
|
|
224
|
+
const html = yield* readFixture("integration-test.html.fixture")
|
|
225
|
+
const doc = yield* parseConfluenceHtml(html)
|
|
226
|
+
|
|
227
|
+
// Doc should have children - layout markers plus actual content
|
|
228
|
+
expect(doc.children.length).toBeGreaterThan(0)
|
|
229
|
+
|
|
230
|
+
// Serialize to markdown - content should be readable (not URL-encoded)
|
|
231
|
+
const markdown = yield* serializeToMarkdown(doc)
|
|
232
|
+
// Layout markers should be present as comments
|
|
233
|
+
expect(markdown).toContain("cf:layout-start")
|
|
234
|
+
expect(markdown).toContain("cf:section:")
|
|
235
|
+
// Content should be readable markdown, not URL-encoded
|
|
236
|
+
expect(markdown).toContain("# Heading 1")
|
|
237
|
+
expect(markdown).toContain("## Heading 2")
|
|
238
|
+
|
|
239
|
+
// Serialize to confluence - should reconstruct layout structure
|
|
240
|
+
const confluenceHtml = yield* serializeToConfluence(doc)
|
|
241
|
+
expect(confluenceHtml).toContain("<ac:layout>")
|
|
242
|
+
expect(confluenceHtml).toContain("<ac:layout-section")
|
|
243
|
+
expect(confluenceHtml).toContain("<ac:layout-cell>")
|
|
244
|
+
// Note: ac:task-list may have attributes, so search for the tag name
|
|
245
|
+
expect(confluenceHtml).toContain("ac:task-list")
|
|
246
|
+
expect(confluenceHtml).toContain("<ac:task-status>incomplete</ac:task-status>")
|
|
247
|
+
expect(confluenceHtml).toContain("<ac:task-status>complete</ac:task-status>")
|
|
248
|
+
expect(confluenceHtml).toContain("ac:emoticon")
|
|
249
|
+
expect(confluenceHtml).toContain("ri:account-id")
|
|
250
|
+
}))
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe("roundtrip HTML -> MD -> HTML", () => {
|
|
254
|
+
it.effect("roundtrips integration-test.html with 1-to-1 preservation", () =>
|
|
255
|
+
Effect.gen(function*() {
|
|
256
|
+
const originalHtml = yield* readFixture("integration-test.html.fixture")
|
|
257
|
+
const doc1 = yield* parseConfluenceHtml(originalHtml)
|
|
258
|
+
const md = yield* serializeToMarkdown(doc1)
|
|
259
|
+
const doc2 = yield* parseMarkdown(md)
|
|
260
|
+
const finalHtml = yield* serializeToConfluence(doc2)
|
|
261
|
+
|
|
262
|
+
// 1-to-1 roundtrip: finalHtml should EXACTLY match originalHtml
|
|
263
|
+
expect(finalHtml).toBe(originalHtml)
|
|
264
|
+
}))
|
|
265
|
+
|
|
266
|
+
it.effect("roundtrips TOC macro", () =>
|
|
267
|
+
Effect.gen(function*() {
|
|
268
|
+
const html =
|
|
269
|
+
`<ac:structured-macro ac:name="toc"><ac:parameter ac:name="minLevel">2</ac:parameter></ac:structured-macro>`
|
|
270
|
+
const doc1 = yield* parseConfluenceHtml(html)
|
|
271
|
+
expect(doc1.children[0]?._tag).toBe("TocMacro")
|
|
272
|
+
|
|
273
|
+
const md = yield* serializeToMarkdown(doc1)
|
|
274
|
+
expect(md).toContain("[[toc]]")
|
|
275
|
+
|
|
276
|
+
const doc2 = yield* parseMarkdown(md)
|
|
277
|
+
expect(doc2.children[0]?._tag).toBe("TocMacro")
|
|
278
|
+
|
|
279
|
+
const html2 = yield* serializeToConfluence(doc2)
|
|
280
|
+
expect(html2).toContain("<ac:structured-macro ac:name=\"toc\">")
|
|
281
|
+
}))
|
|
282
|
+
})
|
|
283
|
+
})
|