@knpkv/confluence-to-markdown 0.6.0 → 0.7.0
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 +39 -0
- package/LICENSE +21 -0
- package/README.md +22 -13
- package/dist/AdfPlaceholders.d.ts +42 -0
- package/dist/AdfPlaceholders.d.ts.map +1 -0
- package/dist/AdfPlaceholders.js +547 -0
- package/dist/AdfPlaceholders.js.map +1 -0
- package/dist/AdfSchemaValidator.d.ts +37 -0
- package/dist/AdfSchemaValidator.d.ts.map +1 -0
- package/dist/AdfSchemaValidator.js +37 -0
- package/dist/AdfSchemaValidator.js.map +1 -0
- package/dist/AdfWalker.d.ts +39 -0
- package/dist/AdfWalker.d.ts.map +1 -0
- package/dist/AdfWalker.js +527 -0
- package/dist/AdfWalker.js.map +1 -0
- package/dist/AtlaskitTransformers.d.ts +35 -0
- package/dist/AtlaskitTransformers.d.ts.map +1 -0
- package/dist/AtlaskitTransformers.js +48 -0
- package/dist/AtlaskitTransformers.js.map +1 -0
- package/dist/Brand.d.ts +6 -6
- package/dist/Brand.d.ts.map +1 -1
- package/dist/Brand.js +8 -6
- package/dist/Brand.js.map +1 -1
- package/dist/ConfluenceAuth.d.ts +4 -4
- package/dist/ConfluenceAuth.d.ts.map +1 -1
- package/dist/ConfluenceAuth.js +37 -39
- package/dist/ConfluenceAuth.js.map +1 -1
- package/dist/ConfluenceClient.d.ts +7 -17
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +81 -38
- package/dist/ConfluenceClient.js.map +1 -1
- package/dist/ConfluenceConfig.d.ts +3 -3
- package/dist/ConfluenceConfig.d.ts.map +1 -1
- package/dist/ConfluenceConfig.js +13 -11
- package/dist/ConfluenceConfig.js.map +1 -1
- package/dist/ConfluenceError.d.ts +68 -16
- package/dist/ConfluenceError.d.ts.map +1 -1
- package/dist/ConfluenceError.js +30 -1
- package/dist/ConfluenceError.js.map +1 -1
- package/dist/GitError.d.ts +5 -5
- package/dist/GitService.d.ts +11 -3
- package/dist/GitService.d.ts.map +1 -1
- package/dist/GitService.js +22 -27
- package/dist/GitService.js.map +1 -1
- package/dist/LocalFileSystem.d.ts +3 -3
- package/dist/LocalFileSystem.d.ts.map +1 -1
- package/dist/LocalFileSystem.js +6 -6
- package/dist/LocalFileSystem.js.map +1 -1
- package/dist/MarkdownConverter.d.ts +16 -65
- package/dist/MarkdownConverter.d.ts.map +1 -1
- package/dist/MarkdownConverter.js +64 -85
- package/dist/MarkdownConverter.js.map +1 -1
- package/dist/Schemas.d.ts +128 -141
- package/dist/Schemas.d.ts.map +1 -1
- package/dist/Schemas.js +21 -23
- package/dist/Schemas.js.map +1 -1
- package/dist/SyncEngine.d.ts +8 -5
- package/dist/SyncEngine.d.ts.map +1 -1
- package/dist/SyncEngine.js +189 -113
- package/dist/SyncEngine.js.map +1 -1
- package/dist/bin.js +23 -35
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +2 -14
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +11 -16
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/clone.d.ts +4 -6
- package/dist/commands/clone.d.ts.map +1 -1
- package/dist/commands/clone.js +34 -32
- package/dist/commands/clone.js.map +1 -1
- package/dist/commands/delete.d.ts +2 -10
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +5 -4
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/errorHandler.d.ts +2 -1
- package/dist/commands/errorHandler.d.ts.map +1 -1
- package/dist/commands/errorHandler.js +22 -15
- package/dist/commands/errorHandler.js.map +1 -1
- package/dist/commands/fetch.d.ts +27 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +48 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/git.d.ts +7 -10
- package/dist/commands/git.d.ts.map +1 -1
- package/dist/commands/git.js +6 -6
- package/dist/commands/git.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/layers.d.ts +10 -9
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +41 -30
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/new.d.ts +2 -6
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +5 -4
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/pageInput.d.ts +19 -0
- package/dist/commands/pageInput.d.ts.map +1 -0
- package/dist/commands/pageInput.js +68 -0
- package/dist/commands/pageInput.js.map +1 -0
- package/dist/commands/root.d.ts +8 -0
- package/dist/commands/root.d.ts.map +1 -0
- package/dist/commands/root.js +29 -0
- package/dist/commands/root.js.map +1 -0
- package/dist/commands/sync.d.ts +6 -9
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -6
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -13
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.d.ts.map +1 -1
- package/dist/internal/NodeLayers.js +1 -2
- package/dist/internal/NodeLayers.js.map +1 -1
- package/dist/internal/adfMetadata.d.ts +30 -0
- package/dist/internal/adfMetadata.d.ts.map +1 -0
- package/dist/internal/adfMetadata.js +126 -0
- package/dist/internal/adfMetadata.js.map +1 -0
- package/dist/internal/cleanMarkdown.d.ts +5 -0
- package/dist/internal/cleanMarkdown.d.ts.map +1 -0
- package/dist/internal/cleanMarkdown.js +13 -0
- package/dist/internal/cleanMarkdown.js.map +1 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +41 -8
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/gitCommands.d.ts +9 -3
- package/dist/internal/gitCommands.d.ts.map +1 -1
- package/dist/internal/gitCommands.js +18 -9
- package/dist/internal/gitCommands.js.map +1 -1
- package/dist/internal/hashUtils.d.ts +1 -1
- package/dist/internal/hashUtils.d.ts.map +1 -1
- package/dist/internal/hashUtils.js +1 -1
- package/dist/internal/hashUtils.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +10 -5
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +19 -40
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/pathUtils.d.ts +1 -1
- package/dist/internal/pathUtils.d.ts.map +1 -1
- package/dist/internal/pathUtils.js +1 -1
- package/dist/internal/pathUtils.js.map +1 -1
- package/dist/internal/process.d.ts +15 -0
- package/dist/internal/process.d.ts.map +1 -0
- package/dist/internal/process.js +10 -0
- package/dist/internal/process.js.map +1 -0
- package/dist/internal/stdio.d.ts +6 -0
- package/dist/internal/stdio.d.ts.map +1 -0
- package/dist/internal/stdio.js +15 -0
- package/dist/internal/stdio.js.map +1 -0
- package/dist/internal/tokenStorage.d.ts +3 -13
- package/dist/internal/tokenStorage.d.ts.map +1 -1
- package/dist/internal/tokenStorage.js +26 -24
- package/dist/internal/tokenStorage.js.map +1 -1
- package/dist/internal/userCache.d.ts +1 -1
- package/dist/internal/userCache.d.ts.map +1 -1
- package/dist/internal/userCache.js +1 -1
- package/dist/internal/userCache.js.map +1 -1
- package/package.json +30 -30
- package/skills/confluence/SKILL.md +143 -0
- package/skills/confluence/agents/openai.yaml +4 -0
- package/src/AdfPlaceholders.ts +310 -13
- package/src/AdfSchemaValidator.ts +2 -4
- package/src/AdfWalker.ts +122 -42
- package/src/AtlaskitTransformers.ts +2 -4
- package/src/Brand.ts +11 -16
- package/src/ConfluenceAuth.ts +22 -30
- package/src/ConfluenceClient.ts +24 -20
- package/src/ConfluenceConfig.ts +14 -14
- package/src/GitService.ts +39 -49
- package/src/LocalFileSystem.ts +7 -9
- package/src/MarkdownConverter.ts +2 -4
- package/src/Schemas.ts +13 -12
- package/src/SyncEngine.ts +151 -53
- package/src/bin.ts +30 -56
- package/src/commands/auth.ts +21 -18
- package/src/commands/clone.ts +38 -37
- package/src/commands/delete.ts +5 -4
- package/src/commands/errorHandler.ts +25 -18
- package/src/commands/fetch.ts +90 -0
- package/src/commands/git.ts +6 -6
- package/src/commands/index.ts +1 -0
- package/src/commands/layers.ts +53 -33
- package/src/commands/new.ts +5 -4
- package/src/commands/pageInput.ts +103 -0
- package/src/commands/root.ts +59 -0
- package/src/commands/sync.ts +7 -6
- package/src/internal/NodeLayers.ts +1 -2
- package/src/internal/adfMetadata.ts +145 -0
- package/src/internal/cleanMarkdown.ts +15 -0
- package/src/internal/frontmatter.ts +45 -8
- package/src/internal/gitCommands.ts +23 -17
- package/src/internal/hashUtils.ts +2 -2
- package/src/internal/oauthServer.ts +84 -105
- package/src/internal/pathUtils.ts +1 -1
- package/src/internal/process.ts +15 -0
- package/src/internal/stdio.ts +22 -0
- package/src/internal/tokenStorage.ts +39 -29
- package/src/internal/userCache.ts +2 -2
- package/test/AdfPlaceholders.test.ts +213 -0
- package/test/AdfSchemaValidator.test.ts +6 -6
- package/test/AdfWalker.test.ts +167 -21
- package/test/AtlaskitTransformers.test.ts +4 -4
- package/test/Brand.test.ts +11 -11
- package/test/GitService.test.ts +6 -2
- package/test/MarkdownConverter.test.ts +12 -11
- package/test/RoundTrip.test.ts +258 -3
- package/test/Schemas.test.ts +40 -40
- package/test/adfMetadata.test.ts +110 -0
- package/test/cleanMarkdown.test.ts +36 -0
- package/test/commandHarness.test.ts +79 -0
- package/test/commandHarness.ts +147 -0
- package/test/fetch.test.ts +61 -0
- package/test/frontmatter.test.ts +41 -0
- package/test/integration.test.ts +569 -156
- package/test/layers.test.ts +12 -0
- package/test/oauthServer.test.ts +4 -5
- package/test/pageInput.test.ts +83 -0
- package/test/tokenStorage.test.ts +17 -17
- package/dist/SchemaConverterError.d.ts +0 -108
- package/dist/SchemaConverterError.d.ts.map +0 -1
- package/dist/SchemaConverterError.js +0 -84
- package/dist/SchemaConverterError.js.map +0 -1
- package/dist/ast/BlockNode.d.ts +0 -468
- package/dist/ast/BlockNode.d.ts.map +0 -1
- package/dist/ast/BlockNode.js +0 -319
- package/dist/ast/BlockNode.js.map +0 -1
- package/dist/ast/Document.d.ts +0 -244
- package/dist/ast/Document.d.ts.map +0 -1
- package/dist/ast/Document.js +0 -69
- package/dist/ast/Document.js.map +0 -1
- package/dist/ast/InlineNode.d.ts +0 -477
- package/dist/ast/InlineNode.d.ts.map +0 -1
- package/dist/ast/InlineNode.js +0 -263
- package/dist/ast/InlineNode.js.map +0 -1
- package/dist/ast/MacroNode.d.ts +0 -267
- package/dist/ast/MacroNode.d.ts.map +0 -1
- package/dist/ast/MacroNode.js +0 -164
- package/dist/ast/MacroNode.js.map +0 -1
- package/dist/ast/index.d.ts +0 -10
- package/dist/ast/index.d.ts.map +0 -1
- package/dist/ast/index.js +0 -14
- package/dist/ast/index.js.map +0 -1
- package/dist/parsers/ConfluenceParser.d.ts +0 -26
- package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
- package/dist/parsers/ConfluenceParser.js +0 -792
- package/dist/parsers/ConfluenceParser.js.map +0 -1
- package/dist/parsers/MarkdownParser.d.ts +0 -26
- package/dist/parsers/MarkdownParser.d.ts.map +0 -1
- package/dist/parsers/MarkdownParser.js +0 -873
- package/dist/parsers/MarkdownParser.js.map +0 -1
- package/dist/parsers/index.d.ts +0 -8
- package/dist/parsers/index.d.ts.map +0 -1
- package/dist/parsers/index.js +0 -8
- package/dist/parsers/index.js.map +0 -1
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +0 -23
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +0 -1
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js +0 -323
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +0 -1
- package/dist/parsers/preprocessing/index.d.ts +0 -7
- package/dist/parsers/preprocessing/index.d.ts.map +0 -1
- package/dist/parsers/preprocessing/index.js +0 -7
- package/dist/parsers/preprocessing/index.js.map +0 -1
- package/dist/schemas/ConfluenceSchema.d.ts +0 -21
- package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
- package/dist/schemas/ConfluenceSchema.js +0 -38
- package/dist/schemas/ConfluenceSchema.js.map +0 -1
- package/dist/schemas/ConversionSchema.d.ts +0 -35
- package/dist/schemas/ConversionSchema.d.ts.map +0 -1
- package/dist/schemas/ConversionSchema.js +0 -208
- package/dist/schemas/ConversionSchema.js.map +0 -1
- package/dist/schemas/MarkdownSchema.d.ts +0 -21
- package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
- package/dist/schemas/MarkdownSchema.js +0 -38
- package/dist/schemas/MarkdownSchema.js.map +0 -1
- package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
- package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
- package/dist/schemas/hast/HastFromHtml.js +0 -107
- package/dist/schemas/hast/HastFromHtml.js.map +0 -1
- package/dist/schemas/hast/HastSchema.d.ts +0 -195
- package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
- package/dist/schemas/hast/HastSchema.js +0 -183
- package/dist/schemas/hast/HastSchema.js.map +0 -1
- package/dist/schemas/hast/index.d.ts +0 -9
- package/dist/schemas/hast/index.d.ts.map +0 -1
- package/dist/schemas/hast/index.js +0 -3
- package/dist/schemas/hast/index.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -14
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -16
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
- package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
- package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
- package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
- package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
- package/dist/schemas/mdast/MdastSchema.js +0 -266
- package/dist/schemas/mdast/MdastSchema.js.map +0 -1
- package/dist/schemas/mdast/index.d.ts +0 -10
- package/dist/schemas/mdast/index.d.ts.map +0 -1
- package/dist/schemas/mdast/index.js +0 -4
- package/dist/schemas/mdast/index.js.map +0 -1
- package/dist/schemas/mdast/mdastToString.d.ts +0 -13
- package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
- package/dist/schemas/mdast/mdastToString.js +0 -85
- package/dist/schemas/mdast/mdastToString.js.map +0 -1
- package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
- package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/block/BlockSchema.js +0 -634
- package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
- package/dist/schemas/nodes/block/index.d.ts +0 -7
- package/dist/schemas/nodes/block/index.d.ts.map +0 -1
- package/dist/schemas/nodes/block/index.js +0 -7
- package/dist/schemas/nodes/block/index.js.map +0 -1
- package/dist/schemas/nodes/index.d.ts +0 -9
- package/dist/schemas/nodes/index.d.ts.map +0 -1
- package/dist/schemas/nodes/index.js +0 -12
- package/dist/schemas/nodes/index.js.map +0 -1
- package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
- package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
- package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
- package/dist/schemas/nodes/inline/index.d.ts +0 -7
- package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
- package/dist/schemas/nodes/inline/index.js +0 -7
- package/dist/schemas/nodes/inline/index.js.map +0 -1
- package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
- package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
- package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
- package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
- package/dist/schemas/nodes/macro/index.d.ts +0 -7
- package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
- package/dist/schemas/nodes/macro/index.js +0 -7
- package/dist/schemas/nodes/macro/index.js.map +0 -1
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -53
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -349
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
- package/dist/schemas/preprocessing/index.d.ts +0 -8
- package/dist/schemas/preprocessing/index.d.ts.map +0 -1
- package/dist/schemas/preprocessing/index.js +0 -2
- package/dist/schemas/preprocessing/index.js.map +0 -1
- package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
- package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
- package/dist/serializers/ConfluenceSerializer.js +0 -551
- package/dist/serializers/ConfluenceSerializer.js.map +0 -1
- package/dist/serializers/MarkdownSerializer.d.ts +0 -34
- package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
- package/dist/serializers/MarkdownSerializer.js +0 -355
- package/dist/serializers/MarkdownSerializer.js.map +0 -1
- package/dist/serializers/index.d.ts +0 -8
- package/dist/serializers/index.d.ts.map +0 -1
- package/dist/serializers/index.js +0 -8
- package/dist/serializers/index.js.map +0 -1
package/src/AdfPlaceholders.ts
CHANGED
|
@@ -15,16 +15,30 @@
|
|
|
15
15
|
* the whole paragraph is just this comment; `attrs` is base64 JSON of
|
|
16
16
|
* the node's full attrs — parameters, localId, layout — and wins over
|
|
17
17
|
* the readable key/type parts; key/type-only is the legacy form)
|
|
18
|
+
* - `<!-- adf:paragraph marks=BASE64 --> BODY <!-- adf:/paragraph -->`
|
|
19
|
+
* (the body paragraph regains its paragraph-level marks)
|
|
18
20
|
* - `<!-- adf:bodiedExtension … --> BODY <!-- adf:/bodiedExtension -->`
|
|
19
21
|
* (the sibling blocks between the markers become the extension's body)
|
|
22
|
+
* - `<!-- adf:inlineCard attrs=BASE64 -->` (inline)
|
|
20
23
|
* - `<!-- adf:inlineExtension key=KEY type=TYPE attrs=BASE64 -->` (inline)
|
|
24
|
+
* - `<!-- adf:date node=BASE64 -->` and `<!-- adf:emoji node=BASE64 -->`
|
|
25
|
+
* (inline)
|
|
26
|
+
* - `<!-- adf:panel type=TYPE attrs=BASE64 --> BODY <!-- adf:/panel -->`
|
|
27
|
+
* (the sibling blocks between the markers become the panel's body)
|
|
28
|
+
* - `<!-- adf:TYPE node=BASE64 --> BODY <!-- adf:/TYPE -->` for selected
|
|
29
|
+
* native block nodes such as task/decision lists, expands, layouts,
|
|
30
|
+
* cards, and tables
|
|
31
|
+
* - `<u>TEXT</u>`, `<sub>TEXT</sub>`, `<sup>TEXT</sup>`, and exact styled
|
|
32
|
+
* spans emitted for Confluence-only inline marks
|
|
21
33
|
* - `[@Name](confluence-mention://ACCOUNT_ID)` (link mark with a
|
|
22
34
|
* custom scheme — the only way to round-trip mention accountIds)
|
|
35
|
+
* - `[[toc]]`, `[[toc:min=1,max=3]]` (block-level native syntax for the
|
|
36
|
+
* Confluence Table of Contents macro)
|
|
23
37
|
*
|
|
24
38
|
* @module
|
|
25
39
|
*/
|
|
26
40
|
|
|
27
|
-
import * as
|
|
41
|
+
import * as Option from "effect/Option"
|
|
28
42
|
import * as Schema from "effect/Schema"
|
|
29
43
|
|
|
30
44
|
interface AdfNode {
|
|
@@ -36,17 +50,50 @@ interface AdfNode {
|
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
const STATUS_RE = /<span class="adf-status"\s+data-color="([^"]+)">([^<]*)<\/span>/g
|
|
53
|
+
const INLINE_NODE_RE = /<!--\s*adf:(date|emoji)(?:\s+node=([\s\S]*?))?\s*-->/g
|
|
54
|
+
const INLINE_CARD_RE = /<!--\s*adf:inlineCard(?:\s+attrs=([\s\S]*?))?\s*-->/g
|
|
39
55
|
const INLINE_EXTENSION_RE =
|
|
40
|
-
/<!--\s*adf:inlineExtension(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([
|
|
41
|
-
const
|
|
56
|
+
/<!--\s*adf:inlineExtension(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->/g
|
|
57
|
+
const UNDERLINE_RE = /<u>([^<]*)<\/u>/g
|
|
58
|
+
const SUBSCRIPT_RE = /<sub>([^<]*)<\/sub>/g
|
|
59
|
+
const SUPERSCRIPT_RE = /<sup>([^<]*)<\/sup>/g
|
|
60
|
+
const TEXT_COLOR_RE = /<span style="color:([^"<>]+)">([^<]*)<\/span>/g
|
|
61
|
+
const BACKGROUND_COLOR_RE = /<span style="background-color:([^"<>]+)">([^<]*)<\/span>/g
|
|
62
|
+
const COMBINED_INLINE_RE = new RegExp(
|
|
63
|
+
[
|
|
64
|
+
INLINE_NODE_RE.source,
|
|
65
|
+
STATUS_RE.source,
|
|
66
|
+
INLINE_CARD_RE.source,
|
|
67
|
+
INLINE_EXTENSION_RE.source,
|
|
68
|
+
UNDERLINE_RE.source,
|
|
69
|
+
SUBSCRIPT_RE.source,
|
|
70
|
+
SUPERSCRIPT_RE.source,
|
|
71
|
+
TEXT_COLOR_RE.source,
|
|
72
|
+
BACKGROUND_COLOR_RE.source
|
|
73
|
+
].join("|"),
|
|
74
|
+
"g"
|
|
75
|
+
)
|
|
42
76
|
|
|
43
77
|
const BLOCK_EXTENSION_RE =
|
|
44
|
-
/^\s*<!--\s*adf:(extension|bodiedExtension)(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([
|
|
78
|
+
/^\s*<!--\s*adf:(extension|bodiedExtension)(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->\s*$/
|
|
45
79
|
const BODIED_EXTENSION_END_RE = /^\s*<!--\s*adf:\/bodiedExtension\s*-->\s*$/
|
|
80
|
+
const PANEL_RE = /^\s*<!--\s*adf:panel(?:\s+type=(\S+?))?(?:\s+attrs=([\s\S]*?))?\s*-->\s*$/
|
|
81
|
+
const PANEL_END_RE = /^\s*<!--\s*adf:\/panel\s*-->\s*$/
|
|
82
|
+
const ENCODED_BLOCK_NODE_RE =
|
|
83
|
+
/^\s*<!--\s*adf:(taskList|decisionList|expand|nestedExpand|table|layoutSection|blockCard|embedCard)(?:\s+node=([\s\S]*?))?\s*-->\s*$/
|
|
84
|
+
const ENCODED_BLOCK_NODE_END_RE =
|
|
85
|
+
/^\s*<!--\s*adf:\/(taskList|decisionList|expand|nestedExpand|table|layoutSection|blockCard|embedCard)\s*-->\s*$/
|
|
86
|
+
const PARAGRAPH_MARKS_RE = /^\s*<!--\s*adf:paragraph(?:\s+marks=([\s\S]*?))?\s*-->\s*$/
|
|
87
|
+
const PARAGRAPH_MARKS_END_RE = /^\s*<!--\s*adf:\/paragraph\s*-->\s*$/
|
|
88
|
+
const TOC_RE = /^\s*\[\[toc(?::([^\]]+))?\]\]\s*$/
|
|
89
|
+
const CONFLUENCE_CORE_MACRO_TYPE = "com.atlassian.confluence.macro.core"
|
|
46
90
|
|
|
47
91
|
const textNode = (text: string, marks: ReadonlyArray<AdfNode> | undefined): AdfNode =>
|
|
48
92
|
marks && marks.length > 0 ? { type: "text", text, marks } : { type: "text", text }
|
|
49
93
|
|
|
94
|
+
const addMark = (marks: ReadonlyArray<AdfNode> | undefined, mark: AdfNode): ReadonlyArray<AdfNode> =>
|
|
95
|
+
marks && marks.length > 0 ? [...marks, mark] : [mark]
|
|
96
|
+
|
|
50
97
|
// Code-marked text is a *quotation* of placeholder syntax, not a placeholder
|
|
51
98
|
// (the walker never emits placeholders with a code mark) — expanding it would
|
|
52
99
|
// corrupt documentation that demonstrates the syntax.
|
|
@@ -65,16 +112,18 @@ const fromBase64 = (b64: string): string => {
|
|
|
65
112
|
}
|
|
66
113
|
|
|
67
114
|
// JSON string → free-form attrs record; rejects null/arrays/primitives.
|
|
68
|
-
const AttrsBlob = Schema.
|
|
69
|
-
const decodeAttrsBlob = Schema.
|
|
115
|
+
const AttrsBlob = Schema.Record(Schema.String, Schema.Unknown)
|
|
116
|
+
const decodeAttrsBlob = Schema.decodeUnknownOption(AttrsBlob)
|
|
70
117
|
|
|
71
118
|
const decodeAttrs = (b64: string | undefined): Record<string, unknown> | null => {
|
|
72
119
|
if (!b64) return null
|
|
73
120
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
121
|
+
const raw = b64.trim()
|
|
122
|
+
const parsed = JSON.parse(raw.startsWith("{") ? raw : fromBase64(raw)) as unknown
|
|
123
|
+
const decoded = decodeAttrsBlob(parsed)
|
|
124
|
+
return Option.isSome(decoded) ? decoded.value : null
|
|
76
125
|
} catch {
|
|
77
|
-
// Invalid base64 (hand-edited file?) — fall back to the readable key/type.
|
|
126
|
+
// Invalid JSON/base64 (hand-edited file?) — fall back to the readable key/type.
|
|
78
127
|
return null
|
|
79
128
|
}
|
|
80
129
|
}
|
|
@@ -92,6 +141,43 @@ const buildExtensionAttrs = (
|
|
|
92
141
|
return attrs
|
|
93
142
|
}
|
|
94
143
|
|
|
144
|
+
const buildPanelAttrs = (type: string | undefined, attrsB64: string | undefined): Record<string, unknown> => {
|
|
145
|
+
const decoded = decodeAttrs(attrsB64)
|
|
146
|
+
if (decoded) return decoded
|
|
147
|
+
return type ? { panelType: type } : {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const buildInlineCardAttrs = (attrsB64: string | undefined): Record<string, unknown> => decodeAttrs(attrsB64) ?? {}
|
|
151
|
+
|
|
152
|
+
const decodeMarks = (b64: string | undefined): ReadonlyArray<AdfNode> => {
|
|
153
|
+
if (!b64) return []
|
|
154
|
+
try {
|
|
155
|
+
const raw = b64.trim()
|
|
156
|
+
const parsed = JSON.parse(raw.startsWith("[") ? raw : fromBase64(raw)) as unknown
|
|
157
|
+
return Array.isArray(parsed)
|
|
158
|
+
? parsed.filter((mark): mark is AdfNode =>
|
|
159
|
+
mark !== null && typeof mark === "object" && typeof (mark as Record<string, unknown>)["type"] === "string"
|
|
160
|
+
)
|
|
161
|
+
: []
|
|
162
|
+
} catch {
|
|
163
|
+
return []
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const decodeNode = (b64: string | undefined): AdfNode | null => {
|
|
168
|
+
if (!b64) return null
|
|
169
|
+
try {
|
|
170
|
+
const raw = b64.trim()
|
|
171
|
+
const parsed = JSON.parse(raw.startsWith("{") ? raw : fromBase64(raw)) as unknown
|
|
172
|
+
return parsed !== null && typeof parsed === "object" &&
|
|
173
|
+
typeof (parsed as Record<string, unknown>)["type"] === "string"
|
|
174
|
+
? parsed as AdfNode
|
|
175
|
+
: null
|
|
176
|
+
} catch {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
95
181
|
/** Split a text node into a sequence of text + status + inlineExtension nodes. */
|
|
96
182
|
const expandInlineText = (
|
|
97
183
|
text: string,
|
|
@@ -107,11 +193,30 @@ const expandInlineText = (
|
|
|
107
193
|
if (match.index > lastIndex) {
|
|
108
194
|
out.push(textNode(text.slice(lastIndex, match.index), marks))
|
|
109
195
|
}
|
|
110
|
-
//
|
|
196
|
+
// Capture groups follow COMBINED_INLINE_RE order.
|
|
197
|
+
// InlineNode: 1=type, 2=node. Status: 3=color, 4=text.
|
|
198
|
+
// InlineCard: 5=attrs. InlineExtension: 6=key, 7=type, 8=attrs.
|
|
199
|
+
// Underline: 9=text. Sub: 10=text. Sup: 11=text.
|
|
200
|
+
// Text color: 12=color, 13=text. Background color: 14=color, 15=text.
|
|
111
201
|
if (match[1] !== undefined) {
|
|
112
|
-
|
|
202
|
+
const decoded = decodeNode(match[2])
|
|
203
|
+
if (decoded && decoded.type === match[1]) out.push(decoded)
|
|
204
|
+
} else if (match[3] !== undefined) {
|
|
205
|
+
out.push({ type: "status", attrs: { text: match[4] ?? "", color: match[3] } })
|
|
206
|
+
} else if (match[5] !== undefined) {
|
|
207
|
+
out.push({ type: "inlineCard", attrs: buildInlineCardAttrs(match[5]) })
|
|
208
|
+
} else if (match[9] !== undefined) {
|
|
209
|
+
out.push(textNode(match[9], addMark(marks, { type: "underline" })))
|
|
210
|
+
} else if (match[10] !== undefined) {
|
|
211
|
+
out.push(textNode(match[10], addMark(marks, { type: "subsup", attrs: { type: "sub" } })))
|
|
212
|
+
} else if (match[11] !== undefined) {
|
|
213
|
+
out.push(textNode(match[11], addMark(marks, { type: "subsup", attrs: { type: "sup" } })))
|
|
214
|
+
} else if (match[12] !== undefined) {
|
|
215
|
+
out.push(textNode(match[13] ?? "", addMark(marks, { type: "textColor", attrs: { color: match[12] } })))
|
|
216
|
+
} else if (match[14] !== undefined) {
|
|
217
|
+
out.push(textNode(match[15] ?? "", addMark(marks, { type: "backgroundColor", attrs: { color: match[14] } })))
|
|
113
218
|
} else {
|
|
114
|
-
out.push({ type: "inlineExtension", attrs: buildExtensionAttrs(match[
|
|
219
|
+
out.push({ type: "inlineExtension", attrs: buildExtensionAttrs(match[6], match[7], match[8]) })
|
|
115
220
|
}
|
|
116
221
|
lastIndex = match.index + match[0].length
|
|
117
222
|
}
|
|
@@ -173,11 +278,96 @@ const parseBlockExtensionParagraph = (node: AdfNode): BlockExtensionMarker | nul
|
|
|
173
278
|
}
|
|
174
279
|
}
|
|
175
280
|
|
|
281
|
+
const parseTocParagraph = (node: AdfNode): AdfNode | null => {
|
|
282
|
+
const child = soleTextChild(node)
|
|
283
|
+
if (!child || !child.text) return null
|
|
284
|
+
const match = TOC_RE.exec(child.text)
|
|
285
|
+
if (!match) return null
|
|
286
|
+
|
|
287
|
+
let minLevel: string | undefined
|
|
288
|
+
let maxLevel: string | undefined
|
|
289
|
+
const params = match[1]?.trim()
|
|
290
|
+
|
|
291
|
+
if (params && params.length > 0) {
|
|
292
|
+
const seen = new Set<string>()
|
|
293
|
+
for (const part of params.split(",")) {
|
|
294
|
+
const [rawKey, rawValue, ...rest] = part.split("=")
|
|
295
|
+
if (rest.length > 0) return null
|
|
296
|
+
const key = rawKey?.trim()
|
|
297
|
+
const value = rawValue?.trim()
|
|
298
|
+
if ((key !== "min" && key !== "max") || !value || !/^[1-6]$/.test(value) || seen.has(key)) return null
|
|
299
|
+
seen.add(key)
|
|
300
|
+
if (key === "min") minLevel = value
|
|
301
|
+
else maxLevel = value
|
|
302
|
+
}
|
|
303
|
+
} else if (params !== undefined) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const macroParams: Record<string, { readonly value: string }> = {}
|
|
308
|
+
if (minLevel) macroParams.minLevel = { value: minLevel }
|
|
309
|
+
if (maxLevel) macroParams.maxLevel = { value: maxLevel }
|
|
310
|
+
|
|
311
|
+
const attrs: Record<string, unknown> = {
|
|
312
|
+
extensionKey: "toc",
|
|
313
|
+
extensionType: CONFLUENCE_CORE_MACRO_TYPE
|
|
314
|
+
}
|
|
315
|
+
if (Object.keys(macroParams).length > 0) {
|
|
316
|
+
attrs.parameters = { macroParams }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { type: "extension", attrs }
|
|
320
|
+
}
|
|
321
|
+
|
|
176
322
|
const isBodiedExtensionEnd = (node: AdfNode): boolean => {
|
|
177
323
|
const child = soleTextChild(node)
|
|
178
324
|
return child !== null && typeof child.text === "string" && BODIED_EXTENSION_END_RE.test(child.text)
|
|
179
325
|
}
|
|
180
326
|
|
|
327
|
+
const parsePanelParagraph = (node: AdfNode): Record<string, unknown> | null => {
|
|
328
|
+
const child = soleTextChild(node)
|
|
329
|
+
if (!child || !child.text) return null
|
|
330
|
+
const match = PANEL_RE.exec(child.text)
|
|
331
|
+
if (!match) return null
|
|
332
|
+
const [, type, attrsB64] = match
|
|
333
|
+
return buildPanelAttrs(type, attrsB64)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const isPanelEnd = (node: AdfNode): boolean => {
|
|
337
|
+
const child = soleTextChild(node)
|
|
338
|
+
return child !== null && typeof child.text === "string" && PANEL_END_RE.test(child.text)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const parseEncodedBlockNodeParagraph = (node: AdfNode): { readonly type: string; readonly node: AdfNode } | null => {
|
|
342
|
+
const child = soleTextChild(node)
|
|
343
|
+
if (!child || !child.text) return null
|
|
344
|
+
const match = ENCODED_BLOCK_NODE_RE.exec(child.text)
|
|
345
|
+
if (!match) return null
|
|
346
|
+
const decoded = decodeNode(match[2])
|
|
347
|
+
if (!decoded || decoded.type !== match[1]) return null
|
|
348
|
+
return { type: match[1]!, node: decoded }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const isEncodedBlockNodeEnd = (node: AdfNode, type: string): boolean => {
|
|
352
|
+
const child = soleTextChild(node)
|
|
353
|
+
if (child === null || typeof child.text !== "string") return false
|
|
354
|
+
const match = ENCODED_BLOCK_NODE_END_RE.exec(child.text)
|
|
355
|
+
return match?.[1] === type
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const parseParagraphMarksParagraph = (node: AdfNode): ReadonlyArray<AdfNode> | null => {
|
|
359
|
+
const child = soleTextChild(node)
|
|
360
|
+
if (!child || !child.text) return null
|
|
361
|
+
const match = PARAGRAPH_MARKS_RE.exec(child.text)
|
|
362
|
+
if (!match) return null
|
|
363
|
+
return decodeMarks(match[1])
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const isParagraphMarksEnd = (node: AdfNode): boolean => {
|
|
367
|
+
const child = soleTextChild(node)
|
|
368
|
+
return child !== null && typeof child.text === "string" && PARAGRAPH_MARKS_END_RE.test(child.text)
|
|
369
|
+
}
|
|
370
|
+
|
|
181
371
|
/**
|
|
182
372
|
* Replace block-extension marker paragraphs among `children`. A bare
|
|
183
373
|
* `extension` marker becomes an extension node; a `bodiedExtension` marker
|
|
@@ -237,6 +427,110 @@ const groupBlockExtensions = (children: ReadonlyArray<AdfNode>, parentType: stri
|
|
|
237
427
|
return out
|
|
238
428
|
}
|
|
239
429
|
|
|
430
|
+
const groupPanels = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
|
|
431
|
+
const out: Array<AdfNode> = []
|
|
432
|
+
for (let i = 0; i < children.length; i++) {
|
|
433
|
+
const child = children[i]
|
|
434
|
+
if (!child) continue
|
|
435
|
+
const attrs = parsePanelParagraph(child)
|
|
436
|
+
if (!attrs) {
|
|
437
|
+
if (!isPanelEnd(child)) out.push(child)
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let end = -1
|
|
442
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
443
|
+
if (isPanelEnd(children[j]!)) {
|
|
444
|
+
end = j
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
if (parsePanelParagraph(children[j]!)) break
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (end === -1) {
|
|
451
|
+
out.push({ type: "panel", attrs, content: [{ type: "paragraph", content: [] }] })
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const body = groupPanels(children.slice(i + 1, end))
|
|
456
|
+
out.push({
|
|
457
|
+
type: "panel",
|
|
458
|
+
attrs,
|
|
459
|
+
content: body.length > 0 ? body : [{ type: "paragraph", content: [] }]
|
|
460
|
+
})
|
|
461
|
+
i = end
|
|
462
|
+
}
|
|
463
|
+
return out
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const groupEncodedBlockNodes = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
|
|
467
|
+
const out: Array<AdfNode> = []
|
|
468
|
+
for (let i = 0; i < children.length; i++) {
|
|
469
|
+
const child = children[i]
|
|
470
|
+
if (!child) continue
|
|
471
|
+
const marker = parseEncodedBlockNodeParagraph(child)
|
|
472
|
+
if (!marker) {
|
|
473
|
+
out.push(child)
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let end = -1
|
|
478
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
479
|
+
if (isEncodedBlockNodeEnd(children[j]!, marker.type)) {
|
|
480
|
+
end = j
|
|
481
|
+
break
|
|
482
|
+
}
|
|
483
|
+
if (parseEncodedBlockNodeParagraph(children[j]!) !== null) break
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
out.push(marker.node)
|
|
487
|
+
if (end !== -1) i = end
|
|
488
|
+
}
|
|
489
|
+
return out
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const groupMarkedParagraphs = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> => {
|
|
493
|
+
const out: Array<AdfNode> = []
|
|
494
|
+
for (let i = 0; i < children.length; i++) {
|
|
495
|
+
const child = children[i]
|
|
496
|
+
if (!child) continue
|
|
497
|
+
const marks = parseParagraphMarksParagraph(child)
|
|
498
|
+
if (!marks) {
|
|
499
|
+
if (!isParagraphMarksEnd(child)) out.push(child)
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let end = -1
|
|
504
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
505
|
+
if (isParagraphMarksEnd(children[j]!)) {
|
|
506
|
+
end = j
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
if (parseParagraphMarksParagraph(children[j]!) !== null) break
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (end === -1) {
|
|
513
|
+
out.push({ type: "paragraph", marks, content: [] })
|
|
514
|
+
continue
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const body = children.slice(i + 1, end)
|
|
518
|
+
const first = body[0]
|
|
519
|
+
if (first?.type === "paragraph") {
|
|
520
|
+
out.push(marks.length > 0 ? { ...first, marks } : first)
|
|
521
|
+
for (const rest of body.slice(1)) out.push(rest)
|
|
522
|
+
} else {
|
|
523
|
+
out.push({ type: "paragraph", marks, content: [] })
|
|
524
|
+
for (const rest of body) out.push(rest)
|
|
525
|
+
}
|
|
526
|
+
i = end
|
|
527
|
+
}
|
|
528
|
+
return out
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const groupNativeMacros = (children: ReadonlyArray<AdfNode>): ReadonlyArray<AdfNode> =>
|
|
532
|
+
children.map((child) => parseTocParagraph(child) ?? child)
|
|
533
|
+
|
|
240
534
|
const transform = (node: AdfNode): AdfNode => {
|
|
241
535
|
// ADF codeBlock permits only plain text children — expanding placeholder-
|
|
242
536
|
// looking text inside one would inject schema-invalid nodes and corrupt
|
|
@@ -259,7 +553,10 @@ const transform = (node: AdfNode): AdfNode => {
|
|
|
259
553
|
newContent.push(transform(child))
|
|
260
554
|
}
|
|
261
555
|
}
|
|
262
|
-
|
|
556
|
+
const nativeMacrosRestored = groupNativeMacros(newContent)
|
|
557
|
+
const paragraphsRestored = groupMarkedParagraphs(nativeMacrosRestored)
|
|
558
|
+
const encodedBlocksRestored = groupEncodedBlockNodes(paragraphsRestored)
|
|
559
|
+
return { ...node, content: groupPanels(groupBlockExtensions(encodedBlocksRestored, node.type)) }
|
|
263
560
|
}
|
|
264
561
|
|
|
265
562
|
/** Walk the document tree and rewrite placeholder text into proper ADF nodes. */
|
|
@@ -27,9 +27,7 @@ const validate = ajv.compile(adfJsonSchema as object)
|
|
|
27
27
|
*
|
|
28
28
|
* @category Service
|
|
29
29
|
*/
|
|
30
|
-
export class AdfSchemaValidator extends Context.
|
|
31
|
-
"@knpkv/confluence-to-markdown/AdfSchemaValidator"
|
|
32
|
-
)<
|
|
30
|
+
export class AdfSchemaValidator extends Context.Service<
|
|
33
31
|
AdfSchemaValidator,
|
|
34
32
|
{
|
|
35
33
|
readonly check: (
|
|
@@ -37,7 +35,7 @@ export class AdfSchemaValidator extends Context.Tag(
|
|
|
37
35
|
direction: "incoming" | "outgoing"
|
|
38
36
|
) => Effect.Effect<DocNode, AdfSchemaError>
|
|
39
37
|
}
|
|
40
|
-
>() {}
|
|
38
|
+
>()("@knpkv/confluence-to-markdown/AdfSchemaValidator") {}
|
|
41
39
|
|
|
42
40
|
/**
|
|
43
41
|
* Live Layer for `AdfSchemaValidator`. The Ajv validator is compiled once at
|
package/src/AdfWalker.ts
CHANGED
|
@@ -93,31 +93,17 @@ const attrNum = (n: AdfNode, key: string): number | undefined => {
|
|
|
93
93
|
const v = n.attrs?.[key]
|
|
94
94
|
return typeof v === "number" ? v : undefined
|
|
95
95
|
}
|
|
96
|
+
const attrRecord = (n: AdfNode, key: string): Record<string, unknown> | undefined => {
|
|
97
|
+
const v = n.attrs?.[key]
|
|
98
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record<string, unknown> : undefined
|
|
99
|
+
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
success: "TIP",
|
|
104
|
-
error: "CAUTION"
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// btoa operates on byte strings; route through TextEncoder so non-ASCII attrs
|
|
108
|
-
// survive. Web APIs only — this module is a standalone subpath export and
|
|
109
|
-
// must not assume Node (same reasoning as internal/hashUtils' Web Crypto).
|
|
110
|
-
const toBase64 = (s: string): string => {
|
|
111
|
-
const bytes = new TextEncoder().encode(s)
|
|
112
|
-
let bin = ""
|
|
113
|
-
for (const b of bytes) bin += String.fromCharCode(b)
|
|
114
|
-
return btoa(bin)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Deterministic JSON for the placeholder attrs blob: object keys are sorted
|
|
118
|
-
// recursively so the same attrs always produce the same base64, no matter
|
|
119
|
-
// what order Confluence happens to serialize them in. Keeps pull → push →
|
|
120
|
-
// pull a byte-level fixed point (and contentHash stable).
|
|
101
|
+
const CONFLUENCE_CORE_MACRO_TYPE = "com.atlassian.confluence.macro.core"
|
|
102
|
+
|
|
103
|
+
// Deterministic JSON for placeholder metadata: object keys are sorted
|
|
104
|
+
// recursively so the same attrs always produce the same marker/sidecar data,
|
|
105
|
+
// no matter what order Confluence happens to serialize them in. Keeps pull →
|
|
106
|
+
// push → pull a byte-level fixed point (and contentHash stable).
|
|
121
107
|
const stableStringify = (v: unknown): string => {
|
|
122
108
|
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`
|
|
123
109
|
if (v !== null && typeof v === "object") {
|
|
@@ -163,24 +149,21 @@ const inlineNode = (n: AdfNode, ctx: Ctx): string => {
|
|
|
163
149
|
return id ? `[${display}](confluence-mention://${encodeURIComponent(id)})` : display
|
|
164
150
|
}
|
|
165
151
|
case "emoji": {
|
|
166
|
-
|
|
167
|
-
return short ? `:${short}:` : (attrStr(n, "text") ?? "")
|
|
152
|
+
return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
|
|
168
153
|
}
|
|
169
154
|
case "inlineCard": {
|
|
170
|
-
const url =
|
|
155
|
+
const url = cardUrl(n)
|
|
171
156
|
if (!url) {
|
|
172
157
|
// data-payload smart links have no URL to render — losing one must
|
|
173
158
|
// at least be visible in the logs.
|
|
174
159
|
ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: "inlineCard" })
|
|
175
160
|
return ""
|
|
176
161
|
}
|
|
177
|
-
|
|
162
|
+
const attrs = n.attrs ?? { url }
|
|
163
|
+
return `<!-- adf:inlineCard attrs=${stableStringify(attrs)} -->`
|
|
178
164
|
}
|
|
179
165
|
case "date": {
|
|
180
|
-
|
|
181
|
-
if (!ts) return ""
|
|
182
|
-
const d = new Date(Number(ts))
|
|
183
|
-
return Number.isNaN(d.getTime()) ? ts : d.toISOString().slice(0, 10)
|
|
166
|
+
return `<!-- adf:${n.type} node=${stableStringify(n)} -->`
|
|
184
167
|
}
|
|
185
168
|
case "status": {
|
|
186
169
|
const text = attrStr(n, "text") ?? ""
|
|
@@ -215,11 +198,55 @@ const extensionPlaceholder = (
|
|
|
215
198
|
// so macros survive a pull → push round-trip with their configuration.
|
|
216
199
|
const attrs = n.attrs ?? {}
|
|
217
200
|
const attrsPart = Object.keys(attrs).length > 0
|
|
218
|
-
? ` attrs=${
|
|
201
|
+
? ` attrs=${stableStringify(attrs)}`
|
|
219
202
|
: ""
|
|
220
203
|
return `<!-- adf:${nodeType}${keyPart}${typePart}${attrsPart} -->`
|
|
221
204
|
}
|
|
222
205
|
|
|
206
|
+
const objectKeys = (v: Record<string, unknown> | undefined): ReadonlyArray<string> => Object.keys(v ?? {})
|
|
207
|
+
const isOnlyKeys = (v: Record<string, unknown> | undefined, keys: ReadonlyArray<string>): boolean => {
|
|
208
|
+
const allowed = new Set(keys)
|
|
209
|
+
return objectKeys(v).every((key) => allowed.has(key))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const tocLevel = (macroParams: Record<string, unknown> | undefined, key: "minLevel" | "maxLevel"): string | null => {
|
|
213
|
+
const param = macroParams?.[key]
|
|
214
|
+
if (param === undefined) return null
|
|
215
|
+
if (param === null || typeof param !== "object" || Array.isArray(param)) return null
|
|
216
|
+
const record = param as Record<string, unknown>
|
|
217
|
+
if (!isOnlyKeys(record, ["value"])) return null
|
|
218
|
+
const value = record["value"]
|
|
219
|
+
return typeof value === "string" && /^[1-6]$/.test(value) ? value : null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const tocMarkdown = (n: AdfNode): string | null => {
|
|
223
|
+
const attrs = n.attrs
|
|
224
|
+
if (!attrs) return null
|
|
225
|
+
if (attrStr(n, "extensionKey") !== "toc" || attrStr(n, "extensionType") !== CONFLUENCE_CORE_MACRO_TYPE) return null
|
|
226
|
+
if (!isOnlyKeys(attrs, ["extensionKey", "extensionType", "parameters"])) return null
|
|
227
|
+
|
|
228
|
+
const parameters = attrRecord(n, "parameters")
|
|
229
|
+
if (!parameters) return "[[toc]]"
|
|
230
|
+
if (!isOnlyKeys(parameters, ["macroParams"])) return null
|
|
231
|
+
|
|
232
|
+
const macroParams = parameters["macroParams"]
|
|
233
|
+
if (macroParams === null || typeof macroParams !== "object" || Array.isArray(macroParams)) return null
|
|
234
|
+
const macroParamRecord = macroParams as Record<string, unknown>
|
|
235
|
+
if (!isOnlyKeys(macroParamRecord, ["minLevel", "maxLevel"])) return null
|
|
236
|
+
|
|
237
|
+
const minLevel = tocLevel(macroParamRecord, "minLevel")
|
|
238
|
+
const maxLevel = tocLevel(macroParamRecord, "maxLevel")
|
|
239
|
+
if (macroParamRecord["minLevel"] !== undefined && minLevel === null) return null
|
|
240
|
+
if (macroParamRecord["maxLevel"] !== undefined && maxLevel === null) return null
|
|
241
|
+
|
|
242
|
+
const parts = [
|
|
243
|
+
minLevel ? `min=${minLevel}` : "",
|
|
244
|
+
maxLevel ? `max=${maxLevel}` : ""
|
|
245
|
+
].filter((part) => part.length > 0)
|
|
246
|
+
|
|
247
|
+
return parts.length > 0 ? `[[toc:${parts.join(",")}]]` : "[[toc]]"
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
const bodiedExtension = (n: AdfNode, ctx: Ctx): string => {
|
|
224
251
|
const open = extensionPlaceholder(n, "bodiedExtension", ctx)
|
|
225
252
|
// Table cells flatten newlines to <br>, which would weld the markers and
|
|
@@ -299,7 +326,7 @@ const indentLines = (s: string, indent: string): string =>
|
|
|
299
326
|
const block = (n: AdfNode, ctx: Ctx): string => {
|
|
300
327
|
switch (n.type) {
|
|
301
328
|
case "paragraph":
|
|
302
|
-
return
|
|
329
|
+
return paragraph(n, ctx)
|
|
303
330
|
case "heading": {
|
|
304
331
|
const level = Math.min(6, Math.max(1, attrNum(n, "level") ?? 1))
|
|
305
332
|
return "#".repeat(level) + " " + inline(n.content, ctx)
|
|
@@ -325,12 +352,19 @@ const block = (n: AdfNode, ctx: Ctx): string => {
|
|
|
325
352
|
return taskList(n, ctx)
|
|
326
353
|
case "decisionList":
|
|
327
354
|
return decisionList(n, ctx)
|
|
355
|
+
case "layoutSection":
|
|
356
|
+
return layoutSection(n, ctx)
|
|
357
|
+
case "layoutColumn":
|
|
358
|
+
return layoutColumn(n, ctx)
|
|
328
359
|
case "mediaSingle":
|
|
329
360
|
return mediaSingle(n, ctx)
|
|
330
361
|
case "mediaGroup":
|
|
331
362
|
return mediaGroup(n, ctx)
|
|
363
|
+
case "blockCard":
|
|
364
|
+
case "embedCard":
|
|
365
|
+
return blockCard(n, ctx)
|
|
332
366
|
case "extension":
|
|
333
|
-
return extensionPlaceholder(n, "extension", ctx)
|
|
367
|
+
return tocMarkdown(n) ?? extensionPlaceholder(n, "extension", ctx)
|
|
334
368
|
case "bodiedExtension":
|
|
335
369
|
return bodiedExtension(n, ctx)
|
|
336
370
|
default:
|
|
@@ -339,6 +373,14 @@ const block = (n: AdfNode, ctx: Ctx): string => {
|
|
|
339
373
|
}
|
|
340
374
|
}
|
|
341
375
|
|
|
376
|
+
const paragraph = (n: AdfNode, ctx: Ctx): string => {
|
|
377
|
+
const body = escapeLineStarts(inline(n.content, ctx))
|
|
378
|
+
const marks = n.marks ?? []
|
|
379
|
+
if (marks.length === 0 || ctx.inTable) return body
|
|
380
|
+
const marksPart = ` marks=${stableStringify(marks)}`
|
|
381
|
+
return `<!-- adf:paragraph${marksPart} -->\n\n${body}\n\n<!-- adf:/paragraph -->`
|
|
382
|
+
}
|
|
383
|
+
|
|
342
384
|
const blockquote = (content: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
|
|
343
385
|
const inner = (content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
344
386
|
return inner.split("\n").map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\n")
|
|
@@ -423,15 +465,25 @@ const table = (n: AdfNode, ctx: Ctx): string => {
|
|
|
423
465
|
const separator = Array<string>(colCount).fill("---")
|
|
424
466
|
const bodyRows = (firstIsHeader ? allRows.slice(1) : allRows).map(pad)
|
|
425
467
|
const fmt = (cells: Array<string>): string => `| ${cells.join(" | ")} |`
|
|
426
|
-
return [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n")
|
|
468
|
+
return encodedBlockNode(n, [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n"), ctx)
|
|
427
469
|
}
|
|
428
470
|
|
|
429
471
|
const panel = (n: AdfNode, ctx: Ctx): string => {
|
|
430
472
|
const panelType = attrStr(n, "panelType") ?? "info"
|
|
431
|
-
const
|
|
473
|
+
const attrs = n.attrs ?? { panelType }
|
|
474
|
+
const attrsPart = Object.keys(attrs).length > 0 ? ` attrs=${stableStringify(attrs)}` : ""
|
|
475
|
+
const open = `<!-- adf:panel type=${panelType}${attrsPart} -->`
|
|
476
|
+
if (ctx.inTable) return open
|
|
432
477
|
const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
433
|
-
const
|
|
434
|
-
return
|
|
478
|
+
const parts = inner.length > 0 ? [open, inner] : [open]
|
|
479
|
+
return [...parts, "<!-- adf:/panel -->"].join("\n\n")
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const encodedBlockNode = (n: AdfNode, body: string, ctx: Ctx): string => {
|
|
483
|
+
if (ctx.inTable) return body
|
|
484
|
+
const open = `<!-- adf:${n.type} node=${stableStringify(n)} -->`
|
|
485
|
+
const parts = body.length > 0 ? [open, body] : [open]
|
|
486
|
+
return [...parts, `<!-- adf:/${n.type} -->`].join("\n\n")
|
|
435
487
|
}
|
|
436
488
|
|
|
437
489
|
const expand = (n: AdfNode, ctx: Ctx): string => {
|
|
@@ -442,7 +494,8 @@ const expand = (n: AdfNode, ctx: Ctx): string => {
|
|
|
442
494
|
// backslash escapes are the correct (and only working) form.
|
|
443
495
|
const safeTitle = ctx.inTable ? escapeText(title) : escapeHtml(title)
|
|
444
496
|
const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
445
|
-
return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
|
|
497
|
+
if (ctx.inTable) return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
|
|
498
|
+
return encodedBlockNode(n, `${title}\n\n${inner}`, ctx)
|
|
446
499
|
}
|
|
447
500
|
|
|
448
501
|
const taskList = (n: AdfNode, ctx: Ctx): string => {
|
|
@@ -457,7 +510,7 @@ const taskList = (n: AdfNode, ctx: Ctx): string => {
|
|
|
457
510
|
const text = inline(item.content, ctx)
|
|
458
511
|
lines.push(`- [${checked}] ${text}`)
|
|
459
512
|
}
|
|
460
|
-
return lines.join("\n")
|
|
513
|
+
return encodedBlockNode(n, lines.join("\n"), ctx)
|
|
461
514
|
}
|
|
462
515
|
|
|
463
516
|
const decisionList = (n: AdfNode, ctx: Ctx): string => {
|
|
@@ -470,7 +523,34 @@ const decisionList = (n: AdfNode, ctx: Ctx): string => {
|
|
|
470
523
|
}
|
|
471
524
|
lines.push(`- 🔘 ${inline(item.content, ctx)}`)
|
|
472
525
|
}
|
|
473
|
-
return lines.join("\n")
|
|
526
|
+
return encodedBlockNode(n, lines.join("\n"), ctx)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const layoutSection = (n: AdfNode, ctx: Ctx): string => {
|
|
530
|
+
const body = (n.content ?? [])
|
|
531
|
+
.map((column) => block(column, ctx))
|
|
532
|
+
.filter((part) => part.trim().length > 0)
|
|
533
|
+
.join("\n\n")
|
|
534
|
+
return encodedBlockNode(n, body, ctx)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const layoutColumn = (n: AdfNode, ctx: Ctx): string => (n.content ?? []).map((child) => block(child, ctx)).join("\n\n")
|
|
538
|
+
|
|
539
|
+
const cardUrl = (n: AdfNode): string | undefined => {
|
|
540
|
+
const url = attrStr(n, "url")
|
|
541
|
+
if (url) return url
|
|
542
|
+
const data = attrRecord(n, "data")
|
|
543
|
+
const dataUrl = data?.["url"]
|
|
544
|
+
return typeof dataUrl === "string" ? dataUrl : undefined
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const blockCard = (n: AdfNode, ctx: Ctx): string => {
|
|
548
|
+
const url = cardUrl(n)
|
|
549
|
+
if (!url) {
|
|
550
|
+
ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
|
|
551
|
+
return `<!-- unsupported ADF node: ${n.type} -->`
|
|
552
|
+
}
|
|
553
|
+
return encodedBlockNode(n, `<${url}>`, ctx)
|
|
474
554
|
}
|
|
475
555
|
|
|
476
556
|
const renderMedia = (media: AdfNode | undefined, ctx: Ctx): string => {
|