@dogsbay/format-dita 0.2.0-beta.101
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/README.md +41 -0
- package/dist/blocks.d.ts +51 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +734 -0
- package/dist/blocks.js.map +1 -0
- package/dist/conref.d.ts +28 -0
- package/dist/conref.d.ts.map +1 -0
- package/dist/conref.js +303 -0
- package/dist/conref.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/inline.d.ts +43 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +281 -0
- package/dist/inline.js.map +1 -0
- package/dist/markdown.d.ts +12 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +72 -0
- package/dist/markdown.js.map +1 -0
- package/dist/parse.d.ts +13 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +295 -0
- package/dist/parse.js.map +1 -0
- package/dist/types.d.ts +105 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { convertInline, trimInline, textOf, inlineSafeSnippet, conrefLedgerDetail, } from "./inline.js";
|
|
2
|
+
const isElement = (node) => node.type === "tag";
|
|
3
|
+
const isNonEmptyText = (node) => node.type === "text" && node.data.trim().length > 0;
|
|
4
|
+
/** DITA note @type → callout type in the dogsbay-md vocabulary. */
|
|
5
|
+
const NOTE_TYPE_MAP = {
|
|
6
|
+
note: "note",
|
|
7
|
+
tip: "tip",
|
|
8
|
+
important: "important",
|
|
9
|
+
warning: "warning",
|
|
10
|
+
caution: "warning",
|
|
11
|
+
danger: "danger",
|
|
12
|
+
attention: "warning",
|
|
13
|
+
remember: "note",
|
|
14
|
+
restriction: "important",
|
|
15
|
+
trouble: "warning",
|
|
16
|
+
};
|
|
17
|
+
/** Containers whose children are spliced through without a wrapper node. */
|
|
18
|
+
const TRANSPARENT_CONTAINERS = new Set([
|
|
19
|
+
"body", "conbody", "refbody", "taskbody", "glossdef", "abstract", "bodydiv", "sectiondiv", "div",
|
|
20
|
+
]);
|
|
21
|
+
/** Children that carry no content — a conref shell with only these is empty. */
|
|
22
|
+
const METADATA_ONLY = new Set(["indexterm", "data", "draft-comment", "sort-as", "topicmeta"]);
|
|
23
|
+
const hasOwnContent = (el) => el.children.some((c) => (isElement(c) && !METADATA_ONLY.has(c.name.toLowerCase())) || isNonEmptyText(c));
|
|
24
|
+
/** '--' is illegal inside HTML comments (and '-->' would close early). */
|
|
25
|
+
const sanitizeCommentRef = (ref) => String(ref).replace(/-(?=-)/g, "- ");
|
|
26
|
+
const anchorHtml = (id) => `<a id="${id.replace(/"/g, """)}"></a>`;
|
|
27
|
+
/**
|
|
28
|
+
* The include directive for a snippet, wrapped in a scoped leveloffset
|
|
29
|
+
* when the snippet carries headings and the site's heading depth differs
|
|
30
|
+
* from the snippet's baked-in base level.
|
|
31
|
+
*/
|
|
32
|
+
export const includeDirective = (snippet, site, headingLevel) => {
|
|
33
|
+
const include = `{% include "${site.includePath(snippet)}" %}`;
|
|
34
|
+
if (snippet.headingBase !== undefined && headingLevel !== snippet.headingBase) {
|
|
35
|
+
const delta = headingLevel - snippet.headingBase;
|
|
36
|
+
return `{% leveloffset ${delta >= 0 ? "+" : ""}${delta} %}${include}{% endleveloffset %}`;
|
|
37
|
+
}
|
|
38
|
+
return include;
|
|
39
|
+
};
|
|
40
|
+
const withProvenance = (node, el) => {
|
|
41
|
+
const id = el.attribs?.id;
|
|
42
|
+
if (id) {
|
|
43
|
+
node.meta = { ...(node.meta ?? {}), dita: { id } };
|
|
44
|
+
}
|
|
45
|
+
return node;
|
|
46
|
+
};
|
|
47
|
+
const noteFilterAttrs = (el, ctx) => {
|
|
48
|
+
for (const attr of ["audience", "platform", "product", "props", "otherprops", "rev"]) {
|
|
49
|
+
if (el.attribs?.[attr]) {
|
|
50
|
+
ctx.note(`@${attr}`, `<${el.name}> ${attr}="${el.attribs[attr]}" — map to a profile condition (ifdef) manually`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const paragraphFrom = (nodes, ctx) => {
|
|
55
|
+
const inline = trimInline(convertInline(nodes, ctx));
|
|
56
|
+
if (inline.length === 0)
|
|
57
|
+
return null;
|
|
58
|
+
return { type: "paragraph", inline };
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* <li>/<entry>/<p>-style mixed content: a leading inline run, block
|
|
62
|
+
* children, and any inline runs BETWEEN or AFTER blocks (text and inline
|
|
63
|
+
* elements alike) wrapped as paragraphs — nothing is dropped.
|
|
64
|
+
*/
|
|
65
|
+
const mixedContent = (el, ctx) => {
|
|
66
|
+
const blockNodes = [];
|
|
67
|
+
let leadingInline;
|
|
68
|
+
let sawBlock = false;
|
|
69
|
+
let run = [];
|
|
70
|
+
const flushRun = () => {
|
|
71
|
+
if (run.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
const inline = trimInline(convertInline(run, ctx));
|
|
74
|
+
run = [];
|
|
75
|
+
if (inline.length === 0)
|
|
76
|
+
return;
|
|
77
|
+
if (!sawBlock) {
|
|
78
|
+
leadingInline = inline;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
blockNodes.push({ type: "paragraph", inline });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
for (const child of el.children) {
|
|
85
|
+
if (isElement(child) && BLOCK_ELEMENTS.has(child.name.toLowerCase())) {
|
|
86
|
+
flushRun();
|
|
87
|
+
sawBlock = true;
|
|
88
|
+
blockNodes.push(...convertBlock(child, ctx));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isElement(child) || isNonEmptyText(child)) {
|
|
92
|
+
run.push(child);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
flushRun();
|
|
96
|
+
return { inline: leadingInline, children: blockNodes };
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Fallback-content check for item-level conref shells. Unlike blocks,
|
|
100
|
+
* items routinely carry DTD-required empty children (`<step conref>
|
|
101
|
+
* <cmd/></step>`), which are placeholders, not fallback content.
|
|
102
|
+
*/
|
|
103
|
+
const itemHasFallback = (el) => el.children.some((c) => (isElement(c) &&
|
|
104
|
+
!METADATA_ONLY.has(c.name.toLowerCase()) &&
|
|
105
|
+
(c.name.toLowerCase() !== "cmd" || hasOwnContent(c))) ||
|
|
106
|
+
isNonEmptyText(c));
|
|
107
|
+
/**
|
|
108
|
+
* conref on a list item / step / substep / choice — these elements never
|
|
109
|
+
* pass through convertBlock, so without this hook a `<step conref=…/>`
|
|
110
|
+
* converted to an EMPTY step with no ledger note (silent loss). Item
|
|
111
|
+
* bodies serialize indented, so only single-line snippets can splice;
|
|
112
|
+
* everything else lands on the ledger, with a source-visible placeholder
|
|
113
|
+
* when the item has no fallback content.
|
|
114
|
+
*/
|
|
115
|
+
const itemConref = (el, ctx, type) => {
|
|
116
|
+
if (!el.attribs?.conref && !el.attribs?.conkeyref)
|
|
117
|
+
return null;
|
|
118
|
+
const name = el.name.toLowerCase();
|
|
119
|
+
const snippet = el.attribs.conref && !el.attribs.conrefend && !el.attribs.conaction
|
|
120
|
+
? ctx.conref?.resolve(el.attribs.conref)
|
|
121
|
+
: undefined;
|
|
122
|
+
if (snippet && inlineSafeSnippet(snippet)) {
|
|
123
|
+
return {
|
|
124
|
+
type,
|
|
125
|
+
inline: [{ type: "text", text: `{% include "${ctx.conref.includePath(snippet)}" %}` }],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const ref = el.attribs.conref ?? el.attribs.conkeyref;
|
|
129
|
+
ctx.note(el.attribs.conref ? "@conref" : "@conkeyref", `<${name}> pulls content from "${ref}" — ${conrefLedgerDetail(el.attribs, snippet, ctx.conref !== undefined)}`);
|
|
130
|
+
if (itemHasFallback(el))
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
type,
|
|
134
|
+
children: [
|
|
135
|
+
{
|
|
136
|
+
type: "html",
|
|
137
|
+
html: `<!-- TODO(dita-${el.attribs.conref ? "conref" : "conkeyref"}): include ${sanitizeCommentRef(ref)} -->`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* conref on a line-constrained PART (<cmd>, <dt>, <dd>, table cells,
|
|
144
|
+
* titles) — content that must land on a single output line. Returns the
|
|
145
|
+
* include as an inline run when the snippet can splice; ledgers and
|
|
146
|
+
* returns null otherwise; undefined when the part has no conref at all.
|
|
147
|
+
*/
|
|
148
|
+
const constrainedConrefInline = (el, ctx) => {
|
|
149
|
+
if (!el.attribs?.conref && !el.attribs?.conkeyref)
|
|
150
|
+
return undefined;
|
|
151
|
+
const snippet = el.attribs.conref && !el.attribs.conrefend && !el.attribs.conaction
|
|
152
|
+
? ctx.conref?.resolve(el.attribs.conref)
|
|
153
|
+
: undefined;
|
|
154
|
+
if (snippet && inlineSafeSnippet(snippet)) {
|
|
155
|
+
return [{ type: "text", text: `{% include "${ctx.conref.includePath(snippet)}" %}` }];
|
|
156
|
+
}
|
|
157
|
+
const ref = el.attribs.conref ?? el.attribs.conkeyref;
|
|
158
|
+
ctx.note(el.attribs.conref ? "@conref" : "@conkeyref", `<${el.name.toLowerCase()}> pulls content from "${ref}" — ${conrefLedgerDetail(el.attribs, snippet, ctx.conref !== undefined)}`);
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Defining site for an extracted ITEM (<li>/<step>/<sli>): items never
|
|
163
|
+
* pass through convertBlock, so without this hook the warehouse item
|
|
164
|
+
* would keep its full text next to the snippet — two divergent copies.
|
|
165
|
+
* Single-line snippets replace the item; multi-line ones are ledgered
|
|
166
|
+
* (content kept) so the duplication is at least never silent.
|
|
167
|
+
*/
|
|
168
|
+
const definingItemNode = (el, ctx, type) => {
|
|
169
|
+
if (!ctx.conref || !el.attribs?.id || el.attribs?.conref || el.attribs?.conkeyref)
|
|
170
|
+
return null;
|
|
171
|
+
const snippet = ctx.conref.definingSnippet(el.attribs.id);
|
|
172
|
+
if (!snippet)
|
|
173
|
+
return null;
|
|
174
|
+
if (inlineSafeSnippet(snippet)) {
|
|
175
|
+
return {
|
|
176
|
+
type,
|
|
177
|
+
inline: [{ type: "text", text: `{% include "${ctx.conref.includePath(snippet)}" %}` }],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
ctx.note("@conref", `<${el.name.toLowerCase()} id="${el.attribs.id}"> is reused elsewhere via "${snippet.file}" but its content is multi-line — replace it with the include manually to keep one copy`);
|
|
181
|
+
return null;
|
|
182
|
+
};
|
|
183
|
+
const convertListItems = (el, ctx, itemTag) => el.children
|
|
184
|
+
.filter((c) => isElement(c) && c.name.toLowerCase() === itemTag)
|
|
185
|
+
.map((li) => {
|
|
186
|
+
const viaConref = itemConref(li, ctx, "list-item") ?? definingItemNode(li, ctx, "list-item");
|
|
187
|
+
if (viaConref)
|
|
188
|
+
return withProvenance(viaConref, li);
|
|
189
|
+
const { inline, children } = mixedContent(li, { ...ctx, constrained: true });
|
|
190
|
+
const item = { type: "list-item" };
|
|
191
|
+
if (inline)
|
|
192
|
+
item.inline = inline;
|
|
193
|
+
if (children.length > 0)
|
|
194
|
+
item.children = children;
|
|
195
|
+
return withProvenance(item, li);
|
|
196
|
+
});
|
|
197
|
+
const convertDefList = (el, ctx, tags = { entry: "dlentry", term: "dt", def: "dd" }) => {
|
|
198
|
+
const children = [];
|
|
199
|
+
for (const entry of el.children.filter(isElement)) {
|
|
200
|
+
if (entry.name.toLowerCase() !== tags.entry)
|
|
201
|
+
continue;
|
|
202
|
+
// Entry-level conref (<dlentry conref>): a term/definition pair can't
|
|
203
|
+
// splice as an include — ledger it so the loss is never silent
|
|
204
|
+
if (entry.attribs?.conref || entry.attribs?.conkeyref) {
|
|
205
|
+
const ref = entry.attribs.conref ?? entry.attribs.conkeyref;
|
|
206
|
+
ctx.note(entry.attribs.conref ? "@conref" : "@conkeyref", `<${tags.entry}> pulls content from "${ref}" — ${conrefLedgerDetail(entry.attribs, undefined, ctx.conref !== undefined)}`);
|
|
207
|
+
}
|
|
208
|
+
for (const part of entry.children.filter(isElement)) {
|
|
209
|
+
const name = part.name.toLowerCase();
|
|
210
|
+
if (name === tags.term) {
|
|
211
|
+
// Term-level conref (<dt conref>/<pt conref>): single-line
|
|
212
|
+
// include or ledger — never a silent empty term
|
|
213
|
+
const viaConref = constrainedConrefInline(part, ctx);
|
|
214
|
+
children.push({
|
|
215
|
+
type: "dt",
|
|
216
|
+
inline: viaConref ?? trimInline(convertInline(part.children, ctx)),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (name === tags.def) {
|
|
220
|
+
const viaConref = constrainedConrefInline(part, ctx);
|
|
221
|
+
if (viaConref) {
|
|
222
|
+
children.push({ type: "dd", inline: viaConref });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const { inline, children: blocks } = mixedContent(part, { ...ctx, constrained: true });
|
|
226
|
+
const dd = { type: "dd" };
|
|
227
|
+
if (inline)
|
|
228
|
+
dd.inline = inline;
|
|
229
|
+
if (blocks.length > 0)
|
|
230
|
+
dd.children = blocks;
|
|
231
|
+
children.push(dd);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { type: "deflist", children };
|
|
236
|
+
};
|
|
237
|
+
/** CALS <table> and <simpletable> → table/tr/td tree. */
|
|
238
|
+
const convertTable = (el, ctx) => {
|
|
239
|
+
const rows = [];
|
|
240
|
+
const headRows = [];
|
|
241
|
+
const convertRow = (rowEl, cellTags) => {
|
|
242
|
+
// Row-level conref: a whole row can't splice as an include — ledger it
|
|
243
|
+
if (rowEl.attribs?.conref || rowEl.attribs?.conkeyref) {
|
|
244
|
+
const ref = rowEl.attribs.conref ?? rowEl.attribs.conkeyref;
|
|
245
|
+
ctx.note(rowEl.attribs.conref ? "@conref" : "@conkeyref", `<${rowEl.name.toLowerCase()}> pulls content from "${ref}" — ${conrefLedgerDetail(rowEl.attribs, undefined, ctx.conref !== undefined)}`);
|
|
246
|
+
}
|
|
247
|
+
const cells = rowEl.children
|
|
248
|
+
.filter((c) => isElement(c) && cellTags.has(c.name.toLowerCase()))
|
|
249
|
+
.map((cellEl) => {
|
|
250
|
+
for (const attr of ["namest", "nameend", "morerows"]) {
|
|
251
|
+
if (cellEl.attribs?.[attr]) {
|
|
252
|
+
ctx.note(`@${attr}`, "table cell spans are not converted — cell emitted without span");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Cell-level conref: single-line snippets splice as an inline
|
|
256
|
+
// include; anything else is ledgered
|
|
257
|
+
const cellConref = constrainedConrefInline(cellEl, ctx);
|
|
258
|
+
if (cellConref) {
|
|
259
|
+
return {
|
|
260
|
+
type: "td",
|
|
261
|
+
children: [{ type: "paragraph", inline: cellConref }],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Cell content always lives in children (leading inline wrapped as a
|
|
265
|
+
// paragraph): the serializer's HTML-table path reads only children,
|
|
266
|
+
// and its pipe path reads children paragraphs too
|
|
267
|
+
const { inline, children } = mixedContent(cellEl, { ...ctx, constrained: true });
|
|
268
|
+
const cellChildren = [];
|
|
269
|
+
if (inline)
|
|
270
|
+
cellChildren.push({ type: "paragraph", inline });
|
|
271
|
+
cellChildren.push(...children);
|
|
272
|
+
const cell = { type: "td" };
|
|
273
|
+
if (cellChildren.length > 0)
|
|
274
|
+
cell.children = cellChildren;
|
|
275
|
+
return cell;
|
|
276
|
+
});
|
|
277
|
+
return { type: "tr", children: cells };
|
|
278
|
+
};
|
|
279
|
+
const CALS_CELLS = new Set(["entry"]);
|
|
280
|
+
const SIMPLE_CELLS = new Set(["stentry"]);
|
|
281
|
+
const CHOICE_HEAD_CELLS = new Set(["choptionhd", "chdeschd"]);
|
|
282
|
+
const CHOICE_ROW_CELLS = new Set(["choption", "chdesc"]);
|
|
283
|
+
const walk = (node) => {
|
|
284
|
+
for (const child of node.children.filter(isElement)) {
|
|
285
|
+
const name = child.name.toLowerCase();
|
|
286
|
+
// Row-set reuse (conref on tgroup/thead/tbody) can't splice into a
|
|
287
|
+
// markdown table — ledger it so the pulled rows are never silently
|
|
288
|
+
// an empty table body
|
|
289
|
+
if ((name === "tgroup" || name === "thead" || name === "tbody") &&
|
|
290
|
+
(child.attribs?.conref || child.attribs?.conkeyref)) {
|
|
291
|
+
const ref = child.attribs.conref ?? child.attribs.conkeyref;
|
|
292
|
+
ctx.note(child.attribs.conref ? "@conref" : "@conkeyref", `<${name}> pulls content from "${ref}" — ${conrefLedgerDetail(child.attribs, undefined, ctx.conref !== undefined)}`);
|
|
293
|
+
}
|
|
294
|
+
if (name === "tgroup")
|
|
295
|
+
walk(child);
|
|
296
|
+
else if (name === "thead") {
|
|
297
|
+
for (const row of child.children.filter(isElement)) {
|
|
298
|
+
if (row.name.toLowerCase() === "row")
|
|
299
|
+
headRows.push(convertRow(row, CALS_CELLS));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (name === "tbody") {
|
|
303
|
+
for (const row of child.children.filter(isElement)) {
|
|
304
|
+
if (row.name.toLowerCase() === "row")
|
|
305
|
+
rows.push(convertRow(row, CALS_CELLS));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (name === "sthead") {
|
|
309
|
+
headRows.push(convertRow(child, SIMPLE_CELLS));
|
|
310
|
+
}
|
|
311
|
+
else if (name === "strow") {
|
|
312
|
+
rows.push(convertRow(child, SIMPLE_CELLS));
|
|
313
|
+
}
|
|
314
|
+
else if (name === "chhead") {
|
|
315
|
+
headRows.push(convertRow(child, CHOICE_HEAD_CELLS));
|
|
316
|
+
}
|
|
317
|
+
else if (name === "chrow") {
|
|
318
|
+
rows.push(convertRow(child, CHOICE_ROW_CELLS));
|
|
319
|
+
}
|
|
320
|
+
else if (name === "colspec") {
|
|
321
|
+
// Column widths are presentation-only; dropped silently
|
|
322
|
+
}
|
|
323
|
+
else if (name === "title") {
|
|
324
|
+
// handled below via caption
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
walk(el);
|
|
329
|
+
const table = {
|
|
330
|
+
type: "table",
|
|
331
|
+
children: [
|
|
332
|
+
...(headRows.length > 0 ? [{ type: "thead", children: headRows }] : []),
|
|
333
|
+
{ type: "tbody", children: rows },
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
const titleEl = el.children
|
|
337
|
+
.filter(isElement)
|
|
338
|
+
.find((c) => c.name.toLowerCase() === "title");
|
|
339
|
+
if (titleEl) {
|
|
340
|
+
// Caption is a plain-string prop — a conref'd title can't carry an
|
|
341
|
+
// include directive, so ledger it instead of emitting empty text
|
|
342
|
+
if (titleEl.attribs?.conref || titleEl.attribs?.conkeyref) {
|
|
343
|
+
const ref = titleEl.attribs.conref ?? titleEl.attribs.conkeyref;
|
|
344
|
+
ctx.note(titleEl.attribs.conref ? "@conref" : "@conkeyref", `<title> (table caption) pulls content from "${ref}" — ${conrefLedgerDetail(titleEl.attribs, undefined, ctx.conref !== undefined)}`);
|
|
345
|
+
}
|
|
346
|
+
table.props = { caption: textOf(titleEl.children).trim() };
|
|
347
|
+
}
|
|
348
|
+
return table;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* One <step>/<substep>'s content: <cmd> as the leading inline run, the
|
|
352
|
+
* rest as block children. Shared by convertSteps (constrained context)
|
|
353
|
+
* and conref snippet extraction (standalone, unconstrained).
|
|
354
|
+
*/
|
|
355
|
+
export const convertStepContent = (stepEl, ctx) => {
|
|
356
|
+
let stepInline;
|
|
357
|
+
const blocks = [];
|
|
358
|
+
for (const part of stepEl.children.filter(isElement)) {
|
|
359
|
+
const name = part.name.toLowerCase();
|
|
360
|
+
if (name === "cmd") {
|
|
361
|
+
// <cmd conref=…/> is the standard reused-command pattern: include
|
|
362
|
+
// when spliceable, ledger otherwise — never a silent empty step
|
|
363
|
+
const viaConref = constrainedConrefInline(part, ctx);
|
|
364
|
+
if (viaConref) {
|
|
365
|
+
stepInline = viaConref;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
stepInline = trimInline(convertInline(part.children, ctx));
|
|
369
|
+
}
|
|
370
|
+
else if (name === "info" || name === "stepresult" || name === "stepxmp" || name === "steptroubleshooting") {
|
|
371
|
+
const { inline, children: inner } = mixedContent(part, ctx);
|
|
372
|
+
if (inline)
|
|
373
|
+
blocks.push({ type: "paragraph", inline });
|
|
374
|
+
blocks.push(...inner);
|
|
375
|
+
}
|
|
376
|
+
else if (name === "substeps") {
|
|
377
|
+
blocks.push(convertSteps(part, ctx, "substeps"));
|
|
378
|
+
}
|
|
379
|
+
else if (name === "choices" || name === "choicetable") {
|
|
380
|
+
ctx.note(`<${name}>`, "step choices converted as a plain list/table");
|
|
381
|
+
blocks.push(...convertBlock(part, ctx));
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
blocks.push(...convertBlock(part, ctx));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { inline: stepInline, children: blocks };
|
|
388
|
+
};
|
|
389
|
+
/** Task <steps>/<substeps> → steps/substeps nodes with step children. */
|
|
390
|
+
const convertSteps = (el, ctx, type) => {
|
|
391
|
+
const stepTag = type === "steps" ? "step" : "substep";
|
|
392
|
+
const children = el.children
|
|
393
|
+
.filter((c) => isElement(c) && c.name.toLowerCase() === stepTag)
|
|
394
|
+
.map((stepEl) => {
|
|
395
|
+
const viaConref = itemConref(stepEl, ctx, "step") ?? definingItemNode(stepEl, ctx, "step");
|
|
396
|
+
if (viaConref)
|
|
397
|
+
return withProvenance(viaConref, stepEl);
|
|
398
|
+
const { inline, children: blocks } = convertStepContent(stepEl, { ...ctx, constrained: true });
|
|
399
|
+
const step = { type: "step" };
|
|
400
|
+
if (inline)
|
|
401
|
+
step.inline = inline;
|
|
402
|
+
if (blocks.length > 0)
|
|
403
|
+
step.children = blocks;
|
|
404
|
+
return withProvenance(step, stepEl);
|
|
405
|
+
});
|
|
406
|
+
return { type, children };
|
|
407
|
+
};
|
|
408
|
+
/**
|
|
409
|
+
* Standalone content of an item-level element (<li>, <step>, …) for
|
|
410
|
+
* conref snippet extraction: leading inline as a paragraph, block
|
|
411
|
+
* children after.
|
|
412
|
+
*/
|
|
413
|
+
export const convertItemContent = (el, ctx) => {
|
|
414
|
+
const name = el.name.toLowerCase();
|
|
415
|
+
const { inline, children } = name === "step" || name === "substep" ? convertStepContent(el, ctx) : mixedContent(el, ctx);
|
|
416
|
+
const out = [];
|
|
417
|
+
if (inline && inline.length > 0)
|
|
418
|
+
out.push({ type: "paragraph", inline });
|
|
419
|
+
out.push(...children);
|
|
420
|
+
return out;
|
|
421
|
+
};
|
|
422
|
+
/** Task sections that render as a heading + content. */
|
|
423
|
+
const TASK_SECTION_HEADINGS = {
|
|
424
|
+
prereq: "Before you begin",
|
|
425
|
+
result: "Result",
|
|
426
|
+
postreq: "What to do next",
|
|
427
|
+
tasktroubleshooting: "Troubleshooting",
|
|
428
|
+
};
|
|
429
|
+
/** Block-level DITA elements (used to split mixed content). */
|
|
430
|
+
const BLOCK_ELEMENTS = new Set([
|
|
431
|
+
"p", "shortdesc", "note", "codeblock", "pre", "screen", "msgblock", "ul", "ol", "sl", "dl", "parml", "table", "simpletable",
|
|
432
|
+
"fig", "image", "section", "example", "steps", "steps-unordered", "substeps", "lq", "object",
|
|
433
|
+
"prereq", "context", "result", "postreq", "tasktroubleshooting", "choices", "choicetable",
|
|
434
|
+
]);
|
|
435
|
+
/** Whether an element name converts as a standalone block (conref-extractable). */
|
|
436
|
+
export const isBlockTarget = (name) => BLOCK_ELEMENTS.has(name);
|
|
437
|
+
export function convertBlock(el, ctx) {
|
|
438
|
+
const name = el.name.toLowerCase();
|
|
439
|
+
noteFilterAttrs(el, ctx);
|
|
440
|
+
// Defining site: this element was extracted into a reuse snippet — emit
|
|
441
|
+
// the include here too, so exactly ONE copy of the text survives
|
|
442
|
+
// migration (the DITA warehouse copy and every referent share the
|
|
443
|
+
// snippet). The element's @id must survive as a real anchor unless the
|
|
444
|
+
// snippet itself renders one (section/example heading {#id}).
|
|
445
|
+
if (ctx.conref && el.attribs?.id && !el.attribs?.conref && !el.attribs?.conkeyref) {
|
|
446
|
+
const defining = ctx.conref.definingSnippet(el.attribs.id);
|
|
447
|
+
if (defining && (!ctx.constrained || inlineSafeSnippet(defining))) {
|
|
448
|
+
const anchor = defining.carriesAnchor ? "" : anchorHtml(el.attribs.id);
|
|
449
|
+
return [
|
|
450
|
+
withProvenance({ type: "html", html: `${anchor}${includeDirective(defining, ctx.conref, ctx.headingLevel)}` }, el),
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
if (defining) {
|
|
454
|
+
ctx.note("@conref", `<${name} id="${el.attribs.id}"> is reused elsewhere via "${defining.file}" but sits in a list/step/table cell — replace its content with the include manually`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (el.attribs?.conref || el.attribs?.conkeyref) {
|
|
458
|
+
const ref = el.attribs.conref ?? el.attribs.conkeyref;
|
|
459
|
+
const snippet = el.attribs.conref && !el.attribs.conrefend && !el.attribs.conaction
|
|
460
|
+
? ctx.conref?.resolve(el.attribs.conref)
|
|
461
|
+
: undefined;
|
|
462
|
+
if (snippet && (!ctx.constrained || inlineSafeSnippet(snippet))) {
|
|
463
|
+
// Local children are the stale preview copy in DITA conref
|
|
464
|
+
// semantics — the include replaces the whole element. The
|
|
465
|
+
// referencing element's own @id (if any) still needs an anchor:
|
|
466
|
+
// links may target the referencing shell, not the snippet source.
|
|
467
|
+
const anchor = el.attribs?.id ? anchorHtml(el.attribs.id) : "";
|
|
468
|
+
return [
|
|
469
|
+
withProvenance({ type: "html", html: `${anchor}${includeDirective(snippet, ctx.conref, ctx.headingLevel)}` }, el),
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
ctx.note(el.attribs.conref ? "@conref" : "@conkeyref", `<${name}> pulls content from "${ref}" — ${conrefLedgerDetail(el.attribs, snippet, ctx.conref !== undefined)}`);
|
|
473
|
+
// Content-less conref shells would render as empty blocks (an empty
|
|
474
|
+
// callout, an empty paragraph). Emit a visible source-level placeholder
|
|
475
|
+
// instead: invisible when rendered, obvious to the migrating writer.
|
|
476
|
+
// Metadata-only children (indexterm, data, …) convert to nothing, so
|
|
477
|
+
// they don't count as content; the @id (if any) must survive as both
|
|
478
|
+
// provenance and a real anchor, since links may target the shell.
|
|
479
|
+
if (!hasOwnContent(el)) {
|
|
480
|
+
const anchor = el.attribs?.id ? anchorHtml(el.attribs.id) : "";
|
|
481
|
+
return [
|
|
482
|
+
withProvenance({
|
|
483
|
+
type: "html",
|
|
484
|
+
html: `${anchor}<!-- TODO(dita-${el.attribs.conref ? "conref" : "conkeyref"}): include ${sanitizeCommentRef(ref)} -->`,
|
|
485
|
+
}, el),
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (TRANSPARENT_CONTAINERS.has(name)) {
|
|
490
|
+
return convertChildren(el, ctx);
|
|
491
|
+
}
|
|
492
|
+
switch (name) {
|
|
493
|
+
case "p":
|
|
494
|
+
case "shortdesc": {
|
|
495
|
+
// DITA allows blocks (ul, note, pre, …) inside <p>: leading inline
|
|
496
|
+
// becomes the paragraph, block children follow as siblings
|
|
497
|
+
const { inline, children } = mixedContent(el, ctx);
|
|
498
|
+
const out = [];
|
|
499
|
+
if (inline)
|
|
500
|
+
out.push({ type: "paragraph", inline });
|
|
501
|
+
out.push(...children);
|
|
502
|
+
if (out.length > 0 && name === "p")
|
|
503
|
+
withProvenance(out[0], el);
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
case "note": {
|
|
507
|
+
const type = NOTE_TYPE_MAP[el.attribs?.type ?? "note"] ?? "note";
|
|
508
|
+
if (el.attribs?.type && !NOTE_TYPE_MAP[el.attribs.type]) {
|
|
509
|
+
ctx.note("@type", `<note type="${el.attribs.type}"> mapped to plain note callout`);
|
|
510
|
+
}
|
|
511
|
+
// Callouts serialize in the GitHub `> [!TYPE]` per-line-prefixed
|
|
512
|
+
// form — a constrained context for includes
|
|
513
|
+
const { inline, children } = mixedContent(el, { ...ctx, constrained: true });
|
|
514
|
+
const body = [];
|
|
515
|
+
if (inline)
|
|
516
|
+
body.push({ type: "paragraph", inline });
|
|
517
|
+
body.push(...children);
|
|
518
|
+
return [withProvenance({ type: "callout", props: { type }, children: body }, el)];
|
|
519
|
+
}
|
|
520
|
+
case "codeblock":
|
|
521
|
+
case "pre":
|
|
522
|
+
case "screen":
|
|
523
|
+
case "msgblock": {
|
|
524
|
+
const language = el.attribs?.outputclass ?? "";
|
|
525
|
+
const content = textOf(el.children).replace(/^\n/, "").replace(/\s+$/, "");
|
|
526
|
+
// Content in BOTH html and props.code: the serializer's block path
|
|
527
|
+
// reads html, its HTML-table path (code inside cells) reads props.code
|
|
528
|
+
const node = {
|
|
529
|
+
type: "code",
|
|
530
|
+
html: content,
|
|
531
|
+
props: { code: content, ...(language ? { language } : {}) },
|
|
532
|
+
};
|
|
533
|
+
return [withProvenance(node, el)];
|
|
534
|
+
}
|
|
535
|
+
case "ul":
|
|
536
|
+
return [withProvenance({ type: "unordered-list", children: convertListItems(el, ctx, "li") }, el)];
|
|
537
|
+
case "ol":
|
|
538
|
+
return [withProvenance({ type: "ordered-list", children: convertListItems(el, ctx, "li") }, el)];
|
|
539
|
+
case "sl": {
|
|
540
|
+
// Simple list → unordered list of <sli> items (same conref hooks
|
|
541
|
+
// as <li>: referencing items include or ledger, defining items
|
|
542
|
+
// hand their text to the snippet)
|
|
543
|
+
const items = el.children
|
|
544
|
+
.filter((c) => isElement(c) && c.name.toLowerCase() === "sli")
|
|
545
|
+
.map((sli) => {
|
|
546
|
+
const viaConref = itemConref(sli, ctx, "list-item") ?? definingItemNode(sli, ctx, "list-item");
|
|
547
|
+
if (viaConref)
|
|
548
|
+
return withProvenance(viaConref, sli);
|
|
549
|
+
const inline = trimInline(convertInline(sli.children, ctx));
|
|
550
|
+
return { type: "list-item", ...(inline.length > 0 ? { inline } : {}) };
|
|
551
|
+
});
|
|
552
|
+
return [withProvenance({ type: "unordered-list", children: items }, el)];
|
|
553
|
+
}
|
|
554
|
+
case "dl":
|
|
555
|
+
return [withProvenance(convertDefList(el, ctx), el)];
|
|
556
|
+
case "parml":
|
|
557
|
+
// Parameter list — structurally a definition list
|
|
558
|
+
return [withProvenance(convertDefList(el, ctx, { entry: "plentry", term: "pt", def: "pd" }), el)];
|
|
559
|
+
case "table":
|
|
560
|
+
case "simpletable":
|
|
561
|
+
return [withProvenance(convertTable(el, ctx), el)];
|
|
562
|
+
case "fig": {
|
|
563
|
+
const titleEl = el.children
|
|
564
|
+
.filter(isElement)
|
|
565
|
+
.find((c) => c.name.toLowerCase() === "title");
|
|
566
|
+
const imageEl = el.children
|
|
567
|
+
.filter(isElement)
|
|
568
|
+
.find((c) => c.name.toLowerCase() === "image");
|
|
569
|
+
if (imageEl) {
|
|
570
|
+
const figure = {
|
|
571
|
+
type: "figure",
|
|
572
|
+
children: [
|
|
573
|
+
{
|
|
574
|
+
type: "image",
|
|
575
|
+
props: {
|
|
576
|
+
src: imageEl.attribs?.href ?? "",
|
|
577
|
+
alt: imageEl.attribs?.alt ??
|
|
578
|
+
textOf(imageEl.children).trim() ??
|
|
579
|
+
"",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
...(titleEl
|
|
584
|
+
? { props: { caption: textOf(titleEl.children).trim() } }
|
|
585
|
+
: {}),
|
|
586
|
+
};
|
|
587
|
+
return [withProvenance(figure, el)];
|
|
588
|
+
}
|
|
589
|
+
// fig without image: treat as transparent container (keep the title as bold para)
|
|
590
|
+
const parts = [];
|
|
591
|
+
if (titleEl) {
|
|
592
|
+
parts.push({
|
|
593
|
+
type: "paragraph",
|
|
594
|
+
inline: [{ type: "text", text: textOf(titleEl.children).trim(), bold: true }],
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
parts.push(...convertChildren(el, ctx, new Set(["title"])));
|
|
598
|
+
return parts;
|
|
599
|
+
}
|
|
600
|
+
case "image": {
|
|
601
|
+
const src = el.attribs?.href;
|
|
602
|
+
if (!src) {
|
|
603
|
+
// Same contract as the inline path: no silent src:"" images
|
|
604
|
+
if (el.attribs?.keyref) {
|
|
605
|
+
ctx.note("@keyref", `<image keyref="${el.attribs.keyref}"/> — key-based images cannot be resolved at conversion; insert the image manually`);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
ctx.note("<image>", "image without href dropped");
|
|
609
|
+
}
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const node = {
|
|
613
|
+
type: "image",
|
|
614
|
+
props: {
|
|
615
|
+
src,
|
|
616
|
+
alt: el.attribs?.alt ?? textOf(el.children).trim(),
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
return [withProvenance(node, el)];
|
|
620
|
+
}
|
|
621
|
+
case "section":
|
|
622
|
+
case "example": {
|
|
623
|
+
const out = [];
|
|
624
|
+
const titleEl = el.children
|
|
625
|
+
.filter(isElement)
|
|
626
|
+
.find((c) => c.name.toLowerCase() === "title");
|
|
627
|
+
if (titleEl) {
|
|
628
|
+
// <title conref=…/> — heading text pulled from elsewhere
|
|
629
|
+
const titleConref = constrainedConrefInline(titleEl, ctx);
|
|
630
|
+
out.push(withProvenance({
|
|
631
|
+
type: "heading",
|
|
632
|
+
// @id also as props.id so the serializer emits a {#id} anchor —
|
|
633
|
+
// cross-topic xref fragments (renamed to #elementid) must land
|
|
634
|
+
props: {
|
|
635
|
+
level: ctx.headingLevel,
|
|
636
|
+
...(el.attribs?.id ? { id: el.attribs.id } : {}),
|
|
637
|
+
},
|
|
638
|
+
inline: titleConref ?? trimInline(convertInline(titleEl.children, ctx)),
|
|
639
|
+
}, el));
|
|
640
|
+
}
|
|
641
|
+
else if (name === "example") {
|
|
642
|
+
out.push({
|
|
643
|
+
type: "heading",
|
|
644
|
+
props: { level: ctx.headingLevel },
|
|
645
|
+
inline: [{ type: "text", text: "Example" }],
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const inner = { ...ctx, headingLevel: Math.min(6, ctx.headingLevel + 1) };
|
|
649
|
+
out.push(...convertChildren(el, inner, new Set(["title"])));
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
case "steps":
|
|
653
|
+
case "steps-unordered":
|
|
654
|
+
return [withProvenance(convertSteps(el, ctx, "steps"), el)];
|
|
655
|
+
case "substeps":
|
|
656
|
+
return [withProvenance(convertSteps(el, ctx, "substeps"), el)];
|
|
657
|
+
case "context":
|
|
658
|
+
return convertChildren(el, ctx);
|
|
659
|
+
case "prereq":
|
|
660
|
+
case "result":
|
|
661
|
+
case "postreq":
|
|
662
|
+
case "tasktroubleshooting": {
|
|
663
|
+
const heading = {
|
|
664
|
+
type: "heading",
|
|
665
|
+
props: { level: ctx.headingLevel },
|
|
666
|
+
inline: [{ type: "text", text: TASK_SECTION_HEADINGS[name] }],
|
|
667
|
+
};
|
|
668
|
+
return [heading, ...convertChildren(el, ctx)];
|
|
669
|
+
}
|
|
670
|
+
case "choices":
|
|
671
|
+
return [{ type: "unordered-list", children: convertListItems(el, ctx, "choice") }];
|
|
672
|
+
case "choicetable":
|
|
673
|
+
return [withProvenance(convertTable(el, ctx), el)];
|
|
674
|
+
case "lq": {
|
|
675
|
+
// Blockquotes serialize `> `-prefixed — constrained for includes
|
|
676
|
+
const { inline, children } = mixedContent(el, { ...ctx, constrained: true });
|
|
677
|
+
const body = [];
|
|
678
|
+
if (inline)
|
|
679
|
+
body.push({ type: "paragraph", inline });
|
|
680
|
+
body.push(...children);
|
|
681
|
+
return [{ type: "blockquote", children: body }];
|
|
682
|
+
}
|
|
683
|
+
case "title":
|
|
684
|
+
// Titles are handled by their parents (topic/section/fig/table)
|
|
685
|
+
return [];
|
|
686
|
+
case "prolog":
|
|
687
|
+
case "related-links":
|
|
688
|
+
ctx.note(`<${name}>`, "metadata/related-links dropped — links belong in the topic map (reltable design pending)");
|
|
689
|
+
return [];
|
|
690
|
+
case "draft-comment":
|
|
691
|
+
case "data":
|
|
692
|
+
case "indexterm":
|
|
693
|
+
return [];
|
|
694
|
+
default: {
|
|
695
|
+
// Unknown block: convert children if any survive, note the loss
|
|
696
|
+
const children = convertChildren(el, ctx);
|
|
697
|
+
ctx.note(`<${name}>`, children.length > 0
|
|
698
|
+
? "unrecognized block element — children kept, wrapper dropped"
|
|
699
|
+
: "unrecognized block element dropped");
|
|
700
|
+
return children;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
export function convertChildren(el, ctx, skip = new Set()) {
|
|
705
|
+
const out = [];
|
|
706
|
+
let inlineRun = [];
|
|
707
|
+
const flushInline = () => {
|
|
708
|
+
const para = paragraphFrom(inlineRun, ctx);
|
|
709
|
+
if (para)
|
|
710
|
+
out.push(para);
|
|
711
|
+
inlineRun = [];
|
|
712
|
+
};
|
|
713
|
+
for (const child of el.children) {
|
|
714
|
+
if (isElement(child)) {
|
|
715
|
+
const name = child.name.toLowerCase();
|
|
716
|
+
if (skip.has(name))
|
|
717
|
+
continue;
|
|
718
|
+
if (BLOCK_ELEMENTS.has(name) || TRANSPARENT_CONTAINERS.has(name) || name === "title" ||
|
|
719
|
+
name === "prolog" || name === "related-links" || name === "draft-comment") {
|
|
720
|
+
flushInline();
|
|
721
|
+
out.push(...convertBlock(child, ctx));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
// Inline element loose in a body: accumulate into a paragraph
|
|
725
|
+
inlineRun.push(child);
|
|
726
|
+
}
|
|
727
|
+
else if (isNonEmptyText(child)) {
|
|
728
|
+
inlineRun.push(child);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
flushInline();
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
//# sourceMappingURL=blocks.js.map
|