@knpkv/confluence-to-markdown 0.4.2 → 0.6.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 +35 -0
- package/README.md +45 -10
- package/dist/ConfluenceAuth.d.ts.map +1 -1
- package/dist/ConfluenceAuth.js +12 -22
- package/dist/ConfluenceAuth.js.map +1 -1
- package/dist/ConfluenceClient.d.ts +13 -3
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +34 -70
- package/dist/ConfluenceClient.js.map +1 -1
- package/dist/ConfluenceError.d.ts +12 -12
- package/dist/GitError.d.ts +5 -5
- package/dist/GitService.d.ts.map +1 -1
- package/dist/GitService.js +0 -3
- package/dist/GitService.js.map +1 -1
- package/dist/SchemaConverterError.d.ts +3 -3
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts +23 -0
- package/dist/parsers/preprocessing/ConfluencePreprocessing.d.ts.map +1 -0
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js +323 -0
- package/dist/parsers/preprocessing/ConfluencePreprocessing.js.map +1 -0
- package/dist/parsers/preprocessing/index.d.ts +7 -0
- package/dist/parsers/preprocessing/index.d.ts.map +1 -0
- package/dist/parsers/preprocessing/index.js +7 -0
- package/dist/parsers/preprocessing/index.js.map +1 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +29 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -1
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js +3 -5
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -1
- package/package.json +35 -26
- package/src/AdfPlaceholders.ts +266 -0
- package/src/AdfSchemaValidator.ts +67 -0
- package/src/AdfWalker.ts +511 -0
- package/src/AtlaskitTransformers.ts +72 -0
- package/src/ConfluenceClient.ts +4 -4
- package/src/ConfluenceError.ts +65 -3
- package/src/MarkdownConverter.ts +106 -139
- package/src/Schemas.ts +4 -4
- package/src/SyncEngine.ts +130 -83
- package/src/atlaskit-adf-schema.d.ts +3 -0
- package/src/commands/clone.ts +8 -1
- package/src/commands/layers.ts +11 -4
- package/src/index.ts +3 -18
- package/test/AdfPlaceholders.test.ts +295 -0
- package/test/AdfSchemaValidator.test.ts +34 -0
- package/test/AdfWalker.test.ts +530 -0
- package/test/AtlaskitTransformers.test.ts +25 -0
- package/test/MarkdownConverter.test.ts +120 -105
- package/test/RoundTrip.test.ts +266 -0
- package/LICENSE +0 -21
- package/src/SchemaConverterError.ts +0 -108
- package/src/ast/BlockNode.ts +0 -425
- package/src/ast/Document.ts +0 -90
- package/src/ast/InlineNode.ts +0 -323
- package/src/ast/MacroNode.ts +0 -245
- package/src/ast/index.ts +0 -83
- package/src/parsers/ConfluenceParser.ts +0 -950
- package/src/parsers/MarkdownParser.ts +0 -1198
- package/src/parsers/index.ts +0 -8
- package/src/schemas/ConfluenceSchema.ts +0 -56
- package/src/schemas/ConversionSchema.ts +0 -318
- package/src/schemas/MarkdownSchema.ts +0 -56
- package/src/schemas/hast/HastFromHtml.ts +0 -153
- package/src/schemas/hast/HastSchema.ts +0 -274
- package/src/schemas/hast/index.ts +0 -35
- package/src/schemas/index.ts +0 -20
- package/src/schemas/mdast/MdastFromMarkdown.ts +0 -118
- package/src/schemas/mdast/MdastSchema.ts +0 -566
- package/src/schemas/mdast/index.ts +0 -59
- package/src/schemas/mdast/mdastToString.ts +0 -102
- package/src/schemas/nodes/block/BlockSchema.ts +0 -773
- package/src/schemas/nodes/block/index.ts +0 -13
- package/src/schemas/nodes/index.ts +0 -20
- package/src/schemas/nodes/inline/InlineSchema.ts +0 -523
- package/src/schemas/nodes/inline/index.ts +0 -14
- package/src/schemas/nodes/macro/MacroSchema.ts +0 -226
- package/src/schemas/nodes/macro/index.ts +0 -6
- package/src/schemas/preprocessing/ConfluencePreprocessor.ts +0 -446
- package/src/schemas/preprocessing/index.ts +0 -8
- package/src/serializers/ConfluenceSerializer.ts +0 -717
- package/src/serializers/MarkdownSerializer.ts +0 -493
- package/src/serializers/index.ts +0 -8
- package/test/ast/BlockNode.test.ts +0 -265
- package/test/ast/Document.test.ts +0 -126
- package/test/ast/InlineNode.test.ts +0 -161
- package/test/fixtures/integration-test.html.fixture +0 -103
- package/test/fixtures/integration-test.md.expected +0 -257
- package/test/parsers/ConfluenceParser.test.ts +0 -283
- package/test/schemas/ConfluencePreprocessor.test.ts +0 -180
- package/test/schemas/ConversionSchema.test.ts +0 -159
- package/test/schemas/HastSchema.test.ts +0 -138
- package/test/schemas/MdastSchema.test.ts +0 -145
- package/test/schemas/nodes/block/BlockSchema.test.ts +0 -173
- package/test/schemas/nodes/inline/InlineSchema.test.ts +0 -198
- package/test/schemas/nodes/macro/MacroSchema.test.ts +0 -142
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse the AdfWalker's placeholder syntax back into proper ADF nodes
|
|
3
|
+
* after the @atlaskit markdown transformer has run.
|
|
4
|
+
*
|
|
5
|
+
* The walker emits Confluence-only nodes (status, extension, inlineExtension)
|
|
6
|
+
* as HTML/comment placeholders so they survive a pull. The markdown transformer
|
|
7
|
+
* has no concept of these nodes, so on push it parses the placeholders as
|
|
8
|
+
* plain text — Confluence then renders the literal HTML/comment. This module
|
|
9
|
+
* walks the produced ADF and rewrites those text patterns into the structured
|
|
10
|
+
* nodes the editor expects.
|
|
11
|
+
*
|
|
12
|
+
* Patterns recognized (must match the AdfWalker emission exactly):
|
|
13
|
+
* - `<span class="adf-status" data-color="COLOR">TEXT</span>`
|
|
14
|
+
* - `<!-- adf:extension key=KEY type=TYPE attrs=BASE64 -->` (block, when
|
|
15
|
+
* the whole paragraph is just this comment; `attrs` is base64 JSON of
|
|
16
|
+
* the node's full attrs — parameters, localId, layout — and wins over
|
|
17
|
+
* the readable key/type parts; key/type-only is the legacy form)
|
|
18
|
+
* - `<!-- adf:bodiedExtension … --> BODY <!-- adf:/bodiedExtension -->`
|
|
19
|
+
* (the sibling blocks between the markers become the extension's body)
|
|
20
|
+
* - `<!-- adf:inlineExtension key=KEY type=TYPE attrs=BASE64 -->` (inline)
|
|
21
|
+
* - `[@Name](confluence-mention://ACCOUNT_ID)` (link mark with a
|
|
22
|
+
* custom scheme — the only way to round-trip mention accountIds)
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as Either from "effect/Either"
|
|
28
|
+
import * as Schema from "effect/Schema"
|
|
29
|
+
|
|
30
|
+
interface AdfNode {
|
|
31
|
+
readonly type: string
|
|
32
|
+
readonly attrs?: Record<string, unknown>
|
|
33
|
+
readonly content?: ReadonlyArray<AdfNode>
|
|
34
|
+
readonly text?: string
|
|
35
|
+
readonly marks?: ReadonlyArray<AdfNode>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STATUS_RE = /<span class="adf-status"\s+data-color="([^"]+)">([^<]*)<\/span>/g
|
|
39
|
+
const INLINE_EXTENSION_RE =
|
|
40
|
+
/<!--\s*adf:inlineExtension(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([A-Za-z0-9+/=]+))?\s*-->/g
|
|
41
|
+
const COMBINED_INLINE_RE = new RegExp(`${STATUS_RE.source}|${INLINE_EXTENSION_RE.source}`, "g")
|
|
42
|
+
|
|
43
|
+
const BLOCK_EXTENSION_RE =
|
|
44
|
+
/^\s*<!--\s*adf:(extension|bodiedExtension)(?:\s+key=(\S+?))?(?:\s+type=(\S+?))?(?:\s+attrs=([A-Za-z0-9+/=]+))?\s*-->\s*$/
|
|
45
|
+
const BODIED_EXTENSION_END_RE = /^\s*<!--\s*adf:\/bodiedExtension\s*-->\s*$/
|
|
46
|
+
|
|
47
|
+
const textNode = (text: string, marks: ReadonlyArray<AdfNode> | undefined): AdfNode =>
|
|
48
|
+
marks && marks.length > 0 ? { type: "text", text, marks } : { type: "text", text }
|
|
49
|
+
|
|
50
|
+
// Code-marked text is a *quotation* of placeholder syntax, not a placeholder
|
|
51
|
+
// (the walker never emits placeholders with a code mark) — expanding it would
|
|
52
|
+
// corrupt documentation that demonstrates the syntax.
|
|
53
|
+
const hasCodeMark = (n: AdfNode): boolean => (n.marks ?? []).some((m) => m.type === "code")
|
|
54
|
+
|
|
55
|
+
// Parents whose content model permits bodiedExtension per @atlaskit/adf-schema
|
|
56
|
+
// (blockquote/listItem/tableCell allow plain extension but NOT bodied — emitting
|
|
57
|
+
// one there fails outgoing validation and the whole push errors out).
|
|
58
|
+
const BODIED_EXTENSION_PARENTS = new Set(["doc", "layoutColumn"])
|
|
59
|
+
|
|
60
|
+
// Web APIs only (atob/TextDecoder) — this module is a standalone subpath
|
|
61
|
+
// export and must not assume Node, mirroring the walker's encoder.
|
|
62
|
+
const fromBase64 = (b64: string): string => {
|
|
63
|
+
const bin = atob(b64)
|
|
64
|
+
return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// JSON string → free-form attrs record; rejects null/arrays/primitives.
|
|
68
|
+
const AttrsBlob = Schema.parseJson(Schema.Record({ key: Schema.String, value: Schema.Unknown }))
|
|
69
|
+
const decodeAttrsBlob = Schema.decodeUnknownEither(AttrsBlob)
|
|
70
|
+
|
|
71
|
+
const decodeAttrs = (b64: string | undefined): Record<string, unknown> | null => {
|
|
72
|
+
if (!b64) return null
|
|
73
|
+
try {
|
|
74
|
+
const decoded = decodeAttrsBlob(fromBase64(b64))
|
|
75
|
+
return Either.isRight(decoded) ? decoded.right : null
|
|
76
|
+
} catch {
|
|
77
|
+
// Invalid base64 (hand-edited file?) — fall back to the readable key/type.
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const buildExtensionAttrs = (
|
|
83
|
+
key: string | undefined,
|
|
84
|
+
type: string | undefined,
|
|
85
|
+
attrsB64: string | undefined
|
|
86
|
+
): Record<string, unknown> => {
|
|
87
|
+
const decoded = decodeAttrs(attrsB64)
|
|
88
|
+
if (decoded) return decoded
|
|
89
|
+
const attrs: Record<string, unknown> = {}
|
|
90
|
+
if (key) attrs.extensionKey = key
|
|
91
|
+
if (type) attrs.extensionType = type
|
|
92
|
+
return attrs
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Split a text node into a sequence of text + status + inlineExtension nodes. */
|
|
96
|
+
const expandInlineText = (
|
|
97
|
+
text: string,
|
|
98
|
+
marks: ReadonlyArray<AdfNode> | undefined
|
|
99
|
+
): ReadonlyArray<AdfNode> => {
|
|
100
|
+
// Reset lastIndex; sticky regexes are stateful across calls.
|
|
101
|
+
const re = new RegExp(COMBINED_INLINE_RE.source, "g")
|
|
102
|
+
const out: Array<AdfNode> = []
|
|
103
|
+
let lastIndex = 0
|
|
104
|
+
let match: RegExpExecArray | null
|
|
105
|
+
|
|
106
|
+
while ((match = re.exec(text)) !== null) {
|
|
107
|
+
if (match.index > lastIndex) {
|
|
108
|
+
out.push(textNode(text.slice(lastIndex, match.index), marks))
|
|
109
|
+
}
|
|
110
|
+
// Status capture groups: 1=color, 2=text. InlineExtension: 3=key, 4=type, 5=attrs.
|
|
111
|
+
if (match[1] !== undefined) {
|
|
112
|
+
out.push({ type: "status", attrs: { text: match[2] ?? "", color: match[1] } })
|
|
113
|
+
} else {
|
|
114
|
+
out.push({ type: "inlineExtension", attrs: buildExtensionAttrs(match[3], match[4], match[5]) })
|
|
115
|
+
}
|
|
116
|
+
lastIndex = match.index + match[0].length
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (lastIndex === 0) return [textNode(text, marks)]
|
|
120
|
+
if (lastIndex < text.length) out.push(textNode(text.slice(lastIndex), marks))
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const MENTION_SCHEME = "confluence-mention://"
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* If the text node carries a `confluence-mention://` link mark, return the
|
|
128
|
+
* corresponding mention node — otherwise null.
|
|
129
|
+
*/
|
|
130
|
+
const tryParseMentionTextNode = (n: AdfNode): AdfNode | null => {
|
|
131
|
+
if (!n.text || !n.marks) return null
|
|
132
|
+
const link = n.marks.find((m) => m.type === "link")
|
|
133
|
+
if (!link) return null
|
|
134
|
+
const href = link.attrs?.["href"]
|
|
135
|
+
if (typeof href !== "string" || !href.startsWith(MENTION_SCHEME)) return null
|
|
136
|
+
let id: string
|
|
137
|
+
try {
|
|
138
|
+
id = decodeURIComponent(href.slice(MENTION_SCHEME.length))
|
|
139
|
+
} catch {
|
|
140
|
+
// Malformed encoding — leave the link alone rather than crashing the push.
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
return { type: "mention", attrs: { id, text: n.text } }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface BlockExtensionMarker {
|
|
147
|
+
readonly kind: "extension" | "bodiedExtension"
|
|
148
|
+
readonly attrs: Record<string, unknown>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* If the paragraph's only content is a single text node holding a block-extension
|
|
153
|
+
* comment, return the marker's kind and reconstructed attrs — otherwise null.
|
|
154
|
+
*/
|
|
155
|
+
const soleTextChild = (node: AdfNode): AdfNode | null => {
|
|
156
|
+
if (node.type !== "paragraph") return null
|
|
157
|
+
const content = node.content ?? []
|
|
158
|
+
if (content.length !== 1) return null
|
|
159
|
+
const child = content[0]
|
|
160
|
+
if (!child || child.type !== "text" || !child.text || hasCodeMark(child)) return null
|
|
161
|
+
return child
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const parseBlockExtensionParagraph = (node: AdfNode): BlockExtensionMarker | null => {
|
|
165
|
+
const child = soleTextChild(node)
|
|
166
|
+
if (!child || !child.text) return null
|
|
167
|
+
const match = BLOCK_EXTENSION_RE.exec(child.text)
|
|
168
|
+
if (!match) return null
|
|
169
|
+
const [, kind, key, type, attrsB64] = match
|
|
170
|
+
return {
|
|
171
|
+
kind: kind === "bodiedExtension" ? "bodiedExtension" : "extension",
|
|
172
|
+
attrs: buildExtensionAttrs(key, type, attrsB64)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const isBodiedExtensionEnd = (node: AdfNode): boolean => {
|
|
177
|
+
const child = soleTextChild(node)
|
|
178
|
+
return child !== null && typeof child.text === "string" && BODIED_EXTENSION_END_RE.test(child.text)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Replace block-extension marker paragraphs among `children`. A bare
|
|
183
|
+
* `extension` marker becomes an extension node; a `bodiedExtension` marker
|
|
184
|
+
* swallows every sibling up to its `adf:/bodiedExtension` end marker as the
|
|
185
|
+
* extension's body.
|
|
186
|
+
*
|
|
187
|
+
* Pairing rules, in order of defence:
|
|
188
|
+
* - the forward scan stops at the next bodied *open* marker, so an unpaired
|
|
189
|
+
* legacy/hand-edited open cannot steal a later macro's end marker and
|
|
190
|
+
* swallow unrelated content in between;
|
|
191
|
+
* - an open with no end marker is downgraded to a plain extension (macro
|
|
192
|
+
* identity and configuration kept, body left in place as siblings);
|
|
193
|
+
* - an open/end pair with nothing in between keeps its bodied kind via a
|
|
194
|
+
* stub empty paragraph (the schema requires non-empty content);
|
|
195
|
+
* - parents whose content model forbids bodiedExtension (blockquote,
|
|
196
|
+
* listItem, …) get the downgrade too, or outgoing validation would fail;
|
|
197
|
+
* - stray end markers are dropped — they are this module's own syntax,
|
|
198
|
+
* never user content.
|
|
199
|
+
*/
|
|
200
|
+
const groupBlockExtensions = (children: ReadonlyArray<AdfNode>, parentType: string): ReadonlyArray<AdfNode> => {
|
|
201
|
+
const allowBodied = BODIED_EXTENSION_PARENTS.has(parentType)
|
|
202
|
+
const out: Array<AdfNode> = []
|
|
203
|
+
for (let i = 0; i < children.length; i++) {
|
|
204
|
+
const child = children[i]
|
|
205
|
+
if (!child) continue
|
|
206
|
+
const marker = parseBlockExtensionParagraph(child)
|
|
207
|
+
if (!marker) {
|
|
208
|
+
if (!isBodiedExtensionEnd(child)) out.push(child)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (marker.kind === "extension") {
|
|
212
|
+
out.push({ type: "extension", attrs: marker.attrs })
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
let end = -1
|
|
216
|
+
for (let j = i + 1; j < children.length; j++) {
|
|
217
|
+
if (isBodiedExtensionEnd(children[j]!)) {
|
|
218
|
+
end = j
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
if (parseBlockExtensionParagraph(children[j]!)?.kind === "bodiedExtension") break
|
|
222
|
+
}
|
|
223
|
+
if (end === -1 || !allowBodied) {
|
|
224
|
+
out.push({ type: "extension", attrs: marker.attrs })
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
// Group recursively so an extension marker *inside* the body (a macro
|
|
228
|
+
// nested in a bodied macro) is also reverted, not left as literal text.
|
|
229
|
+
const body = groupBlockExtensions(children.slice(i + 1, end), "bodiedExtension")
|
|
230
|
+
out.push({
|
|
231
|
+
type: "bodiedExtension",
|
|
232
|
+
attrs: marker.attrs,
|
|
233
|
+
content: body.length > 0 ? body : [{ type: "paragraph", content: [] }]
|
|
234
|
+
})
|
|
235
|
+
i = end
|
|
236
|
+
}
|
|
237
|
+
return out
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const transform = (node: AdfNode): AdfNode => {
|
|
241
|
+
// ADF codeBlock permits only plain text children — expanding placeholder-
|
|
242
|
+
// looking text inside one would inject schema-invalid nodes and corrupt
|
|
243
|
+
// code samples that merely *quote* the placeholder syntax.
|
|
244
|
+
if (node.type === "codeBlock") return node
|
|
245
|
+
if (!node.content) return node
|
|
246
|
+
|
|
247
|
+
const newContent: Array<AdfNode> = []
|
|
248
|
+
for (const child of node.content) {
|
|
249
|
+
if (child.type === "text" && child.text && !hasCodeMark(child)) {
|
|
250
|
+
const mention = tryParseMentionTextNode(child)
|
|
251
|
+
if (mention) {
|
|
252
|
+
newContent.push(mention)
|
|
253
|
+
} else {
|
|
254
|
+
for (const piece of expandInlineText(child.text, child.marks)) {
|
|
255
|
+
newContent.push(piece)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
newContent.push(transform(child))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { ...node, content: groupBlockExtensions(newContent, node.type) }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Walk the document tree and rewrite placeholder text into proper ADF nodes. */
|
|
266
|
+
export const revertPlaceholders = (doc: unknown): unknown => transform(doc as AdfNode)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime JSON-Schema validator for ADF documents.
|
|
3
|
+
*
|
|
4
|
+
* Wraps Ajv compiled against the canonical schema bundled in
|
|
5
|
+
* `@atlaskit/adf-schema` (`json-schema/v1/full.json`). Used on both
|
|
6
|
+
* directions of the conversion: incoming (after `JSON.parse`, before walking)
|
|
7
|
+
* and outgoing (after the @atlaskit transformer produces JSON, before
|
|
8
|
+
* `JSON.stringify`). The single project-wide cast `as DocNode` lives here,
|
|
9
|
+
* bridging Ajv's runtime predicate to the TypeScript types.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { DocNode } from "@atlaskit/adf-schema"
|
|
14
|
+
import adfJsonSchema from "@atlaskit/adf-schema/json-schema/v1/full.json" with { type: "json" }
|
|
15
|
+
import Ajv from "ajv-draft-04"
|
|
16
|
+
import * as Context from "effect/Context"
|
|
17
|
+
import * as Effect from "effect/Effect"
|
|
18
|
+
import * as Layer from "effect/Layer"
|
|
19
|
+
import { AdfSchemaError, type AdfSchemaIssue } from "./ConfluenceError.js"
|
|
20
|
+
|
|
21
|
+
const ajv = new Ajv({ strict: false, allErrors: true })
|
|
22
|
+
const validate = ajv.compile(adfJsonSchema as object)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Effect service that runtime-validates ADF documents against the canonical
|
|
26
|
+
* @atlaskit/adf-schema JSON Schema and narrows the success type to `DocNode`.
|
|
27
|
+
*
|
|
28
|
+
* @category Service
|
|
29
|
+
*/
|
|
30
|
+
export class AdfSchemaValidator extends Context.Tag(
|
|
31
|
+
"@knpkv/confluence-to-markdown/AdfSchemaValidator"
|
|
32
|
+
)<
|
|
33
|
+
AdfSchemaValidator,
|
|
34
|
+
{
|
|
35
|
+
readonly check: (
|
|
36
|
+
doc: unknown,
|
|
37
|
+
direction: "incoming" | "outgoing"
|
|
38
|
+
) => Effect.Effect<DocNode, AdfSchemaError>
|
|
39
|
+
}
|
|
40
|
+
>() {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Live Layer for `AdfSchemaValidator`. The Ajv validator is compiled once at
|
|
44
|
+
* module load.
|
|
45
|
+
*
|
|
46
|
+
* @category Layers
|
|
47
|
+
*/
|
|
48
|
+
export const layer: Layer.Layer<AdfSchemaValidator> = Layer.succeed(
|
|
49
|
+
AdfSchemaValidator,
|
|
50
|
+
AdfSchemaValidator.of({
|
|
51
|
+
check: (doc, direction) =>
|
|
52
|
+
validate(doc)
|
|
53
|
+
? Effect.succeed(doc as DocNode)
|
|
54
|
+
: Effect.fail(
|
|
55
|
+
new AdfSchemaError({
|
|
56
|
+
direction,
|
|
57
|
+
issues: (validate.errors ?? []).map((e): AdfSchemaIssue => ({
|
|
58
|
+
instancePath: e.instancePath,
|
|
59
|
+
schemaPath: e.schemaPath,
|
|
60
|
+
keyword: e.keyword,
|
|
61
|
+
params: e.params,
|
|
62
|
+
...(e.message !== undefined ? { message: e.message } : {})
|
|
63
|
+
}))
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
)
|