@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.
- package/CHANGELOG.md +60 -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 +356 -0
- package/dist/ConfluenceAuth.js.map +1 -0
- package/dist/ConfluenceClient.d.ts +26 -2
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +98 -92
- 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 +89 -6
- 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 +27 -16
- package/src/ConfluenceAuth.ts +571 -0
- package/src/ConfluenceClient.ts +188 -156
- 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,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, "&")
|
|
715
|
+
.replace(/</g, "<")
|
|
716
|
+
.replace(/>/g, ">")
|
|
717
|
+
.replace(/"/g, """)
|