@knpkv/confluence-to-markdown 0.5.0 → 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 +22 -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/ast/BlockNode.d.ts +48 -33
- package/dist/ast/BlockNode.d.ts.map +1 -1
- package/dist/ast/BlockNode.js +11 -2
- package/dist/ast/BlockNode.js.map +1 -1
- package/dist/ast/Document.d.ts +30 -2
- package/dist/ast/Document.d.ts.map +1 -1
- package/dist/parsers/ConfluenceParser.d.ts.map +1 -1
- package/dist/parsers/ConfluenceParser.js +7 -12
- package/dist/parsers/ConfluenceParser.js.map +1 -1
- package/dist/parsers/MarkdownParser.js +8 -117
- package/dist/parsers/MarkdownParser.js.map +1 -1
- 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 +5 -15
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -1
- package/dist/serializers/ConfluenceSerializer.js +0 -9
- package/dist/serializers/ConfluenceSerializer.js.map +1 -1
- package/dist/serializers/MarkdownSerializer.js +9 -49
- package/dist/serializers/MarkdownSerializer.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 -469
- 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 -956
- package/src/parsers/MarkdownParser.ts +0 -1338
- 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 -455
- package/src/schemas/preprocessing/index.ts +0 -8
- package/src/serializers/ConfluenceSerializer.ts +0 -737
- package/src/serializers/MarkdownSerializer.ts +0 -543
- 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 -452
- 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,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
|
+
)
|
package/src/AdfWalker.ts
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owned ADF → Markdown tree walker.
|
|
3
|
+
*
|
|
4
|
+
* Pure recursive descent over an ADF document's `node.type` discriminant.
|
|
5
|
+
* Returns GFM markdown plus a list of warnings for lossy or unknown nodes.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { DocNode } from "@atlaskit/adf-schema"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Warning emitted by the walker. Surfaced via `Effect.logWarning` at the
|
|
13
|
+
* facade boundary; never escalated to errors so a single weird node cannot
|
|
14
|
+
* break a clone of many pages.
|
|
15
|
+
*/
|
|
16
|
+
export type WalkerWarning =
|
|
17
|
+
| { readonly _tag: "UnsupportedNode"; readonly nodeType: string }
|
|
18
|
+
| { readonly _tag: "LossyMark"; readonly mark: string }
|
|
19
|
+
| { readonly _tag: "MediaWithoutUrl"; readonly mediaId: string }
|
|
20
|
+
| {
|
|
21
|
+
readonly _tag: "UnsupportedExtension"
|
|
22
|
+
readonly nodeType: "extension" | "bodiedExtension" | "inlineExtension"
|
|
23
|
+
readonly extensionKey: string
|
|
24
|
+
readonly extensionType: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WalkResult {
|
|
28
|
+
readonly markdown: string
|
|
29
|
+
readonly warnings: ReadonlyArray<WalkerWarning>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AdfNode {
|
|
33
|
+
readonly type: string
|
|
34
|
+
readonly attrs?: Record<string, unknown>
|
|
35
|
+
readonly content?: ReadonlyArray<AdfNode>
|
|
36
|
+
readonly text?: string
|
|
37
|
+
readonly marks?: ReadonlyArray<AdfNode>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Ctx {
|
|
41
|
+
readonly inTable: boolean
|
|
42
|
+
readonly warnings: Array<WalkerWarning>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Mid-line characters that change inline parsing in GFM. We deliberately omit
|
|
46
|
+
// `.` and `-` because they only carry meaning at line-start (numbered lists,
|
|
47
|
+
// setext rules) and escaping them mid-line produces noisy output like
|
|
48
|
+
// `v1\.0\.0` for ordinary version strings. Same reasoning drops `#`, `+`, `>`
|
|
49
|
+
// (line-start only), `(`/`)` (only meaningful right after `]`, which we
|
|
50
|
+
// escape), `!` (only meaningful right before `[`, ditto) and `{`/`}` (not
|
|
51
|
+
// special in GFM at all) — escaping those produced noise like `\(v2\)`.
|
|
52
|
+
const ESCAPE_RE = /[\\`*_[\]<|]/g
|
|
53
|
+
const escapeText = (s: string): string => s.replace(ESCAPE_RE, "\\$&")
|
|
54
|
+
const escapeAttr = (s: string): string => s.replace(/[\\"]/g, "\\$&")
|
|
55
|
+
// For text inside HTML *blocks* (`<details>`/`<summary>`): CommonMark treats
|
|
56
|
+
// everything up to the closing blank line as raw HTML, so backslash escapes
|
|
57
|
+
// would render literally — entity-escape instead.
|
|
58
|
+
const escapeHtml = (s: string): string =>
|
|
59
|
+
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
|
60
|
+
// `(`/`)`/space/`<`/`>`/`\` in a destination break `[text](url)` — wrap in
|
|
61
|
+
// angle brackets, percent-encoding the characters that would terminate or
|
|
62
|
+
// escape the wrapper itself.
|
|
63
|
+
const safeHref = (href: string): string =>
|
|
64
|
+
/[() <>\\]/.test(href) ? `<${href.replace(/[<>\\]/g, (c) => encodeURIComponent(c))}>` : href
|
|
65
|
+
// Alt text is substituted rather than escaped: @atlaskit's media markdown
|
|
66
|
+
// plugin throws on `\[`/`\]` in alt (making the page un-pushable), and
|
|
67
|
+
// newlines split the construct outright.
|
|
68
|
+
const sanitizeAlt = (s: string): string =>
|
|
69
|
+
s.replace(/\[/g, "(").replace(/\]/g, ")").replace(/\\/g, "/").replace(/\s+/g, " ").trim()
|
|
70
|
+
|
|
71
|
+
// ESCAPE_RE deliberately skips characters that are only special at line
|
|
72
|
+
// start, so lines assembled from paragraph text (including after hardBreak)
|
|
73
|
+
// must neutralize leading block markers: ATX headings, blockquotes, list
|
|
74
|
+
// bullets, ordered-list markers, thematic breaks, and setext underlines.
|
|
75
|
+
// A superfluous escape is harmless when the line later lands mid-line
|
|
76
|
+
// (after a list marker etc.) — backslash before punctuation always renders
|
|
77
|
+
// as the bare character.
|
|
78
|
+
const escapeLineStart = (line: string): string => {
|
|
79
|
+
if (/^(#{1,6}|[-+])(\s|$)/.test(line) || line.startsWith(">") || /^-{3,}\s*$/.test(line) || /^=+\s*$/.test(line)) {
|
|
80
|
+
return "\\" + line
|
|
81
|
+
}
|
|
82
|
+
const ordered = /^(\d+)([.)])(\s|$)/.exec(line)?.[1]
|
|
83
|
+
if (ordered) return `${ordered}\\${line.slice(ordered.length)}`
|
|
84
|
+
return line
|
|
85
|
+
}
|
|
86
|
+
const escapeLineStarts = (s: string): string => s.split("\n").map(escapeLineStart).join("\n")
|
|
87
|
+
|
|
88
|
+
const attrStr = (n: AdfNode, key: string): string | undefined => {
|
|
89
|
+
const v = n.attrs?.[key]
|
|
90
|
+
return typeof v === "string" ? v : undefined
|
|
91
|
+
}
|
|
92
|
+
const attrNum = (n: AdfNode, key: string): number | undefined => {
|
|
93
|
+
const v = n.attrs?.[key]
|
|
94
|
+
return typeof v === "number" ? v : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Color-matched to GitHub's admonition palette: info/blue→NOTE, note/purple→
|
|
98
|
+
// IMPORTANT, success/green→TIP, warning/yellow→WARNING, error/red→CAUTION.
|
|
99
|
+
const PANEL_MAP: Record<string, string> = {
|
|
100
|
+
info: "NOTE",
|
|
101
|
+
note: "IMPORTANT",
|
|
102
|
+
warning: "WARNING",
|
|
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).
|
|
121
|
+
const stableStringify = (v: unknown): string => {
|
|
122
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`
|
|
123
|
+
if (v !== null && typeof v === "object") {
|
|
124
|
+
const entries = Object.entries(v as Record<string, unknown>)
|
|
125
|
+
.filter(([, value]) => value !== undefined)
|
|
126
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
127
|
+
.map(([k, value]) => `${JSON.stringify(k)}:${stableStringify(value)}`)
|
|
128
|
+
return `{${entries.join(",")}}`
|
|
129
|
+
}
|
|
130
|
+
return JSON.stringify(v) ?? "null"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const inline = (nodes: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
|
|
134
|
+
if (!nodes) return ""
|
|
135
|
+
let out = ""
|
|
136
|
+
for (const n of nodes) out += inlineNode(n, ctx)
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const inlineNode = (n: AdfNode, ctx: Ctx): string => {
|
|
141
|
+
switch (n.type) {
|
|
142
|
+
case "text": {
|
|
143
|
+
const marks = n.marks ?? []
|
|
144
|
+
const text = n.text ?? ""
|
|
145
|
+
// Code spans render their content literally — backslash-escaping inside
|
|
146
|
+
// backticks would emit literal backslashes, which the push side then
|
|
147
|
+
// preserves verbatim, doubling them on every pull/push round-trip.
|
|
148
|
+
const hasCode = marks.some((m) => m.type === "code")
|
|
149
|
+
return applyMarks(hasCode ? text : escapeText(text), marks, ctx)
|
|
150
|
+
}
|
|
151
|
+
case "hardBreak":
|
|
152
|
+
return ctx.inTable ? "<br>" : " \n"
|
|
153
|
+
case "mention": {
|
|
154
|
+
// Confluence stores the mention's `text` attr with the leading `@`
|
|
155
|
+
// already (e.g. "@John Doe"). Strip it so we don't emit `@@John Doe`.
|
|
156
|
+
const id = attrStr(n, "id")
|
|
157
|
+
const raw = attrStr(n, "text") ?? id ?? ""
|
|
158
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw
|
|
159
|
+
const display = `@${escapeText(stripped)}`
|
|
160
|
+
// Encode the accountId in a custom-scheme link so the push side can
|
|
161
|
+
// reconstruct a real mention node. Without `id` we can only emit plain
|
|
162
|
+
// text; on push, that becomes plain text in Confluence (lossy).
|
|
163
|
+
return id ? `[${display}](confluence-mention://${encodeURIComponent(id)})` : display
|
|
164
|
+
}
|
|
165
|
+
case "emoji": {
|
|
166
|
+
const short = attrStr(n, "shortName")
|
|
167
|
+
return short ? `:${short}:` : (attrStr(n, "text") ?? "")
|
|
168
|
+
}
|
|
169
|
+
case "inlineCard": {
|
|
170
|
+
const url = attrStr(n, "url")
|
|
171
|
+
if (!url) {
|
|
172
|
+
// data-payload smart links have no URL to render — losing one must
|
|
173
|
+
// at least be visible in the logs.
|
|
174
|
+
ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: "inlineCard" })
|
|
175
|
+
return ""
|
|
176
|
+
}
|
|
177
|
+
return `<${url}>`
|
|
178
|
+
}
|
|
179
|
+
case "date": {
|
|
180
|
+
const ts = attrStr(n, "timestamp")
|
|
181
|
+
if (!ts) return ""
|
|
182
|
+
const d = new Date(Number(ts))
|
|
183
|
+
return Number.isNaN(d.getTime()) ? ts : d.toISOString().slice(0, 10)
|
|
184
|
+
}
|
|
185
|
+
case "status": {
|
|
186
|
+
const text = attrStr(n, "text") ?? ""
|
|
187
|
+
const color = attrStr(n, "color") ?? "neutral"
|
|
188
|
+
return `<span class="adf-status" data-color="${color}">${escapeText(text)}</span>`
|
|
189
|
+
}
|
|
190
|
+
case "mediaInline": {
|
|
191
|
+
const id = attrStr(n, "id") ?? ""
|
|
192
|
+
ctx.warnings.push({ _tag: "MediaWithoutUrl", mediaId: id })
|
|
193
|
+
return `<!-- adf:media id=${id} -->`
|
|
194
|
+
}
|
|
195
|
+
case "inlineExtension":
|
|
196
|
+
return extensionPlaceholder(n, "inlineExtension", ctx)
|
|
197
|
+
default:
|
|
198
|
+
ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
|
|
199
|
+
return `<!-- unsupported ADF inline: ${n.type} -->`
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const extensionPlaceholder = (
|
|
204
|
+
n: AdfNode,
|
|
205
|
+
nodeType: "extension" | "bodiedExtension" | "inlineExtension",
|
|
206
|
+
ctx: Ctx
|
|
207
|
+
): string => {
|
|
208
|
+
const extensionKey = attrStr(n, "extensionKey") ?? ""
|
|
209
|
+
const extensionType = attrStr(n, "extensionType") ?? ""
|
|
210
|
+
ctx.warnings.push({ _tag: "UnsupportedExtension", nodeType, extensionKey, extensionType })
|
|
211
|
+
const keyPart = extensionKey ? ` key=${extensionKey}` : ""
|
|
212
|
+
const typePart = extensionType ? ` type=${extensionType}` : ""
|
|
213
|
+
// key/type are repeated for human readability; `attrs` is the source of
|
|
214
|
+
// truth on push — it carries the *full* attrs (parameters, localId, layout)
|
|
215
|
+
// so macros survive a pull → push round-trip with their configuration.
|
|
216
|
+
const attrs = n.attrs ?? {}
|
|
217
|
+
const attrsPart = Object.keys(attrs).length > 0
|
|
218
|
+
? ` attrs=${toBase64(stableStringify(attrs))}`
|
|
219
|
+
: ""
|
|
220
|
+
return `<!-- adf:${nodeType}${keyPart}${typePart}${attrsPart} -->`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const bodiedExtension = (n: AdfNode, ctx: Ctx): string => {
|
|
224
|
+
const open = extensionPlaceholder(n, "bodiedExtension", ctx)
|
|
225
|
+
// Table cells flatten newlines to <br>, which would weld the markers and
|
|
226
|
+
// body into one un-revertible line — emit only the single-line marker
|
|
227
|
+
// there (body dropped; the placeholder warning above covers it).
|
|
228
|
+
if (ctx.inTable) return open
|
|
229
|
+
// Render the body so it stays visible/editable; the end marker lets the
|
|
230
|
+
// push side re-attach everything in between as the bodiedExtension's body.
|
|
231
|
+
// It is emitted even for an empty body so the push side can tell "bodied
|
|
232
|
+
// macro with nothing in it" apart from a legacy/corrupted open marker.
|
|
233
|
+
const body = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
234
|
+
const parts = body.length > 0 ? [open, body] : [open]
|
|
235
|
+
return [...parts, "<!-- adf:/bodiedExtension -->"].join("\n\n")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const applyMarks = (text: string, marks: ReadonlyArray<AdfNode>, ctx: Ctx): string => {
|
|
239
|
+
let out = text
|
|
240
|
+
for (const m of marks) {
|
|
241
|
+
switch (m.type) {
|
|
242
|
+
case "code": {
|
|
243
|
+
// Code-span content is unescaped, so per GFM the delimiter must be a
|
|
244
|
+
// backtick run longer than any run inside, space-padded when the
|
|
245
|
+
// content starts/ends with a backtick (or is empty).
|
|
246
|
+
const runs = out.match(/`+/g) ?? []
|
|
247
|
+
const fence = "`".repeat(runs.reduce((max, r) => Math.max(max, r.length), 0) + 1)
|
|
248
|
+
const pad = out === "" || out.startsWith("`") || out.endsWith("`") ? " " : ""
|
|
249
|
+
out = `${fence}${pad}${out}${pad}${fence}`
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
case "strong":
|
|
253
|
+
out = `**${out}**`
|
|
254
|
+
break
|
|
255
|
+
case "em":
|
|
256
|
+
out = `_${out}_`
|
|
257
|
+
break
|
|
258
|
+
case "strike":
|
|
259
|
+
out = `~~${out}~~`
|
|
260
|
+
break
|
|
261
|
+
case "link": {
|
|
262
|
+
const href = attrStr(m, "href") ?? ""
|
|
263
|
+
const title = attrStr(m, "title")
|
|
264
|
+
const titlePart = title ? ` "${escapeAttr(title)}"` : ""
|
|
265
|
+
out = `[${out}](${safeHref(href)}${titlePart})`
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
case "underline":
|
|
269
|
+
ctx.warnings.push({ _tag: "LossyMark", mark: "underline" })
|
|
270
|
+
out = `<u>${out}</u>`
|
|
271
|
+
break
|
|
272
|
+
case "textColor": {
|
|
273
|
+
const color = attrStr(m, "color") ?? ""
|
|
274
|
+
ctx.warnings.push({ _tag: "LossyMark", mark: "textColor" })
|
|
275
|
+
out = `<span style="color:${color}">${out}</span>`
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
case "backgroundColor": {
|
|
279
|
+
const color = attrStr(m, "color") ?? ""
|
|
280
|
+
ctx.warnings.push({ _tag: "LossyMark", mark: "backgroundColor" })
|
|
281
|
+
out = `<span style="background-color:${color}">${out}</span>`
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
case "subsup": {
|
|
285
|
+
const t = attrStr(m, "type") === "sup" ? "sup" : "sub"
|
|
286
|
+
out = `<${t}>${out}</${t}>`
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
default:
|
|
290
|
+
ctx.warnings.push({ _tag: "LossyMark", mark: m.type })
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const indentLines = (s: string, indent: string): string =>
|
|
297
|
+
s.split("\n").map((line, i) => (i === 0 ? line : indent + line)).join("\n")
|
|
298
|
+
|
|
299
|
+
const block = (n: AdfNode, ctx: Ctx): string => {
|
|
300
|
+
switch (n.type) {
|
|
301
|
+
case "paragraph":
|
|
302
|
+
return escapeLineStarts(inline(n.content, ctx))
|
|
303
|
+
case "heading": {
|
|
304
|
+
const level = Math.min(6, Math.max(1, attrNum(n, "level") ?? 1))
|
|
305
|
+
return "#".repeat(level) + " " + inline(n.content, ctx)
|
|
306
|
+
}
|
|
307
|
+
case "rule":
|
|
308
|
+
return "---"
|
|
309
|
+
case "blockquote":
|
|
310
|
+
return blockquote(n.content, ctx)
|
|
311
|
+
case "codeBlock":
|
|
312
|
+
return codeBlock(n)
|
|
313
|
+
case "bulletList":
|
|
314
|
+
return list(n, ctx, false)
|
|
315
|
+
case "orderedList":
|
|
316
|
+
return list(n, ctx, true)
|
|
317
|
+
case "table":
|
|
318
|
+
return table(n, ctx)
|
|
319
|
+
case "panel":
|
|
320
|
+
return panel(n, ctx)
|
|
321
|
+
case "expand":
|
|
322
|
+
case "nestedExpand":
|
|
323
|
+
return expand(n, ctx)
|
|
324
|
+
case "taskList":
|
|
325
|
+
return taskList(n, ctx)
|
|
326
|
+
case "decisionList":
|
|
327
|
+
return decisionList(n, ctx)
|
|
328
|
+
case "mediaSingle":
|
|
329
|
+
return mediaSingle(n, ctx)
|
|
330
|
+
case "mediaGroup":
|
|
331
|
+
return mediaGroup(n, ctx)
|
|
332
|
+
case "extension":
|
|
333
|
+
return extensionPlaceholder(n, "extension", ctx)
|
|
334
|
+
case "bodiedExtension":
|
|
335
|
+
return bodiedExtension(n, ctx)
|
|
336
|
+
default:
|
|
337
|
+
ctx.warnings.push({ _tag: "UnsupportedNode", nodeType: n.type })
|
|
338
|
+
return `<!-- unsupported ADF node: ${n.type} -->`
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const blockquote = (content: ReadonlyArray<AdfNode> | undefined, ctx: Ctx): string => {
|
|
343
|
+
const inner = (content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
344
|
+
return inner.split("\n").map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\n")
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const codeBlock = (n: AdfNode): string => {
|
|
348
|
+
// A fence's info string may not contain backticks (CommonMark) and a
|
|
349
|
+
// newline would inject lines into the code content — the editor UI uses a
|
|
350
|
+
// fixed language list, but the REST API accepts arbitrary strings.
|
|
351
|
+
const lang = (attrStr(n, "language") ?? "").replace(/[`\s]+/g, "")
|
|
352
|
+
const text = (n.content ?? []).map((c) => c.text ?? "").join("")
|
|
353
|
+
// A fixed ``` fence would be terminated early by code that itself contains
|
|
354
|
+
// a triple-backtick run — use one backtick more than the longest run inside.
|
|
355
|
+
const runs = text.match(/`+/g) ?? []
|
|
356
|
+
const fence = "`".repeat(Math.max(3, runs.reduce((max, r) => Math.max(max, r.length), 0) + 1))
|
|
357
|
+
return fence + lang + "\n" + text + "\n" + fence
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const listItemBlocks = (item: AdfNode, ctx: Ctx): string => {
|
|
361
|
+
const blocks = item.content ?? []
|
|
362
|
+
const parts: Array<string> = []
|
|
363
|
+
for (const b of blocks) {
|
|
364
|
+
if (b.type === "paragraph") {
|
|
365
|
+
// Continuation lines (after hardBreak) sit at line start once the
|
|
366
|
+
// item is indented, so they need the same leading-marker escapes as
|
|
367
|
+
// top-level paragraphs.
|
|
368
|
+
parts.push(escapeLineStarts(inline(b.content, ctx)))
|
|
369
|
+
} else {
|
|
370
|
+
parts.push(block(b, ctx))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return parts.join("\n\n")
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const list = (n: AdfNode, ctx: Ctx, ordered: boolean): string => {
|
|
377
|
+
const items = n.content ?? []
|
|
378
|
+
const startNum = ordered ? Math.max(1, attrNum(n, "order") ?? 1) : 1
|
|
379
|
+
const indent = " "
|
|
380
|
+
const inner: Array<string> = []
|
|
381
|
+
for (let i = 0; i < items.length; i++) {
|
|
382
|
+
const item = items[i]
|
|
383
|
+
if (!item) continue
|
|
384
|
+
const marker = ordered ? `${startNum + i}. ` : "- "
|
|
385
|
+
const body = listItemBlocks(item, ctx)
|
|
386
|
+
inner.push(marker + indentLines(body, indent))
|
|
387
|
+
}
|
|
388
|
+
return inner.join("\n")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const tableCellInline = (cell: AdfNode, ctx: Ctx): string => {
|
|
392
|
+
const cellCtx: Ctx = { ...ctx, inTable: true }
|
|
393
|
+
const blocks = cell.content ?? []
|
|
394
|
+
const parts: Array<string> = []
|
|
395
|
+
for (const b of blocks) {
|
|
396
|
+
if (b.type === "paragraph") parts.push(inline(b.content, cellCtx))
|
|
397
|
+
else parts.push(block(b, cellCtx).replace(/\n/g, "<br>"))
|
|
398
|
+
}
|
|
399
|
+
// Escape `|` so it can't open a new column — but only pipes that aren't
|
|
400
|
+
// already escaped (inline() escapes them in plain text; code spans, URLs
|
|
401
|
+
// and <br>-flattened blocks don't). A pipe is escaped iff it's preceded by
|
|
402
|
+
// an odd run of backslashes, so count the run rather than peek one char.
|
|
403
|
+
return parts.join("<br>").replace(
|
|
404
|
+
/(\\*)\|/g,
|
|
405
|
+
(match, backslashes: string) => backslashes.length % 2 === 0 ? `${backslashes}\\|` : match
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const table = (n: AdfNode, ctx: Ctx): string => {
|
|
410
|
+
const rows = n.content ?? []
|
|
411
|
+
if (rows.length === 0) return ""
|
|
412
|
+
const renderRow = (row: AdfNode): Array<string> => (row.content ?? []).map((cell) => tableCellInline(cell, ctx))
|
|
413
|
+
const allRows = rows.map(renderRow)
|
|
414
|
+
const colCount = Math.max(...allRows.map((r) => r.length))
|
|
415
|
+
const pad = (cells: Array<string>): Array<string> => {
|
|
416
|
+
const out = cells.slice()
|
|
417
|
+
while (out.length < colCount) out.push("")
|
|
418
|
+
return out
|
|
419
|
+
}
|
|
420
|
+
const firstRow = rows[0]
|
|
421
|
+
const firstIsHeader = (firstRow?.content ?? []).every((c) => c.type === "tableHeader")
|
|
422
|
+
const header = firstIsHeader ? pad(allRows[0] ?? []) : Array<string>(colCount).fill("")
|
|
423
|
+
const separator = Array<string>(colCount).fill("---")
|
|
424
|
+
const bodyRows = (firstIsHeader ? allRows.slice(1) : allRows).map(pad)
|
|
425
|
+
const fmt = (cells: Array<string>): string => `| ${cells.join(" | ")} |`
|
|
426
|
+
return [fmt(header), fmt(separator), ...bodyRows.map(fmt)].join("\n")
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const panel = (n: AdfNode, ctx: Ctx): string => {
|
|
430
|
+
const panelType = attrStr(n, "panelType") ?? "info"
|
|
431
|
+
const tag = PANEL_MAP[panelType] ?? "NOTE"
|
|
432
|
+
const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
433
|
+
const lines = [`[!${tag}]`, ...inner.split("\n")]
|
|
434
|
+
return lines.map((l) => (l.length === 0 ? ">" : `> ${l}`)).join("\n")
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const expand = (n: AdfNode, ctx: Ctx): string => {
|
|
438
|
+
const title = attrStr(n, "title") ?? ""
|
|
439
|
+
// At block level <details> is a CommonMark type-6 HTML block, so the title
|
|
440
|
+
// needs entity escaping. Inside a table cell the flattened output becomes
|
|
441
|
+
// *inline* HTML where the text between tags is still markdown — there the
|
|
442
|
+
// backslash escapes are the correct (and only working) form.
|
|
443
|
+
const safeTitle = ctx.inTable ? escapeText(title) : escapeHtml(title)
|
|
444
|
+
const inner = (n.content ?? []).map((c) => block(c, ctx)).join("\n\n")
|
|
445
|
+
return `<details><summary>${safeTitle}</summary>\n\n${inner}\n\n</details>`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const taskList = (n: AdfNode, ctx: Ctx): string => {
|
|
449
|
+
const items = n.content ?? []
|
|
450
|
+
const lines: Array<string> = []
|
|
451
|
+
for (const item of items) {
|
|
452
|
+
if (item.type !== "taskItem") {
|
|
453
|
+
lines.push(block(item, ctx))
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
const checked = attrStr(item, "state") === "DONE" ? "x" : " "
|
|
457
|
+
const text = inline(item.content, ctx)
|
|
458
|
+
lines.push(`- [${checked}] ${text}`)
|
|
459
|
+
}
|
|
460
|
+
return lines.join("\n")
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const decisionList = (n: AdfNode, ctx: Ctx): string => {
|
|
464
|
+
const items = n.content ?? []
|
|
465
|
+
const lines: Array<string> = []
|
|
466
|
+
for (const item of items) {
|
|
467
|
+
if (item.type !== "decisionItem") {
|
|
468
|
+
lines.push(block(item, ctx))
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
lines.push(`- 🔘 ${inline(item.content, ctx)}`)
|
|
472
|
+
}
|
|
473
|
+
return lines.join("\n")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const renderMedia = (media: AdfNode | undefined, ctx: Ctx): string => {
|
|
477
|
+
const id = (media && attrStr(media, "id")) ?? ""
|
|
478
|
+
const alt = (media && attrStr(media, "alt")) ?? ""
|
|
479
|
+
const url = media && attrStr(media, "url")
|
|
480
|
+
if (url) return `})`
|
|
481
|
+
ctx.warnings.push({ _tag: "MediaWithoutUrl", mediaId: id })
|
|
482
|
+
return `<!-- adf:media id=${id} -->`
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const mediaSingle = (n: AdfNode, ctx: Ctx): string => {
|
|
486
|
+
const children = n.content ?? []
|
|
487
|
+
const rendered = renderMedia(children.find((c) => c.type === "media"), ctx)
|
|
488
|
+
const caption = children.find((c) => c.type === "caption")
|
|
489
|
+
const captionText = caption ? inline(caption.content, ctx).trim() : ""
|
|
490
|
+
if (captionText.length === 0) return rendered
|
|
491
|
+
// An em-marked caption already renders as `_…_`; wrapping again would make
|
|
492
|
+
// `__…__` (strong). Leave captions that touch an underscore unwrapped.
|
|
493
|
+
const line = captionText.startsWith("_") || captionText.endsWith("_") ? captionText : `_${captionText}_`
|
|
494
|
+
return `${rendered}\n${line}`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const mediaGroup = (n: AdfNode, ctx: Ctx): string =>
|
|
498
|
+
(n.content ?? []).map((media) => renderMedia(media, ctx)).join("\n\n")
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Walk an ADF document and emit GFM markdown. Always synchronous; warnings
|
|
502
|
+
* are collected, not thrown.
|
|
503
|
+
*/
|
|
504
|
+
export const walk = (doc: DocNode): WalkResult => {
|
|
505
|
+
const ctx: Ctx = { inTable: false, warnings: [] }
|
|
506
|
+
const root = doc as unknown as AdfNode
|
|
507
|
+
const blocks = (root.content ?? []).map((c) => block(c, ctx))
|
|
508
|
+
const body = blocks.join("\n\n")
|
|
509
|
+
const markdown = body.endsWith("\n") ? body : body + "\n"
|
|
510
|
+
return { markdown, warnings: ctx.warnings }
|
|
511
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect wrapper around the official @atlaskit markdown / JSON transformers.
|
|
3
|
+
*
|
|
4
|
+
* Push direction (markdown → ADF) routes through `MarkdownTransformer.parse()`
|
|
5
|
+
* (markdown → ProseMirror node) followed by `JSONTransformer.encode()`
|
|
6
|
+
* (ProseMirror node → ADF JSON). Both libraries are stateless once
|
|
7
|
+
* constructed, so we instantiate them once at module load.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
// Deep import into the published CJS file: Atlaskit's `schema-default`
|
|
12
|
+
// subpath has no `exports` map (Node ESM rejects it as a directory import)
|
|
13
|
+
// and its ESM build uses extensionless relative imports (also rejected).
|
|
14
|
+
// The CJS file works under Node's CJS-into-ESM interop. Types come from the
|
|
15
|
+
// ambient declaration in `atlaskit-adf-schema.d.ts`.
|
|
16
|
+
import { defaultSchema } from "@atlaskit/adf-schema/dist/cjs/schema/default-schema.js"
|
|
17
|
+
import { type JSONDocNode, JSONTransformer } from "@atlaskit/editor-json-transformer"
|
|
18
|
+
import { MarkdownTransformer } from "@atlaskit/editor-markdown-transformer"
|
|
19
|
+
import * as Context from "effect/Context"
|
|
20
|
+
import * as Effect from "effect/Effect"
|
|
21
|
+
import * as Layer from "effect/Layer"
|
|
22
|
+
import { AtlaskitTransformersError } from "./ConfluenceError.js"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bag of the two transformer instances handed to the `use` callback.
|
|
26
|
+
*/
|
|
27
|
+
export interface Transformers {
|
|
28
|
+
readonly md: MarkdownTransformer
|
|
29
|
+
readonly json: JSONTransformer
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const md = new MarkdownTransformer(defaultSchema)
|
|
33
|
+
const json = new JSONTransformer(defaultSchema)
|
|
34
|
+
const transformers: Transformers = { md, json }
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Effect service exposing the @atlaskit markdown + JSON transformers via a
|
|
38
|
+
* `use` callback. Errors thrown synchronously by the underlying libraries are
|
|
39
|
+
* surfaced as `AtlaskitTransformersError`.
|
|
40
|
+
*
|
|
41
|
+
* @category Service
|
|
42
|
+
*/
|
|
43
|
+
export class AtlaskitTransformers extends Context.Tag(
|
|
44
|
+
"@knpkv/confluence-to-markdown/AtlaskitTransformers"
|
|
45
|
+
)<
|
|
46
|
+
AtlaskitTransformers,
|
|
47
|
+
{
|
|
48
|
+
readonly use: <A>(
|
|
49
|
+
fn: (t: Transformers) => A
|
|
50
|
+
) => Effect.Effect<A, AtlaskitTransformersError>
|
|
51
|
+
}
|
|
52
|
+
>() {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Live Layer providing the wrapped @atlaskit transformers. The transformer
|
|
56
|
+
* instances are module-level singletons; the layer just hands out a
|
|
57
|
+
* `use`-callback service that catches synchronous throws.
|
|
58
|
+
*
|
|
59
|
+
* @category Layers
|
|
60
|
+
*/
|
|
61
|
+
export const layer: Layer.Layer<AtlaskitTransformers> = Layer.succeed(
|
|
62
|
+
AtlaskitTransformers,
|
|
63
|
+
AtlaskitTransformers.of({
|
|
64
|
+
use: <A>(fn: (t: Transformers) => A) =>
|
|
65
|
+
Effect.try({
|
|
66
|
+
try: () => fn(transformers),
|
|
67
|
+
catch: (cause) => new AtlaskitTransformersError({ cause })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
export type { JSONDocNode }
|
package/src/ConfluenceClient.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface CreatePageRequest {
|
|
|
26
26
|
readonly title: string
|
|
27
27
|
readonly parentId?: string
|
|
28
28
|
readonly body: {
|
|
29
|
-
readonly representation: "
|
|
29
|
+
readonly representation: "atlas_doc_format"
|
|
30
30
|
readonly value: string
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -45,7 +45,7 @@ export interface UpdatePageRequest {
|
|
|
45
45
|
readonly message?: string
|
|
46
46
|
}
|
|
47
47
|
readonly body: {
|
|
48
|
-
readonly representation: "
|
|
48
|
+
readonly representation: "atlas_doc_format"
|
|
49
49
|
readonly value: string
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -194,7 +194,7 @@ const make = (
|
|
|
194
194
|
|
|
195
195
|
const getPage = (id: PageId): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
|
|
196
196
|
toEffect(apiClient.v2.client.GET("/pages/{id}", {
|
|
197
|
-
params: { path: { id: Number(id) }, query: { "body-format": "
|
|
197
|
+
params: { path: { id: Number(id) }, query: { "body-format": "atlas_doc_format" } }
|
|
198
198
|
})).pipe(
|
|
199
199
|
Effect.mapError((e) => mapApiError(e, `/pages/${id}`, id)),
|
|
200
200
|
Effect.retry(rateLimitSchedule)
|
|
@@ -312,7 +312,7 @@ const make = (
|
|
|
312
312
|
params: {
|
|
313
313
|
path: { id: Number(id) },
|
|
314
314
|
query: {
|
|
315
|
-
...(options?.includeBody ? { "body-format": "
|
|
315
|
+
...(options?.includeBody ? { "body-format": "atlas_doc_format" as const } : {}),
|
|
316
316
|
...(cursor ? { cursor } : {}),
|
|
317
317
|
limit: VERSIONS_PAGE_SIZE
|
|
318
318
|
}
|