@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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform schemas for macro nodes (Hast <-> AST).
|
|
3
|
+
*
|
|
4
|
+
* Provides transforms for Confluence-specific macro elements like info panels,
|
|
5
|
+
* expand sections, TOC, and status badges.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import type * as ParseResult from "effect/ParseResult"
|
|
11
|
+
import type {
|
|
12
|
+
BlockNode,
|
|
13
|
+
CodeBlock,
|
|
14
|
+
Heading,
|
|
15
|
+
Image,
|
|
16
|
+
Paragraph,
|
|
17
|
+
Table,
|
|
18
|
+
ThematicBreak,
|
|
19
|
+
UnsupportedBlock
|
|
20
|
+
} from "../../../ast/BlockNode.js"
|
|
21
|
+
import {
|
|
22
|
+
type CodeMacro,
|
|
23
|
+
type ExpandMacro,
|
|
24
|
+
type InfoPanel,
|
|
25
|
+
type MacroNode,
|
|
26
|
+
PanelTypes,
|
|
27
|
+
type StatusMacro,
|
|
28
|
+
type TocMacro
|
|
29
|
+
} from "../../../ast/MacroNode.js"
|
|
30
|
+
import type { HastElement, HastNode } from "../../hast/index.js"
|
|
31
|
+
import { getTextContent, isHastElement, makeHastElement, makeHastText } from "../../hast/index.js"
|
|
32
|
+
import type { MdastBlockContent } from "../../mdast/index.js"
|
|
33
|
+
import { makeMdastCode, makeMdastParagraph, makeMdastText, mdastToString } from "../../mdast/index.js"
|
|
34
|
+
import { blockNodeToHast, blockNodeToMdast } from "../block/index.js"
|
|
35
|
+
|
|
36
|
+
type SimpleBlock = Heading | Paragraph | CodeBlock | ThematicBreak | Image | Table | UnsupportedBlock
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert HAST element to AST macro node.
|
|
40
|
+
*/
|
|
41
|
+
export const macroNodeFromHastElement = (
|
|
42
|
+
element: HastElement,
|
|
43
|
+
parseBlockChildren: (
|
|
44
|
+
children: ReadonlyArray<HastNode>
|
|
45
|
+
) => Effect.Effect<ReadonlyArray<BlockNode>, ParseResult.ParseError>
|
|
46
|
+
): Effect.Effect<MacroNode | null, ParseResult.ParseError> =>
|
|
47
|
+
Effect.gen(function*() {
|
|
48
|
+
const tagName = element.tagName.toLowerCase()
|
|
49
|
+
|
|
50
|
+
// Info/warning/note panels
|
|
51
|
+
if (tagName === "div" && element.properties?.["dataMacro"]) {
|
|
52
|
+
const macro = element.properties["dataMacro"] as string
|
|
53
|
+
if ((PanelTypes as ReadonlyArray<string>).includes(macro)) {
|
|
54
|
+
const children = yield* parseBlockChildren(element.children)
|
|
55
|
+
// Cast to SimpleBlock[] - at runtime only simple blocks are parsed for panel children
|
|
56
|
+
return {
|
|
57
|
+
_tag: "InfoPanel" as const,
|
|
58
|
+
version: 1,
|
|
59
|
+
panelType: macro as (typeof PanelTypes)[number],
|
|
60
|
+
title: (element.properties["dataTitle"] as string) || undefined,
|
|
61
|
+
children: children as ReadonlyArray<SimpleBlock>
|
|
62
|
+
} satisfies InfoPanel
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Expand/details
|
|
67
|
+
if (tagName === "details") {
|
|
68
|
+
const summary = element.children.find(
|
|
69
|
+
(c): c is HastElement => isHastElement(c) && c.tagName === "summary"
|
|
70
|
+
)
|
|
71
|
+
const title = summary ? getTextContent(summary) : undefined
|
|
72
|
+
const contentChildren = element.children.filter(
|
|
73
|
+
(c) => !(isHastElement(c) && c.tagName === "summary")
|
|
74
|
+
)
|
|
75
|
+
const children = yield* parseBlockChildren(contentChildren)
|
|
76
|
+
// Cast to SimpleBlock[] - at runtime only simple blocks are parsed for expand children
|
|
77
|
+
return {
|
|
78
|
+
_tag: "ExpandMacro" as const,
|
|
79
|
+
version: 1,
|
|
80
|
+
title,
|
|
81
|
+
children: children as ReadonlyArray<SimpleBlock>
|
|
82
|
+
} satisfies ExpandMacro
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TOC
|
|
86
|
+
if (tagName === "nav" && element.properties?.["dataMacro"] === "toc") {
|
|
87
|
+
const minStr = element.properties["dataMin"] as string | undefined
|
|
88
|
+
const maxStr = element.properties["dataMax"] as string | undefined
|
|
89
|
+
return {
|
|
90
|
+
_tag: "TocMacro" as const,
|
|
91
|
+
version: 1,
|
|
92
|
+
minLevel: minStr ? parseInt(minStr) : undefined,
|
|
93
|
+
maxLevel: maxStr ? parseInt(maxStr) : undefined
|
|
94
|
+
} satisfies TocMacro
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Code macro (from preprocessed data)
|
|
98
|
+
if (tagName === "pre" && element.properties?.["dataMacro"] === "code") {
|
|
99
|
+
const codeEl = element.children.find(
|
|
100
|
+
(c): c is HastElement => isHastElement(c) && c.tagName === "code"
|
|
101
|
+
)
|
|
102
|
+
const code = codeEl ? getTextContent(codeEl) : getTextContent(element)
|
|
103
|
+
const language = (element.properties["dataLanguage"] as string) || undefined
|
|
104
|
+
return {
|
|
105
|
+
_tag: "CodeMacro" as const,
|
|
106
|
+
version: 1,
|
|
107
|
+
language,
|
|
108
|
+
code,
|
|
109
|
+
lineNumbers: false,
|
|
110
|
+
collapse: false
|
|
111
|
+
} satisfies CodeMacro
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Status macro
|
|
115
|
+
if (tagName === "span" && element.properties?.["dataMacro"] === "status") {
|
|
116
|
+
const color = (element.properties["dataColor"] as string) || "Grey"
|
|
117
|
+
const text = getTextContent(element)
|
|
118
|
+
return {
|
|
119
|
+
_tag: "StatusMacro" as const,
|
|
120
|
+
version: 1,
|
|
121
|
+
text,
|
|
122
|
+
color: normalizeStatusColor(color)
|
|
123
|
+
} satisfies StatusMacro
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalize status color to allowed values.
|
|
131
|
+
*/
|
|
132
|
+
const normalizeStatusColor = (color: string): "Grey" | "Red" | "Yellow" | "Green" | "Blue" => {
|
|
133
|
+
const normalized = color.toLowerCase()
|
|
134
|
+
switch (normalized) {
|
|
135
|
+
case "red":
|
|
136
|
+
return "Red"
|
|
137
|
+
case "yellow":
|
|
138
|
+
return "Yellow"
|
|
139
|
+
case "green":
|
|
140
|
+
return "Green"
|
|
141
|
+
case "blue":
|
|
142
|
+
return "Blue"
|
|
143
|
+
default:
|
|
144
|
+
return "Grey"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert AST macro node to HAST element.
|
|
150
|
+
*/
|
|
151
|
+
export const macroNodeToHast = (node: MacroNode): HastElement => {
|
|
152
|
+
switch (node._tag) {
|
|
153
|
+
case "InfoPanel":
|
|
154
|
+
return makeHastElement(
|
|
155
|
+
"div",
|
|
156
|
+
{
|
|
157
|
+
dataMacro: node.panelType,
|
|
158
|
+
...(node.title ? { dataTitle: node.title } : {})
|
|
159
|
+
},
|
|
160
|
+
node.children.map(blockNodeToHast)
|
|
161
|
+
)
|
|
162
|
+
case "ExpandMacro":
|
|
163
|
+
return makeHastElement(
|
|
164
|
+
"details",
|
|
165
|
+
{ dataMacro: "expand" },
|
|
166
|
+
[
|
|
167
|
+
...(node.title ? [makeHastElement("summary", {}, [makeHastText(node.title)])] : []),
|
|
168
|
+
...node.children.map(blockNodeToHast)
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
case "TocMacro":
|
|
172
|
+
return makeHastElement("nav", {
|
|
173
|
+
dataMacro: "toc",
|
|
174
|
+
...(node.minLevel !== undefined ? { dataMin: String(node.minLevel) } : {}),
|
|
175
|
+
...(node.maxLevel !== undefined ? { dataMax: String(node.maxLevel) } : {})
|
|
176
|
+
})
|
|
177
|
+
case "CodeMacro":
|
|
178
|
+
return makeHastElement(
|
|
179
|
+
"pre",
|
|
180
|
+
{
|
|
181
|
+
dataMacro: "code",
|
|
182
|
+
...(node.language ? { dataLanguage: node.language } : {})
|
|
183
|
+
},
|
|
184
|
+
[makeHastElement("code", {}, [makeHastText(node.code)])]
|
|
185
|
+
)
|
|
186
|
+
case "StatusMacro":
|
|
187
|
+
return makeHastElement(
|
|
188
|
+
"span",
|
|
189
|
+
{ dataMacro: "status", dataColor: node.color },
|
|
190
|
+
[makeHastText(node.text)]
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Convert AST macro node to MDAST block content.
|
|
197
|
+
*/
|
|
198
|
+
export const macroNodeToMdast = (node: MacroNode): MdastBlockContent => {
|
|
199
|
+
switch (node._tag) {
|
|
200
|
+
case "InfoPanel":
|
|
201
|
+
// Render as container syntax with children
|
|
202
|
+
return {
|
|
203
|
+
type: "html",
|
|
204
|
+
value: `:::${node.panelType}${node.title ? ` ${node.title}` : ""}\n${
|
|
205
|
+
node.children.map((c) => mdastToString(blockNodeToMdast(c))).join("\n")
|
|
206
|
+
}\n:::`
|
|
207
|
+
}
|
|
208
|
+
case "ExpandMacro":
|
|
209
|
+
return {
|
|
210
|
+
type: "html",
|
|
211
|
+
value: `<details>\n<summary>${node.title ?? ""}</summary>\n${
|
|
212
|
+
node.children.map((c) => mdastToString(blockNodeToMdast(c))).join("\n")
|
|
213
|
+
}\n</details>`
|
|
214
|
+
}
|
|
215
|
+
case "TocMacro":
|
|
216
|
+
return {
|
|
217
|
+
type: "html",
|
|
218
|
+
value: "[[toc]]"
|
|
219
|
+
}
|
|
220
|
+
case "CodeMacro":
|
|
221
|
+
return makeMdastCode(node.code, node.language)
|
|
222
|
+
case "StatusMacro":
|
|
223
|
+
// Render as badge-like text
|
|
224
|
+
return makeMdastParagraph([makeMdastText(`[${node.text}]`)])
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-based Confluence HTML preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* Transforms raw Confluence storage format into parseable HTML by expanding
|
|
5
|
+
* macros, converting task lists, and normalizing Confluence-specific markup.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type * as Brand from "effect/Brand"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import { pipe } from "effect/Function"
|
|
12
|
+
import * as ParseResult from "effect/ParseResult"
|
|
13
|
+
import * as Schema from "effect/Schema"
|
|
14
|
+
import { PanelTypes } from "../../ast/MacroNode.js"
|
|
15
|
+
|
|
16
|
+
/** Maximum HTML input size (1MB) to prevent ReDoS attacks */
|
|
17
|
+
const MAX_HTML_SIZE = 1024 * 1024
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Branded type for preprocessed Confluence HTML.
|
|
21
|
+
*
|
|
22
|
+
* @category Types
|
|
23
|
+
*/
|
|
24
|
+
export type PreprocessedHtml = string & Brand.Brand<"PreprocessedHtml">
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schema for preprocessed HTML brand.
|
|
28
|
+
*
|
|
29
|
+
* @category Schemas
|
|
30
|
+
*/
|
|
31
|
+
export const PreprocessedHtmlSchema = Schema.String.pipe(
|
|
32
|
+
Schema.brand("PreprocessedHtml")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Transform raw Confluence HTML to preprocessed HTML.
|
|
37
|
+
*
|
|
38
|
+
* Applies the following transformations:
|
|
39
|
+
* - Layout section extraction with markers
|
|
40
|
+
* - Structured macro expansion (code, info panels, expand, TOC, status)
|
|
41
|
+
* - Task list normalization
|
|
42
|
+
* - Image attachment processing
|
|
43
|
+
* - Emoticon conversion
|
|
44
|
+
* - User mention extraction
|
|
45
|
+
* - ADF extension handling
|
|
46
|
+
* - Namespace stripping (ac:, ri: tags)
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* import { PreprocessedHtmlFromConfluence } from "@knpkv/confluence-to-markdown/schemas/preprocessing"
|
|
51
|
+
* import * as Schema from "effect/Schema"
|
|
52
|
+
* import { Effect } from "effect"
|
|
53
|
+
*
|
|
54
|
+
* const program = Effect.gen(function* () {
|
|
55
|
+
* const html = yield* Schema.decode(PreprocessedHtmlFromConfluence)(
|
|
56
|
+
* '<ac:structured-macro ac:name="info"><ac:rich-text-body>Content</ac:rich-text-body></ac:structured-macro>'
|
|
57
|
+
* )
|
|
58
|
+
* // html contains: <div data-macro="info">Content</div>
|
|
59
|
+
* })
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @category Schemas
|
|
63
|
+
*/
|
|
64
|
+
const makePreprocessedHtml = Schema.decodeSync(PreprocessedHtmlSchema)
|
|
65
|
+
|
|
66
|
+
export const PreprocessedHtmlFromConfluence = Schema.transformOrFail(
|
|
67
|
+
Schema.String,
|
|
68
|
+
PreprocessedHtmlSchema,
|
|
69
|
+
{
|
|
70
|
+
strict: true,
|
|
71
|
+
decode: (html, _options, ast) =>
|
|
72
|
+
Effect.gen(function*() {
|
|
73
|
+
if (html.length > MAX_HTML_SIZE) {
|
|
74
|
+
return yield* Effect.fail(
|
|
75
|
+
new ParseResult.Type(
|
|
76
|
+
ast,
|
|
77
|
+
html,
|
|
78
|
+
`HTML input too large: ${html.length} bytes (max ${MAX_HTML_SIZE})`
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return pipe(html, preprocessConfluenceHtml, makePreprocessedHtml)
|
|
83
|
+
}),
|
|
84
|
+
encode: (preprocessed) =>
|
|
85
|
+
// Identity - branded string is already a string
|
|
86
|
+
Effect.succeed(preprocessed)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Main preprocessing pipeline.
|
|
92
|
+
*/
|
|
93
|
+
const preprocessConfluenceHtml = (html: string): string => {
|
|
94
|
+
let result = html
|
|
95
|
+
|
|
96
|
+
// 1. Process layouts FIRST - before any other preprocessing
|
|
97
|
+
result = preprocessLayouts(result)
|
|
98
|
+
|
|
99
|
+
// 2. Process structured macros iteratively
|
|
100
|
+
result = processStructuredMacros(result)
|
|
101
|
+
|
|
102
|
+
// 3. Process task lists BEFORE stripping ac: tags
|
|
103
|
+
result = preprocessTaskLists(result)
|
|
104
|
+
|
|
105
|
+
// 4. Process images with attachments
|
|
106
|
+
result = preprocessImages(result)
|
|
107
|
+
|
|
108
|
+
// 5. Process emoticons
|
|
109
|
+
result = preprocessEmoticons(result)
|
|
110
|
+
|
|
111
|
+
// 6. Process user mentions
|
|
112
|
+
result = preprocessUserMentions(result)
|
|
113
|
+
|
|
114
|
+
// 7. Process Confluence links with link-body
|
|
115
|
+
result = preprocessConfluenceLinks(result)
|
|
116
|
+
|
|
117
|
+
// 8. Process ADF extensions (decision lists)
|
|
118
|
+
result = preprocessAdfExtensions(result)
|
|
119
|
+
|
|
120
|
+
// 9. Strip remaining ac/ri namespace tags
|
|
121
|
+
result = stripNamespaces(result)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Process Confluence layouts.
|
|
128
|
+
* Inserts markers for roundtrip preservation.
|
|
129
|
+
*/
|
|
130
|
+
const preprocessLayouts = (html: string): string => {
|
|
131
|
+
return html.replace(
|
|
132
|
+
/<ac:layout>([\s\S]*?)<\/ac:layout>/gi,
|
|
133
|
+
(_, layoutContent) => {
|
|
134
|
+
let result = "<div data-cf-marker><!--cf:layout-start--></div>"
|
|
135
|
+
let sectionIndex = 0
|
|
136
|
+
|
|
137
|
+
const sectionRegex = /<ac:layout-section([^>]*)>([\s\S]*?)<\/ac:layout-section>/gi
|
|
138
|
+
let sectionMatch
|
|
139
|
+
while ((sectionMatch = sectionRegex.exec(layoutContent)) !== null) {
|
|
140
|
+
const sectionAttrs = sectionMatch[1] ?? ""
|
|
141
|
+
const sectionContent = sectionMatch[2] ?? ""
|
|
142
|
+
|
|
143
|
+
const typeMatch = sectionAttrs.match(/ac:type="([^"]*)"/)
|
|
144
|
+
const sectionType = typeMatch?.[1] ?? "fixed-width"
|
|
145
|
+
|
|
146
|
+
const breakoutModeMatch = sectionAttrs.match(/ac:breakout-mode="([^"]*)"/)
|
|
147
|
+
const breakoutWidthMatch = sectionAttrs.match(/ac:breakout-width="([^"]*)"/)
|
|
148
|
+
const breakoutMode = breakoutModeMatch?.[1] ?? ""
|
|
149
|
+
const breakoutWidth = breakoutWidthMatch?.[1] ?? ""
|
|
150
|
+
|
|
151
|
+
const cellContents: Array<string> = []
|
|
152
|
+
const cellRegex = /<ac:layout-cell[^>]*>([\s\S]*?)<\/ac:layout-cell>/gi
|
|
153
|
+
let cellMatch
|
|
154
|
+
while ((cellMatch = cellRegex.exec(sectionContent)) !== null) {
|
|
155
|
+
cellContents.push(cellMatch[1] ?? "")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
result += `<div data-cf-marker><!--cf:section:${sectionIndex};${encodeURIComponent(sectionType)};${
|
|
159
|
+
encodeURIComponent(breakoutMode)
|
|
160
|
+
};${encodeURIComponent(breakoutWidth)};${cellContents.length}--></div>`
|
|
161
|
+
|
|
162
|
+
cellContents.forEach((cellContent, cellIndex) => {
|
|
163
|
+
result += `<div data-cf-marker><!--cf:cell:${sectionIndex};${cellIndex}--></div>${cellContent}`
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
result += `<div data-cf-marker><!--cf:section-end:${sectionIndex}--></div>`
|
|
167
|
+
sectionIndex++
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
result += "<div data-cf-marker><!--cf:layout-end--></div>"
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Process structured macros iteratively.
|
|
178
|
+
*/
|
|
179
|
+
const processStructuredMacros = (html: string): string => {
|
|
180
|
+
let result = html
|
|
181
|
+
let iterations = 0
|
|
182
|
+
const maxIterations = 100
|
|
183
|
+
|
|
184
|
+
while (iterations < maxIterations) {
|
|
185
|
+
const macroStart = result.indexOf("<ac:structured-macro")
|
|
186
|
+
if (macroStart === -1) break
|
|
187
|
+
|
|
188
|
+
// First, find the end of the opening tag to check if self-closing
|
|
189
|
+
const openingTagEnd = result.indexOf(">", macroStart)
|
|
190
|
+
if (openingTagEnd === -1) break
|
|
191
|
+
|
|
192
|
+
// Check if self-closing (ends with />)
|
|
193
|
+
const isSelfClosing = result[openingTagEnd - 1] === "/"
|
|
194
|
+
|
|
195
|
+
let endPos: number
|
|
196
|
+
if (isSelfClosing) {
|
|
197
|
+
// Self-closing macro: <ac:structured-macro ... />
|
|
198
|
+
endPos = openingTagEnd + 1
|
|
199
|
+
} else {
|
|
200
|
+
// Regular macro with body: find matching closing tag
|
|
201
|
+
let depth = 1
|
|
202
|
+
let pos = openingTagEnd + 1
|
|
203
|
+
endPos = -1
|
|
204
|
+
|
|
205
|
+
while (pos < result.length && depth > 0) {
|
|
206
|
+
if (result.slice(pos, pos + 20) === "<ac:structured-macro") {
|
|
207
|
+
// Check if this nested opening is also self-closing
|
|
208
|
+
const nestedEnd = result.indexOf(">", pos)
|
|
209
|
+
if (nestedEnd !== -1 && result[nestedEnd - 1] === "/") {
|
|
210
|
+
// Self-closing nested macro - don't change depth
|
|
211
|
+
pos = nestedEnd + 1
|
|
212
|
+
} else {
|
|
213
|
+
depth++
|
|
214
|
+
pos += 20
|
|
215
|
+
}
|
|
216
|
+
} else if (result.slice(pos, pos + 21) === "</ac:structured-macro") {
|
|
217
|
+
depth--
|
|
218
|
+
if (depth === 0) {
|
|
219
|
+
endPos = result.indexOf(">", pos) + 1
|
|
220
|
+
}
|
|
221
|
+
pos += 21
|
|
222
|
+
} else {
|
|
223
|
+
pos++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (endPos === -1) break
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const macroContent = result.slice(macroStart, endPos)
|
|
231
|
+
const replacement = processSingleMacro(macroContent)
|
|
232
|
+
result = result.slice(0, macroStart) + replacement + result.slice(endPos)
|
|
233
|
+
iterations++
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Process a single Confluence macro to HTML.
|
|
241
|
+
*/
|
|
242
|
+
const processSingleMacro = (macroContent: string): string => {
|
|
243
|
+
const nameMatch = macroContent.match(/ac:name="([^"]+)"/)
|
|
244
|
+
const macroName = nameMatch?.[1] ?? ""
|
|
245
|
+
|
|
246
|
+
// Plain-text body (for code macros)
|
|
247
|
+
const plainBodyStart = macroContent.indexOf("<ac:plain-text-body><![CDATA[")
|
|
248
|
+
const plainBodyEnd = macroContent.indexOf("]]></ac:plain-text-body>")
|
|
249
|
+
if (plainBodyStart !== -1 && plainBodyEnd !== -1) {
|
|
250
|
+
const content = macroContent.slice(plainBodyStart + 29, plainBodyEnd)
|
|
251
|
+
const langMatch = macroContent.match(/ac:name="code".*?<ac:parameter[^>]*ac:name="language"[^>]*>([^<]+)/)
|
|
252
|
+
const language = langMatch?.[1] ?? ""
|
|
253
|
+
return `<pre data-macro="code" data-language="${language}"><code>${escapeHtml(content)}</code></pre>`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Rich-text body
|
|
257
|
+
const richBodyStart = macroContent.indexOf("<ac:rich-text-body>")
|
|
258
|
+
const richBodyEnd = macroContent.indexOf("</ac:rich-text-body>")
|
|
259
|
+
if (richBodyStart !== -1 && richBodyEnd !== -1) {
|
|
260
|
+
const content = macroContent.slice(richBodyStart + 19, richBodyEnd)
|
|
261
|
+
|
|
262
|
+
// Info/warning/note panels
|
|
263
|
+
if ((PanelTypes as ReadonlyArray<string>).includes(macroName)) {
|
|
264
|
+
const titleMatch = macroContent.match(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]+)/)
|
|
265
|
+
const title = titleMatch?.[1] ?? ""
|
|
266
|
+
return `<div data-macro="${macroName}" data-title="${escapeHtml(title)}">${content}</div>`
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Expand macro
|
|
270
|
+
if (macroName === "expand") {
|
|
271
|
+
const titleMatch = macroContent.match(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]+)/)
|
|
272
|
+
const title = titleMatch?.[1] ?? ""
|
|
273
|
+
return `<details data-macro="expand"><summary>${escapeHtml(title)}</summary>${content}</details>`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return content
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// TOC macro
|
|
280
|
+
if (macroName === "toc") {
|
|
281
|
+
const minMatch = macroContent.match(/<ac:parameter[^>]*ac:name="minLevel"[^>]*>(\d+)/)
|
|
282
|
+
const maxMatch = macroContent.match(/<ac:parameter[^>]*ac:name="maxLevel"[^>]*>(\d+)/)
|
|
283
|
+
return `<nav data-macro="toc" data-min="${minMatch?.[1] ?? ""}" data-max="${maxMatch?.[1] ?? ""}"></nav>`
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Status macro
|
|
287
|
+
if (macroName === "status") {
|
|
288
|
+
const colorMatch = macroContent.match(/<ac:parameter[^>]*ac:name="colour"[^>]*>([^<]+)/)
|
|
289
|
+
const titleMatch = macroContent.match(/<ac:parameter[^>]*ac:name="title"[^>]*>([^<]+)/)
|
|
290
|
+
return `<span data-macro="status" data-color="${colorMatch?.[1] ?? ""}">${escapeHtml(titleMatch?.[1] ?? "")}</span>`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Unknown macro - preserve as unsupported
|
|
294
|
+
return `<div data-unsupported-macro="${macroName}">${macroContent}</div>`
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Preprocess task lists.
|
|
299
|
+
*/
|
|
300
|
+
const preprocessTaskLists = (html: string): string => {
|
|
301
|
+
let result = html
|
|
302
|
+
const taskRegex = /<ac:task>([\s\S]*?)<\/ac:task>/gi
|
|
303
|
+
result = result.replace(taskRegex, (_, taskContent) => {
|
|
304
|
+
const idMatch = taskContent.match(/<ac:task-id>([^<]*)<\/ac:task-id>/)
|
|
305
|
+
const uuidMatch = taskContent.match(/<ac:task-uuid>([^<]*)<\/ac:task-uuid>/)
|
|
306
|
+
const statusMatch = taskContent.match(/<ac:task-status>([^<]*)<\/ac:task-status>/)
|
|
307
|
+
const bodyMatch = taskContent.match(/<ac:task-body>([\s\S]*?)<\/ac:task-body>/)
|
|
308
|
+
|
|
309
|
+
const id = idMatch?.[1] ?? ""
|
|
310
|
+
const uuid = uuidMatch?.[1] ?? ""
|
|
311
|
+
const status = statusMatch?.[1] ?? "incomplete"
|
|
312
|
+
const body = bodyMatch?.[1] ?? ""
|
|
313
|
+
const cleanBody = body.replace(/<[^>]+>/g, "").trim()
|
|
314
|
+
return `<li data-task-id="${id}" data-task-uuid="${uuid}" data-task-status="${status}">${cleanBody}</li>`
|
|
315
|
+
})
|
|
316
|
+
result = result.replace(/<ac:task-list[^>]*>/gi, "<ul data-macro=\"task-list\">")
|
|
317
|
+
result = result.replace(/<\/ac:task-list>/gi, "</ul>")
|
|
318
|
+
return result
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Preprocess images with attachments.
|
|
323
|
+
*/
|
|
324
|
+
const preprocessImages = (html: string): string => {
|
|
325
|
+
return html.replace(
|
|
326
|
+
/<ac:image([^>]*)>[\s\S]*?<ri:attachment([^>]*)\/>[\s\S]*?<\/ac:image>/gi,
|
|
327
|
+
(_, imageAttrs, attachmentAttrs) => {
|
|
328
|
+
const filename = attachmentAttrs.match(/ri:filename="([^"]*)"/)?.[1] ?? ""
|
|
329
|
+
const align = imageAttrs.match(/ac:align="([^"]*)"/)?.[1] ?? ""
|
|
330
|
+
const width = imageAttrs.match(/ac:width="([^"]*)"/)?.[1] ?? ""
|
|
331
|
+
const alt = imageAttrs.match(/ac:alt="([^"]*)"/)?.[1] ?? ""
|
|
332
|
+
const attrs = [
|
|
333
|
+
`data-attachment="${escapeHtml(filename)}"`,
|
|
334
|
+
align && `data-align="${align}"`,
|
|
335
|
+
width && `data-width="${width}"`,
|
|
336
|
+
alt && `alt="${escapeHtml(alt)}"`
|
|
337
|
+
].filter(Boolean).join(" ")
|
|
338
|
+
return `<img ${attrs}>`
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Preprocess emoticons.
|
|
345
|
+
*/
|
|
346
|
+
const preprocessEmoticons = (html: string): string => {
|
|
347
|
+
return html.replace(
|
|
348
|
+
/<ac:emoticon([^>]*)\/?>/gi,
|
|
349
|
+
(_, attrs) => {
|
|
350
|
+
const shortname = attrs.match(/ac:emoji-shortname="([^"]*)"/)?.[1] ?? ""
|
|
351
|
+
const emojiId = attrs.match(/ac:emoji-id="([^"]*)"/)?.[1] ?? ""
|
|
352
|
+
const fallback = attrs.match(/ac:emoji-fallback="([^"]*)"/)?.[1] ?? ""
|
|
353
|
+
return `<span data-emoji="${escapeHtml(shortname)}" data-emoji-id="${emojiId}">${fallback}</span>`
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Preprocess user mentions.
|
|
360
|
+
*/
|
|
361
|
+
const preprocessUserMentions = (html: string): string => {
|
|
362
|
+
return html.replace(
|
|
363
|
+
/<ac:link>\s*<ri:user([^>]*)\/?>\s*<\/ac:link>/gi,
|
|
364
|
+
(_, attrs) => {
|
|
365
|
+
const accountId = attrs.match(/ri:account-id="([^"]*)"/)?.[1] ?? ""
|
|
366
|
+
return `<span data-user-mention="${escapeHtml(accountId)}"></span>`
|
|
367
|
+
}
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Preprocess Confluence links with link-body.
|
|
373
|
+
* <ac:link><ac:link-body>Link text</ac:link-body></ac:link>
|
|
374
|
+
* -> <span data-confluence-link>Link text</span>
|
|
375
|
+
*/
|
|
376
|
+
const preprocessConfluenceLinks = (html: string): string => {
|
|
377
|
+
return html.replace(
|
|
378
|
+
/<ac:link>\s*<ac:link-body>([\s\S]*?)<\/ac:link-body>\s*<\/ac:link>/gi,
|
|
379
|
+
(_, linkText) => {
|
|
380
|
+
return `<span data-confluence-link>${linkText}</span>`
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Preprocess ADF extensions (decision lists).
|
|
387
|
+
*/
|
|
388
|
+
const preprocessAdfExtensions = (html: string): string => {
|
|
389
|
+
return html.replace(
|
|
390
|
+
/<ac:adf-extension>([\s\S]*?)<\/ac:adf-extension>/gi,
|
|
391
|
+
(_, content) => {
|
|
392
|
+
if (content.includes("type=\"decision-list\"")) {
|
|
393
|
+
const items: Array<{ localId: string; state: string; content: string }> = []
|
|
394
|
+
const itemRegex = /<ac:adf-node\s+type="decision-item">([\s\S]*?)<\/ac:adf-node>/gi
|
|
395
|
+
let itemMatch
|
|
396
|
+
while ((itemMatch = itemRegex.exec(content)) !== null) {
|
|
397
|
+
const itemContent = itemMatch[1] ?? ""
|
|
398
|
+
const localIdMatch = itemContent.match(/<ac:adf-attribute\s+key="local-id">([^<]*)<\/ac:adf-attribute>/)
|
|
399
|
+
const stateMatch = itemContent.match(/<ac:adf-attribute\s+key="state">([^<]*)<\/ac:adf-attribute>/)
|
|
400
|
+
const textMatch = itemContent.match(/<ac:adf-content>([^<]*)<\/ac:adf-content>/)
|
|
401
|
+
items.push({
|
|
402
|
+
localId: localIdMatch?.[1] ?? "",
|
|
403
|
+
state: stateMatch?.[1] ?? "UNDECIDED",
|
|
404
|
+
content: textMatch?.[1] ?? ""
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
if (items.length > 0) {
|
|
408
|
+
const encoded = items.map((item) =>
|
|
409
|
+
`${encodeURIComponent(item.localId)};${encodeURIComponent(item.state)};${encodeURIComponent(item.content)}`
|
|
410
|
+
).join("|")
|
|
411
|
+
return `<!--cf:decision:${encoded}-->`
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Check if it's an ADF panel (note, info, warning, etc.)
|
|
415
|
+
if (content.includes("type=\"panel\"")) {
|
|
416
|
+
const panelTypeMatch = content.match(/<ac:adf-attribute\s+key="panel-type">([^<]*)<\/ac:adf-attribute>/)
|
|
417
|
+
const panelType = panelTypeMatch?.[1] ?? "info"
|
|
418
|
+
const contentMatch = content.match(/<ac:adf-content>([\s\S]*?)<\/ac:adf-content>/)
|
|
419
|
+
const innerContent = contentMatch?.[1] ?? ""
|
|
420
|
+
return `<div data-macro="${panelType}" data-title="">${innerContent}</div>`
|
|
421
|
+
}
|
|
422
|
+
const fallbackMatch = content.match(/<ac:adf-fallback>([\s\S]*?)<\/ac:adf-fallback>/)
|
|
423
|
+
if (fallbackMatch) {
|
|
424
|
+
return fallbackMatch[1] ?? ""
|
|
425
|
+
}
|
|
426
|
+
return ""
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Strip remaining ac/ri namespace tags.
|
|
433
|
+
*/
|
|
434
|
+
const stripNamespaces = (html: string): string => {
|
|
435
|
+
return html
|
|
436
|
+
.replace(/<ac:parameter[^>]{0,1000}>[^<]{0,10000}<\/ac:parameter>/gi, "")
|
|
437
|
+
.replace(/<\/?ac:[a-z-]{1,50}[^>]{0,1000}>/gi, "")
|
|
438
|
+
.replace(/<\/?ri:[a-z-]{1,50}[^>]{0,1000}\/?>/gi, "")
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const escapeHtml = (str: string): string =>
|
|
442
|
+
str
|
|
443
|
+
.replace(/&/g, "&")
|
|
444
|
+
.replace(/</g, "<")
|
|
445
|
+
.replace(/>/g, ">")
|
|
446
|
+
.replace(/"/g, """)
|