@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,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializer for AST to Markdown.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import type { CodeBlock, Heading, Image, Paragraph, Table, ThematicBreak, UnsupportedBlock } from "../ast/BlockNode.js"
|
|
8
|
+
import type { Document, DocumentNode } from "../ast/Document.js"
|
|
9
|
+
import type { InlineNode } from "../ast/InlineNode.js"
|
|
10
|
+
import type { SerializeError } from "../SchemaConverterError.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serialize Document AST to Markdown.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { serializeToMarkdown } from "@knpkv/confluence-to-markdown/serializers/MarkdownSerializer"
|
|
18
|
+
* import { makeDocument, Heading, Text } from "@knpkv/confluence-to-markdown/ast"
|
|
19
|
+
* import { Effect } from "effect"
|
|
20
|
+
*
|
|
21
|
+
* Effect.gen(function* () {
|
|
22
|
+
* const doc = makeDocument([
|
|
23
|
+
* new Heading({ level: 1, children: [new Text({ value: "Title" })] })
|
|
24
|
+
* ])
|
|
25
|
+
* const md = yield* serializeToMarkdown(doc)
|
|
26
|
+
* console.log(md) // # Title
|
|
27
|
+
* })
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @category Serializers
|
|
31
|
+
*/
|
|
32
|
+
export interface SerializeOptions {
|
|
33
|
+
/** Include raw Confluence HTML for lossless roundtrip. Default: true */
|
|
34
|
+
readonly includeRawSource?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const serializeToMarkdown = (
|
|
38
|
+
doc: Document,
|
|
39
|
+
options: SerializeOptions = {}
|
|
40
|
+
): Effect.Effect<string, SerializeError> =>
|
|
41
|
+
Effect.gen(function*() {
|
|
42
|
+
const { includeRawSource = true } = options
|
|
43
|
+
const parts: Array<string> = []
|
|
44
|
+
for (const node of doc.children) {
|
|
45
|
+
const serialized = yield* serializeDocumentNode(node)
|
|
46
|
+
parts.push(serialized)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content = parts.join("\n\n")
|
|
50
|
+
|
|
51
|
+
// Embed rawConfluence in comment for 1-to-1 roundtrip preservation
|
|
52
|
+
if (includeRawSource && doc.rawConfluence !== undefined) {
|
|
53
|
+
// Encode entire raw HTML for roundtrip
|
|
54
|
+
const encoded = Buffer.from(doc.rawConfluence, "utf-8").toString("base64")
|
|
55
|
+
return `${content}\n\n<!--cf:raw:${encoded}-->`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return content
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serialize a document node to Markdown.
|
|
63
|
+
*/
|
|
64
|
+
const serializeDocumentNode = (node: DocumentNode): Effect.Effect<string, SerializeError> =>
|
|
65
|
+
Effect.gen(function*() {
|
|
66
|
+
switch (node._tag) {
|
|
67
|
+
// Block nodes
|
|
68
|
+
case "Heading":
|
|
69
|
+
return yield* serializeHeading(node)
|
|
70
|
+
case "Paragraph":
|
|
71
|
+
return yield* serializeParagraph({
|
|
72
|
+
children: node.children,
|
|
73
|
+
alignment: node.alignment,
|
|
74
|
+
indent: node.indent
|
|
75
|
+
})
|
|
76
|
+
case "CodeBlock":
|
|
77
|
+
return serializeCodeBlock(node)
|
|
78
|
+
case "ThematicBreak":
|
|
79
|
+
return "---"
|
|
80
|
+
case "Image":
|
|
81
|
+
return serializeImage(node)
|
|
82
|
+
case "Table":
|
|
83
|
+
return yield* serializeTable(node)
|
|
84
|
+
case "List":
|
|
85
|
+
return yield* serializeList(node)
|
|
86
|
+
case "BlockQuote":
|
|
87
|
+
return yield* serializeBlockQuote(node)
|
|
88
|
+
case "UnsupportedBlock":
|
|
89
|
+
return node.rawMarkdown || node.rawHtml || ""
|
|
90
|
+
|
|
91
|
+
// Macro nodes
|
|
92
|
+
case "InfoPanel":
|
|
93
|
+
return yield* serializeInfoPanel(node)
|
|
94
|
+
case "ExpandMacro":
|
|
95
|
+
return yield* serializeExpandMacro(node)
|
|
96
|
+
case "TocMacro":
|
|
97
|
+
return serializeTocMacro(node)
|
|
98
|
+
case "CodeMacro":
|
|
99
|
+
return serializeCodeMacro(node)
|
|
100
|
+
case "StatusMacro":
|
|
101
|
+
return serializeStatusMacro(node)
|
|
102
|
+
case "TaskList":
|
|
103
|
+
return yield* serializeTaskList(node.children)
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
return ""
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serialize heading.
|
|
112
|
+
*/
|
|
113
|
+
const serializeHeading = (
|
|
114
|
+
node: { level: 1 | 2 | 3 | 4 | 5 | 6; children: ReadonlyArray<InlineNode> }
|
|
115
|
+
): Effect.Effect<string, SerializeError> =>
|
|
116
|
+
Effect.gen(function*() {
|
|
117
|
+
const prefix = "#".repeat(node.level)
|
|
118
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
119
|
+
return `${prefix} ${content}`
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Serialize paragraph.
|
|
124
|
+
*/
|
|
125
|
+
const serializeParagraph = (
|
|
126
|
+
node: {
|
|
127
|
+
children: ReadonlyArray<InlineNode>
|
|
128
|
+
alignment?: "left" | "center" | "right" | undefined
|
|
129
|
+
indent?: number | undefined
|
|
130
|
+
}
|
|
131
|
+
): Effect.Effect<string, SerializeError> =>
|
|
132
|
+
Effect.gen(function*() {
|
|
133
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
134
|
+
// If has alignment or indent, wrap in HTML div for roundtrip
|
|
135
|
+
if (node.alignment || node.indent) {
|
|
136
|
+
const styles: Array<string> = []
|
|
137
|
+
if (node.alignment) styles.push(`text-align: ${node.alignment};`)
|
|
138
|
+
if (node.indent) styles.push(`margin-left: ${node.indent}px;`)
|
|
139
|
+
return `<p style="${styles.join(" ")}">${content}</p>`
|
|
140
|
+
}
|
|
141
|
+
return content
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Serialize code block.
|
|
146
|
+
*/
|
|
147
|
+
const serializeCodeBlock = (node: { code: string; language?: string | undefined }): string => {
|
|
148
|
+
const lang = node.language || ""
|
|
149
|
+
return `\`\`\`${lang}\n${node.code}\n\`\`\``
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Serialize image (supports both URL and Confluence attachments).
|
|
154
|
+
* Uses comment-encoding for attachments to preserve roundtrip fidelity.
|
|
155
|
+
*/
|
|
156
|
+
const serializeImage = (node: {
|
|
157
|
+
src?: string | undefined
|
|
158
|
+
attachment?: { filename: string; version?: number | undefined } | undefined
|
|
159
|
+
alt?: string | undefined
|
|
160
|
+
title?: string | undefined
|
|
161
|
+
align?: string | undefined
|
|
162
|
+
width?: number | undefined
|
|
163
|
+
}): string => {
|
|
164
|
+
// If image has attachment or align/width, use comment encoding for roundtrip
|
|
165
|
+
if (node.attachment || node.align || node.width) {
|
|
166
|
+
const parts: Array<string> = []
|
|
167
|
+
if (node.attachment) {
|
|
168
|
+
parts.push(`f=${encodeURIComponent(node.attachment.filename)}`)
|
|
169
|
+
if (node.attachment.version) parts.push(`v=${node.attachment.version}`)
|
|
170
|
+
}
|
|
171
|
+
if (node.src) parts.push(`s=${encodeURIComponent(node.src)}`)
|
|
172
|
+
if (node.alt) parts.push(`a=${encodeURIComponent(node.alt)}`)
|
|
173
|
+
if (node.title) parts.push(`t=${encodeURIComponent(node.title)}`)
|
|
174
|
+
if (node.align) parts.push(`al=${node.align}`)
|
|
175
|
+
if (node.width) parts.push(`w=${node.width}`)
|
|
176
|
+
return `<!--cf:image:${parts.join("|")}-->`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Simple external image - use markdown syntax
|
|
180
|
+
const alt = node.alt || ""
|
|
181
|
+
const title = node.title ? ` "${node.title}"` : ""
|
|
182
|
+
const src = node.src || ""
|
|
183
|
+
return ``
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Serialize table.
|
|
188
|
+
*/
|
|
189
|
+
const serializeTable = (
|
|
190
|
+
node: {
|
|
191
|
+
header?: { cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> } | undefined
|
|
192
|
+
rows: ReadonlyArray<{ cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> }>
|
|
193
|
+
}
|
|
194
|
+
): Effect.Effect<string, SerializeError> =>
|
|
195
|
+
Effect.gen(function*() {
|
|
196
|
+
const lines: Array<string> = []
|
|
197
|
+
|
|
198
|
+
// Header
|
|
199
|
+
if (node.header) {
|
|
200
|
+
const headerCells: Array<string> = []
|
|
201
|
+
for (const cell of node.header.cells) {
|
|
202
|
+
headerCells.push(yield* serializeInlineNodes(cell.children))
|
|
203
|
+
}
|
|
204
|
+
lines.push(`| ${headerCells.join(" | ")} |`)
|
|
205
|
+
lines.push(`| ${headerCells.map(() => "---").join(" | ")} |`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Body rows
|
|
209
|
+
for (const row of node.rows) {
|
|
210
|
+
const cells: Array<string> = []
|
|
211
|
+
for (const cell of row.cells) {
|
|
212
|
+
cells.push(yield* serializeInlineNodes(cell.children))
|
|
213
|
+
}
|
|
214
|
+
lines.push(`| ${cells.join(" | ")} |`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return lines.join("\n")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Simple block type for list items
|
|
221
|
+
type SimpleBlock =
|
|
222
|
+
| Heading
|
|
223
|
+
| Paragraph
|
|
224
|
+
| CodeBlock
|
|
225
|
+
| ThematicBreak
|
|
226
|
+
| Image
|
|
227
|
+
| Table
|
|
228
|
+
| UnsupportedBlock
|
|
229
|
+
|
|
230
|
+
// List item type
|
|
231
|
+
type ListItemType = {
|
|
232
|
+
readonly _tag: "ListItem"
|
|
233
|
+
readonly checked?: boolean | undefined
|
|
234
|
+
readonly children: ReadonlyArray<SimpleBlock>
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Serialize list.
|
|
239
|
+
*/
|
|
240
|
+
const serializeList = (
|
|
241
|
+
node: { ordered: boolean; start?: number | undefined; children: ReadonlyArray<ListItemType> }
|
|
242
|
+
): Effect.Effect<string, SerializeError> =>
|
|
243
|
+
Effect.gen(function*() {
|
|
244
|
+
const lines: Array<string> = []
|
|
245
|
+
let counter = node.start || 1
|
|
246
|
+
|
|
247
|
+
for (const item of node.children) {
|
|
248
|
+
const prefix = node.ordered ? `${counter}.` : "-"
|
|
249
|
+
const checkbox = item.checked !== undefined ? (item.checked ? "[x] " : "[ ] ") : ""
|
|
250
|
+
|
|
251
|
+
// Serialize item content
|
|
252
|
+
const itemParts: Array<string> = []
|
|
253
|
+
for (const child of item.children) {
|
|
254
|
+
const serialized = yield* serializeSimpleBlock(child)
|
|
255
|
+
itemParts.push(serialized)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const content = itemParts.join("\n")
|
|
259
|
+
const indentedContent = content
|
|
260
|
+
.split("\n")
|
|
261
|
+
.map((line, i) => (i === 0 ? `${prefix} ${checkbox}${line}` : ` ${line}`))
|
|
262
|
+
.join("\n")
|
|
263
|
+
|
|
264
|
+
lines.push(indentedContent)
|
|
265
|
+
counter++
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return lines.join("\n")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Serialize simple block (for nested content).
|
|
273
|
+
*/
|
|
274
|
+
const serializeSimpleBlock = (node: SimpleBlock): Effect.Effect<string, SerializeError> =>
|
|
275
|
+
Effect.gen(function*() {
|
|
276
|
+
switch (node._tag) {
|
|
277
|
+
case "Heading":
|
|
278
|
+
return yield* serializeHeading(
|
|
279
|
+
node as unknown as { level: 1 | 2 | 3 | 4 | 5 | 6; children: ReadonlyArray<InlineNode> }
|
|
280
|
+
)
|
|
281
|
+
case "Paragraph":
|
|
282
|
+
return yield* serializeParagraph(node as unknown as { children: ReadonlyArray<InlineNode> })
|
|
283
|
+
case "CodeBlock":
|
|
284
|
+
return serializeCodeBlock(node as unknown as { code: string; language?: string | undefined })
|
|
285
|
+
case "ThematicBreak":
|
|
286
|
+
return "---"
|
|
287
|
+
case "Image":
|
|
288
|
+
return serializeImage(node as unknown as { src: string; alt?: string | undefined; title?: string | undefined })
|
|
289
|
+
case "Table":
|
|
290
|
+
return yield* serializeTable(
|
|
291
|
+
node as unknown as {
|
|
292
|
+
header?: { cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> } | undefined
|
|
293
|
+
rows: ReadonlyArray<{ cells: ReadonlyArray<{ children: ReadonlyArray<InlineNode> }> }>
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
case "UnsupportedBlock": {
|
|
297
|
+
const unsupported = node as unknown as { rawMarkdown?: string; rawHtml?: string }
|
|
298
|
+
return unsupported.rawMarkdown || unsupported.rawHtml || ""
|
|
299
|
+
}
|
|
300
|
+
default:
|
|
301
|
+
return ""
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Serialize block quote.
|
|
307
|
+
*/
|
|
308
|
+
const serializeBlockQuote = (
|
|
309
|
+
node: { children: ReadonlyArray<SimpleBlock> }
|
|
310
|
+
): Effect.Effect<string, SerializeError> =>
|
|
311
|
+
Effect.gen(function*() {
|
|
312
|
+
const lines: Array<string> = []
|
|
313
|
+
for (const child of node.children) {
|
|
314
|
+
const serialized = yield* serializeSimpleBlock(child as SimpleBlock)
|
|
315
|
+
const quoted = serialized.split("\n").map((line) => `> ${line}`).join("\n")
|
|
316
|
+
lines.push(quoted)
|
|
317
|
+
}
|
|
318
|
+
return lines.join("\n>\n")
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Serialize info panel to container syntax.
|
|
323
|
+
*/
|
|
324
|
+
const serializeInfoPanel = (
|
|
325
|
+
node: { panelType: string; title?: string | undefined; children: ReadonlyArray<SimpleBlock> }
|
|
326
|
+
): Effect.Effect<string, SerializeError> =>
|
|
327
|
+
Effect.gen(function*() {
|
|
328
|
+
const type = node.panelType
|
|
329
|
+
const title = node.title ? ` ${node.title}` : ""
|
|
330
|
+
const lines: Array<string> = [`:::${type}${title}`]
|
|
331
|
+
|
|
332
|
+
for (const child of node.children) {
|
|
333
|
+
const serialized = yield* serializeSimpleBlock(child as SimpleBlock)
|
|
334
|
+
lines.push(serialized)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lines.push(":::")
|
|
338
|
+
return lines.join("\n")
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Serialize expand macro - use comment encoding for roundtrip.
|
|
343
|
+
*/
|
|
344
|
+
const serializeExpandMacro = (
|
|
345
|
+
node: { title?: string | undefined; children: ReadonlyArray<SimpleBlock> }
|
|
346
|
+
): Effect.Effect<string, SerializeError> =>
|
|
347
|
+
Effect.gen(function*() {
|
|
348
|
+
const title = node.title || ""
|
|
349
|
+
const contentParts: Array<string> = []
|
|
350
|
+
|
|
351
|
+
for (const child of node.children) {
|
|
352
|
+
const serialized = yield* serializeSimpleBlock(child as SimpleBlock)
|
|
353
|
+
contentParts.push(serialized)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const content = contentParts.join("\n")
|
|
357
|
+
// Use comment encoding for roundtrip
|
|
358
|
+
return `<!--cf:expand:${encodeURIComponent(title)}:${encodeURIComponent(content)}-->`
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Serialize TOC macro.
|
|
363
|
+
*/
|
|
364
|
+
const serializeTocMacro = (_node: { minLevel?: number | undefined; maxLevel?: number | undefined }): string => {
|
|
365
|
+
return "[[toc]]"
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Serialize code macro (similar to code block but may have title).
|
|
370
|
+
*/
|
|
371
|
+
const serializeCodeMacro = (
|
|
372
|
+
node: { language?: string | undefined; title?: string | undefined; code: string }
|
|
373
|
+
): string => {
|
|
374
|
+
const lang = node.language || ""
|
|
375
|
+
const title = node.title ? ` title="${node.title}"` : ""
|
|
376
|
+
return `\`\`\`${lang}${title}\n${node.code}\n\`\`\``
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Serialize status macro.
|
|
381
|
+
*/
|
|
382
|
+
const serializeStatusMacro = (node: { text: string; color: string }): string => {
|
|
383
|
+
return `**[${node.text}]**`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Serialize task list - preserve as comment-encoded for roundtrip (single line).
|
|
388
|
+
*/
|
|
389
|
+
const serializeTaskList = (
|
|
390
|
+
children: ReadonlyArray<{
|
|
391
|
+
_tag: "TaskItem"
|
|
392
|
+
id: string
|
|
393
|
+
uuid: string
|
|
394
|
+
status: "incomplete" | "complete"
|
|
395
|
+
body: ReadonlyArray<InlineNode>
|
|
396
|
+
}>
|
|
397
|
+
): Effect.Effect<string, SerializeError> =>
|
|
398
|
+
Effect.gen(function*() {
|
|
399
|
+
const items: Array<string> = []
|
|
400
|
+
for (const item of children) {
|
|
401
|
+
const body = yield* serializeInlineNodes(item.body)
|
|
402
|
+
// Encode task item - use | separator to avoid : in content issues
|
|
403
|
+
items.push(`${item.id}|${item.uuid}|${item.status}|${encodeURIComponent(body)}`)
|
|
404
|
+
}
|
|
405
|
+
// Single line comment to prevent remark from splitting
|
|
406
|
+
return `<!--cf:tasklist:${items.join(";")}-->`
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Serialize inline nodes to Markdown.
|
|
411
|
+
*/
|
|
412
|
+
const serializeInlineNodes = (
|
|
413
|
+
nodes: ReadonlyArray<InlineNode>
|
|
414
|
+
): Effect.Effect<string, SerializeError> =>
|
|
415
|
+
Effect.gen(function*() {
|
|
416
|
+
const parts: Array<string> = []
|
|
417
|
+
for (const node of nodes) {
|
|
418
|
+
parts.push(yield* serializeInlineNode(node))
|
|
419
|
+
}
|
|
420
|
+
return parts.join("")
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Serialize inline node to Markdown.
|
|
425
|
+
*/
|
|
426
|
+
const serializeInlineNode = (node: InlineNode): Effect.Effect<string, SerializeError> =>
|
|
427
|
+
Effect.gen(function*() {
|
|
428
|
+
switch (node._tag) {
|
|
429
|
+
case "Text":
|
|
430
|
+
return node.value
|
|
431
|
+
case "Strong": {
|
|
432
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
433
|
+
return `**${content}**`
|
|
434
|
+
}
|
|
435
|
+
case "Emphasis": {
|
|
436
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
437
|
+
return `*${content}*`
|
|
438
|
+
}
|
|
439
|
+
case "Underline": {
|
|
440
|
+
// No native markdown support, use HTML
|
|
441
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
442
|
+
return `<u>${content}</u>`
|
|
443
|
+
}
|
|
444
|
+
case "Strikethrough": {
|
|
445
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
446
|
+
return `~~${content}~~`
|
|
447
|
+
}
|
|
448
|
+
case "Subscript": {
|
|
449
|
+
// No native markdown support, use HTML
|
|
450
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
451
|
+
return `<sub>${content}</sub>`
|
|
452
|
+
}
|
|
453
|
+
case "Superscript": {
|
|
454
|
+
// No native markdown support, use HTML
|
|
455
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
456
|
+
return `<sup>${content}</sup>`
|
|
457
|
+
}
|
|
458
|
+
case "InlineCode":
|
|
459
|
+
return `\`${node.value}\``
|
|
460
|
+
case "Link": {
|
|
461
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
462
|
+
const title = node.title ? ` "${node.title}"` : ""
|
|
463
|
+
return `[${content}](${node.href}${title})`
|
|
464
|
+
}
|
|
465
|
+
case "LineBreak":
|
|
466
|
+
return " \n"
|
|
467
|
+
case "Emoticon":
|
|
468
|
+
// Wrap in HTML comment with URL-encoded values
|
|
469
|
+
return `<!--cf:emoticon:${encodeURIComponent(node.shortname)}|${encodeURIComponent(node.emojiId)}|${
|
|
470
|
+
encodeURIComponent(node.fallback)
|
|
471
|
+
}-->`
|
|
472
|
+
case "UserMention":
|
|
473
|
+
// Wrap in HTML comment to prevent remark from parsing
|
|
474
|
+
return `<!--cf:user:${node.accountId}-->`
|
|
475
|
+
case "DateTime":
|
|
476
|
+
// Wrap in HTML comment to prevent remark from parsing
|
|
477
|
+
return `<!--cf:date:${node.datetime}-->`
|
|
478
|
+
case "ColoredText": {
|
|
479
|
+
// Preserve as HTML for roundtrip
|
|
480
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
481
|
+
return `<span style="color: ${node.color};">${content}</span>`
|
|
482
|
+
}
|
|
483
|
+
case "Highlight": {
|
|
484
|
+
// Preserve as HTML for roundtrip
|
|
485
|
+
const content = yield* serializeInlineNodes(node.children)
|
|
486
|
+
return `<span style="background-color: ${node.backgroundColor};">${content}</span>`
|
|
487
|
+
}
|
|
488
|
+
case "UnsupportedInline":
|
|
489
|
+
return node.raw
|
|
490
|
+
default:
|
|
491
|
+
return ""
|
|
492
|
+
}
|
|
493
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import { GitService, layer as GitServiceLayer } from "../src/GitService.js"
|
|
4
|
+
import { getConflictedFiles, GIT_LOG_FORMAT, parseGitLog, parseGitStatus } from "../src/internal/gitCommands.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for git parsing utilities and GitService.
|
|
8
|
+
*/
|
|
9
|
+
describe("GitService", () => {
|
|
10
|
+
describe("parseGitStatus", () => {
|
|
11
|
+
it("parses empty status", () => {
|
|
12
|
+
const entries = parseGitStatus("")
|
|
13
|
+
expect(entries).toEqual([])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("parses whitespace-only status", () => {
|
|
17
|
+
const entries = parseGitStatus(" \n ")
|
|
18
|
+
expect(entries).toEqual([])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("parses modified file", () => {
|
|
22
|
+
const entries = parseGitStatus(" M src/file.ts")
|
|
23
|
+
expect(entries).toHaveLength(1)
|
|
24
|
+
expect(entries[0]).toEqual({
|
|
25
|
+
status: "modified",
|
|
26
|
+
path: "src/file.ts",
|
|
27
|
+
staged: false
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("parses staged modified file", () => {
|
|
32
|
+
const entries = parseGitStatus("M src/file.ts")
|
|
33
|
+
expect(entries).toHaveLength(1)
|
|
34
|
+
expect(entries[0]).toEqual({
|
|
35
|
+
status: "modified",
|
|
36
|
+
path: "src/file.ts",
|
|
37
|
+
staged: true
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("parses added file", () => {
|
|
42
|
+
const entries = parseGitStatus("A new-file.ts")
|
|
43
|
+
expect(entries).toHaveLength(1)
|
|
44
|
+
expect(entries[0]).toEqual({
|
|
45
|
+
status: "added",
|
|
46
|
+
path: "new-file.ts",
|
|
47
|
+
staged: true
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("parses deleted file", () => {
|
|
52
|
+
const entries = parseGitStatus("D removed.ts")
|
|
53
|
+
expect(entries).toHaveLength(1)
|
|
54
|
+
expect(entries[0]).toEqual({
|
|
55
|
+
status: "deleted",
|
|
56
|
+
path: "removed.ts",
|
|
57
|
+
staged: true
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("parses untracked file", () => {
|
|
62
|
+
const entries = parseGitStatus("?? untracked.ts")
|
|
63
|
+
expect(entries).toHaveLength(1)
|
|
64
|
+
expect(entries[0]).toEqual({
|
|
65
|
+
status: "untracked",
|
|
66
|
+
path: "untracked.ts",
|
|
67
|
+
staged: false
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("parses unmerged file", () => {
|
|
72
|
+
const entries = parseGitStatus("UU conflicted.ts")
|
|
73
|
+
expect(entries).toHaveLength(1)
|
|
74
|
+
expect(entries[0]).toEqual({
|
|
75
|
+
status: "unmerged",
|
|
76
|
+
path: "conflicted.ts",
|
|
77
|
+
staged: true
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("parses multiple entries", () => {
|
|
82
|
+
const output = `M staged.ts
|
|
83
|
+
M unstaged.ts
|
|
84
|
+
?? new.ts`
|
|
85
|
+
const entries = parseGitStatus(output)
|
|
86
|
+
expect(entries).toHaveLength(3)
|
|
87
|
+
expect(entries[0]?.status).toBe("modified")
|
|
88
|
+
expect(entries[0]?.staged).toBe(true)
|
|
89
|
+
expect(entries[1]?.status).toBe("modified")
|
|
90
|
+
expect(entries[1]?.staged).toBe(false)
|
|
91
|
+
expect(entries[2]?.status).toBe("untracked")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("parses renamed file", () => {
|
|
95
|
+
const entries = parseGitStatus("R old.ts -> new.ts")
|
|
96
|
+
expect(entries).toHaveLength(1)
|
|
97
|
+
expect(entries[0]).toEqual({
|
|
98
|
+
status: "renamed",
|
|
99
|
+
path: "old.ts -> new.ts",
|
|
100
|
+
staged: true
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("parseGitLog", () => {
|
|
106
|
+
it("parses empty log", () => {
|
|
107
|
+
const entries = parseGitLog("")
|
|
108
|
+
expect(entries).toEqual([])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("parses whitespace-only log", () => {
|
|
112
|
+
const entries = parseGitLog(" \n ")
|
|
113
|
+
expect(entries).toEqual([])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("parses single log entry", () => {
|
|
117
|
+
const output = "abc123<|>John Doe<|>john@example.com<|>2024-01-15T10:30:00Z<|>Initial commit"
|
|
118
|
+
const entries = parseGitLog(output)
|
|
119
|
+
expect(entries).toHaveLength(1)
|
|
120
|
+
expect(entries[0]).toEqual({
|
|
121
|
+
hash: "abc123",
|
|
122
|
+
author: "John Doe",
|
|
123
|
+
email: "john@example.com",
|
|
124
|
+
date: new Date("2024-01-15T10:30:00Z"),
|
|
125
|
+
message: "Initial commit"
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("parses multiple log entries", () => {
|
|
130
|
+
const output = `abc123<|>John Doe<|>john@example.com<|>2024-01-15T10:30:00Z<|>First commit
|
|
131
|
+
def456<|>Jane Smith<|>jane@example.com<|>2024-01-16T11:00:00Z<|>Second commit`
|
|
132
|
+
const entries = parseGitLog(output)
|
|
133
|
+
expect(entries).toHaveLength(2)
|
|
134
|
+
expect(entries[0]?.hash).toBe("abc123")
|
|
135
|
+
expect(entries[1]?.hash).toBe("def456")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("handles message with delimiter characters", () => {
|
|
139
|
+
const output = "abc123<|>John<|>john@test.com<|>2024-01-15T10:30:00Z<|>Fix: a<|>b issue"
|
|
140
|
+
const entries = parseGitLog(output)
|
|
141
|
+
expect(entries).toHaveLength(1)
|
|
142
|
+
expect(entries[0]?.message).toBe("Fix: a<|>b issue")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("skips malformed entries", () => {
|
|
146
|
+
const output = `abc123<|>John<|>john@test.com<|>2024-01-15T10:30:00Z<|>Valid
|
|
147
|
+
malformed entry without enough parts
|
|
148
|
+
def456<|>Jane<|>jane@test.com<|>2024-01-16T11:00:00Z<|>Also valid`
|
|
149
|
+
const entries = parseGitLog(output)
|
|
150
|
+
expect(entries).toHaveLength(2)
|
|
151
|
+
expect(entries[0]?.hash).toBe("abc123")
|
|
152
|
+
expect(entries[1]?.hash).toBe("def456")
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe("getConflictedFiles", () => {
|
|
157
|
+
it("returns empty array for no conflicts", () => {
|
|
158
|
+
const output = `M file.ts
|
|
159
|
+
?? new.ts`
|
|
160
|
+
const files = getConflictedFiles(output)
|
|
161
|
+
expect(files).toEqual([])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it("returns conflicted files", () => {
|
|
165
|
+
const output = `M normal.ts
|
|
166
|
+
UU conflicted.ts
|
|
167
|
+
?? new.ts`
|
|
168
|
+
const files = getConflictedFiles(output)
|
|
169
|
+
expect(files).toEqual(["conflicted.ts"])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("returns multiple conflicted files", () => {
|
|
173
|
+
const output = `UU file1.ts
|
|
174
|
+
UU file2.ts
|
|
175
|
+
M normal.ts`
|
|
176
|
+
const files = getConflictedFiles(output)
|
|
177
|
+
expect(files).toHaveLength(2)
|
|
178
|
+
expect(files).toContain("file1.ts")
|
|
179
|
+
expect(files).toContain("file2.ts")
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe("GIT_LOG_FORMAT", () => {
|
|
184
|
+
it("contains expected format specifiers", () => {
|
|
185
|
+
expect(GIT_LOG_FORMAT).toContain("%H") // full hash
|
|
186
|
+
expect(GIT_LOG_FORMAT).toContain("%an") // author name
|
|
187
|
+
expect(GIT_LOG_FORMAT).toContain("%ae") // author email
|
|
188
|
+
expect(GIT_LOG_FORMAT).toContain("%aI") // author date ISO
|
|
189
|
+
expect(GIT_LOG_FORMAT).toContain("%s") // subject
|
|
190
|
+
expect(GIT_LOG_FORMAT).toContain("<|>") // delimiter
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe("GitService layer", () => {
|
|
195
|
+
it.effect("validates git is available", () =>
|
|
196
|
+
Effect.gen(function*() {
|
|
197
|
+
const git = yield* GitService
|
|
198
|
+
const version = yield* git.validateGit()
|
|
199
|
+
expect(version).toContain("git version")
|
|
200
|
+
}).pipe(Effect.provide(GitServiceLayer)))
|
|
201
|
+
|
|
202
|
+
it.effect("isInitialized returns boolean", () =>
|
|
203
|
+
Effect.gen(function*() {
|
|
204
|
+
const git = yield* GitService
|
|
205
|
+
const initialized = yield* git.isInitialized()
|
|
206
|
+
expect(typeof initialized).toBe("boolean")
|
|
207
|
+
}).pipe(Effect.provide(GitServiceLayer)))
|
|
208
|
+
})
|
|
209
|
+
})
|