@dogsbay/format-dita 0.2.0-beta.121 → 0.2.0-beta.123
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 +6 -0
- package/dist/anchors.d.ts +0 -1
- package/dist/anchors.js +0 -1
- package/dist/blocks.d.ts +8 -3
- package/dist/blocks.js +525 -17
- package/dist/conref.d.ts +0 -1
- package/dist/conref.js +1 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/inline.d.ts +0 -9
- package/dist/inline.js +87 -10
- package/dist/markdown.d.ts +0 -1
- package/dist/markdown.js +1 -2
- package/dist/parse.d.ts +0 -1
- package/dist/parse.js +11 -1
- package/dist/publish-support.d.ts +0 -1
- package/dist/publish-support.js +0 -1
- package/dist/qualifiers.d.ts +25 -0
- package/dist/qualifiers.js +40 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/well-formed.d.ts +14 -0
- package/dist/well-formed.js +110 -0
- package/package.json +5 -4
- package/dist/anchors.d.ts.map +0 -1
- package/dist/anchors.js.map +0 -1
- package/dist/blocks.d.ts.map +0 -1
- package/dist/blocks.js.map +0 -1
- package/dist/conref.d.ts.map +0 -1
- package/dist/conref.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/markdown.d.ts.map +0 -1
- package/dist/markdown.js.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/parse.js.map +0 -1
- package/dist/publish-support.d.ts.map +0 -1
- package/dist/publish-support.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/blocks.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { inlineToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
1
|
+
import { inlineToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
|
|
2
2
|
import { convertInline, elementName, idAnchor, publishImage, trimInline, textOf, inlineSafeSnippet, conrefLedgerDetail, } from "./inline.js";
|
|
3
3
|
import { publishHref, publishLabel, publishLanguage, publishString } from "./types.js";
|
|
4
4
|
import { anchorId } from "./anchors.js";
|
|
5
|
+
import { DB_QUALIFIERS, outputTokens, youtubeId } from "./qualifiers.js";
|
|
5
6
|
const isElement = (node) => node.type === "tag";
|
|
6
7
|
const isNonEmptyText = (node) => node.type === "text" && node.data.trim().length > 0;
|
|
7
8
|
/** DITA note @type → callout type in the dogsbay-md vocabulary. */
|
|
@@ -224,6 +225,415 @@ const convertListItems = (el, ctx, itemTag) => el.children
|
|
|
224
225
|
item.children = children;
|
|
225
226
|
return withProvenance(item, li);
|
|
226
227
|
});
|
|
228
|
+
/** Components an `@outputclass` `db-*` token selects, per element. */
|
|
229
|
+
const DB_COMPONENTS = {
|
|
230
|
+
dl: ["db-tabs", "db-accordion", "db-cards"],
|
|
231
|
+
section: ["db-details"],
|
|
232
|
+
// A section cannot nest, and cannot hold one: these can, so a details or
|
|
233
|
+
// sheet goes inside a section (or another div) through them
|
|
234
|
+
div: ["db-details", "db-grid"],
|
|
235
|
+
sectiondiv: ["db-details", "db-grid"],
|
|
236
|
+
bodydiv: ["db-details", "db-grid"],
|
|
237
|
+
ul: ["db-files"],
|
|
238
|
+
ol: ["db-steps"],
|
|
239
|
+
// Alone in a <p>: a block component (xrefComponent)
|
|
240
|
+
xref: ["db-link-card", "db-button", "db-video"],
|
|
241
|
+
};
|
|
242
|
+
/** Elements without a <title>: a `db-details` one takes it from a leading `<p outputclass="db-title">`. */
|
|
243
|
+
const UNTITLED_DETAILS = new Set(["div", "sectiondiv", "bodydiv"]);
|
|
244
|
+
/**
|
|
245
|
+
* A `db-details` element as a collapsible block. The modifiers: `sheet` opens
|
|
246
|
+
* it as a side sheet (`drawer`, the first name, is accepted with a note);
|
|
247
|
+
* other tokens are classes. The element's id stays on the block.
|
|
248
|
+
*/
|
|
249
|
+
const toDetails = (el, ctx, classes, title, skip, titleId) => {
|
|
250
|
+
const props = { title: title || "Details" };
|
|
251
|
+
// The title paragraph's id (a cross-reference to it) lands on the block,
|
|
252
|
+
// or, when the block has its own, as an anchor at the start of its body
|
|
253
|
+
const ownId = el.attribs?.id ?? titleId;
|
|
254
|
+
if (ownId)
|
|
255
|
+
props.id = anchorId(ctx, ownId);
|
|
256
|
+
const q = qualify("db-details", classes, ctx);
|
|
257
|
+
Object.assign(props, q.props);
|
|
258
|
+
const tokens = (q.classes ?? "").split(" ").filter(Boolean);
|
|
259
|
+
if (tokens.includes("drawer"))
|
|
260
|
+
ctx.note("@outputclass", `"drawer" on db-details is now "sheet" — converted as a sheet`);
|
|
261
|
+
if (tokens.includes("sheet") || tokens.includes("drawer"))
|
|
262
|
+
props.sheet = true;
|
|
263
|
+
const rest = tokens.filter((c) => c !== "sheet" && c !== "drawer");
|
|
264
|
+
if (rest.length)
|
|
265
|
+
props.class = rest.join(" ");
|
|
266
|
+
const inner = { ...ctx, headingLevel: Math.min(6, ctx.headingLevel + 1) };
|
|
267
|
+
let children = convertChildren(el, inner, skip);
|
|
268
|
+
if (titleId && el.attribs?.id) {
|
|
269
|
+
const anchors = anchorNodes([titleId], ctx);
|
|
270
|
+
if (anchors.length)
|
|
271
|
+
children = prependToFirstParagraph(children, anchors) ?? [{ type: "paragraph", inline: anchors }, ...children];
|
|
272
|
+
}
|
|
273
|
+
if (children.length === 0)
|
|
274
|
+
ctx.note("@outputclass", `db-details "${props.title}" has no content`);
|
|
275
|
+
return withProvenance({ type: "details", props, children }, el);
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* `<div outputclass="db-grid cols-3">`: each child `<div>` is a cell (its
|
|
279
|
+
* `span-N` token spans columns); any other child block is a cell of its own.
|
|
280
|
+
* DITA-OT shows the cells one after another.
|
|
281
|
+
*/
|
|
282
|
+
const toGrid = (el, ctx, classes) => {
|
|
283
|
+
const q = qualify("db-grid", classes, ctx);
|
|
284
|
+
const props = { ...q.props, ...(q.classes ? { class: q.classes } : {}) };
|
|
285
|
+
if (el.attribs?.id)
|
|
286
|
+
props.id = anchorId(ctx, el.attribs.id);
|
|
287
|
+
const cells = [];
|
|
288
|
+
for (const child of el.children) {
|
|
289
|
+
if (!isElement(child)) {
|
|
290
|
+
if (isNonEmptyText(child))
|
|
291
|
+
cells.push({ type: "grid-item", children: [{ type: "paragraph", inline: convertInline([child], ctx) }] });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (blockName(child, ctx) === "div") {
|
|
295
|
+
const cellProps = {};
|
|
296
|
+
const rest = [];
|
|
297
|
+
for (const t of outputTokens(child.attribs?.outputclass)) {
|
|
298
|
+
const span = /^span-(\d+)$/.exec(t);
|
|
299
|
+
if (span && DB_QUALIFIERS.gridColumns.includes(Number(span[1])))
|
|
300
|
+
cellProps.span = Number(span[1]);
|
|
301
|
+
else if (/^[a-z0-9][a-z0-9-]*$/.test(t) && !t.startsWith("db-"))
|
|
302
|
+
rest.push(t);
|
|
303
|
+
else
|
|
304
|
+
ctx.note("@outputclass", `"${t}" on a db-grid cell ignored: a cell takes span-2 to span-6`);
|
|
305
|
+
}
|
|
306
|
+
if (rest.length)
|
|
307
|
+
cellProps.class = rest.join(" ");
|
|
308
|
+
if (child.attribs?.id)
|
|
309
|
+
cellProps.id = anchorId(ctx, child.attribs.id);
|
|
310
|
+
cells.push(withProvenance({ type: "grid-item", ...(Object.keys(cellProps).length ? { props: cellProps } : {}), children: convertChildren(child, ctx) }, child));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const blocks = convertBlock(child, ctx);
|
|
314
|
+
if (blocks.length)
|
|
315
|
+
cells.push({ type: "grid-item", children: blocks });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!cells.length)
|
|
319
|
+
ctx.note("@outputclass", `db-grid has no cells`);
|
|
320
|
+
return withProvenance({ type: "grid", props, children: cells }, el);
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* A `<p>` holding only an `<xref outputclass="db-link-card|db-button|db-video">`
|
|
324
|
+
* (and whitespace): the block component. The href resolves exactly as the
|
|
325
|
+
* xref's would (rename, fragment, key, publish paths), because the link is
|
|
326
|
+
* converted first and read back. Undefined when the paragraph is anything
|
|
327
|
+
* else; an xref component beside other text stays a link, with a note.
|
|
328
|
+
*/
|
|
329
|
+
const xrefComponent = (p, ctx) => {
|
|
330
|
+
const kids = p.children.filter((c) => isElement(c) || isNonEmptyText(c));
|
|
331
|
+
const xrefs = p.children.filter((c) => isElement(c) && c.name.toLowerCase() === "xref" && /(^|\s)db-/.test(c.attribs?.outputclass ?? ""));
|
|
332
|
+
if (!xrefs.length)
|
|
333
|
+
return undefined;
|
|
334
|
+
if (kids.length !== 1) {
|
|
335
|
+
ctx.note("@outputclass", `an <xref> component must be alone in its <p> — kept as a link`);
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
const x = xrefs[0];
|
|
339
|
+
const component = dbComponent(x, ctx);
|
|
340
|
+
if (!component)
|
|
341
|
+
return undefined;
|
|
342
|
+
if (ctx.inTableCell) {
|
|
343
|
+
// A cell holds inline content only: the component would lose its link
|
|
344
|
+
ctx.note("@outputclass", `${component.name} inside a table cell — kept as a link`);
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const link = convertInline([x], ctx).find((n) => n.type === "link");
|
|
348
|
+
if (!link?.href) {
|
|
349
|
+
ctx.note("@outputclass", `${component.name} without a resolvable href — kept as text`);
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
const title = inlineLabel(link.children);
|
|
353
|
+
const q = qualify(component.name, component.classes, ctx);
|
|
354
|
+
const props = { ...q.props, ...(q.classes ? { class: q.classes } : {}) };
|
|
355
|
+
if (component.name === "db-link-card") {
|
|
356
|
+
const desc = x.children.find((c) => isElement(c) && c.name.toLowerCase() === "desc");
|
|
357
|
+
const description = desc ? inlineLabel(convertInline(desc.children, ctx)) : "";
|
|
358
|
+
return withProvenance({ type: "link-card", props: { ...props, title: title || link.href, href: link.href, ...(description ? { description } : {}) } }, x);
|
|
359
|
+
}
|
|
360
|
+
if (component.name === "db-button") {
|
|
361
|
+
return withProvenance({ type: "link-button", props: { ...props, href: link.href }, children: [{ type: "paragraph", inline: link.children }] }, x);
|
|
362
|
+
}
|
|
363
|
+
const id = youtubeId(link.href);
|
|
364
|
+
if (!id) {
|
|
365
|
+
ctx.note("@outputclass", `db-video "${link.href}" is not a YouTube URL — kept as a link`);
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
return withProvenance({ type: "youtube", props: { ...props, id, ...(title && title !== link.href ? { title } : {}) } }, x);
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* The component an element's `@outputclass` selects: its first `db-*` token,
|
|
372
|
+
* and the remaining tokens as classes. `db-` makes the token ours: other
|
|
373
|
+
* values (DITAVAL flagging, vendor CSS) pass through untouched. A class must
|
|
374
|
+
* be `[a-z0-9-]`, so a Tailwind arbitrary value (`bg-[url(…)]`) never reaches
|
|
375
|
+
* a site build that compiles the classes it finds; others are noted and
|
|
376
|
+
* dropped. An unknown or misplaced `db-*` is noted and the element converts
|
|
377
|
+
* as plain DITA.
|
|
378
|
+
*/
|
|
379
|
+
const dbComponent = (el, ctx) => {
|
|
380
|
+
// `db-theme-*` is a scope, not a component (themeOf): never picked or kept as a class
|
|
381
|
+
const tokens = outputTokens(el.attribs?.outputclass);
|
|
382
|
+
const at = tokens.findIndex((t) => t.startsWith("db-"));
|
|
383
|
+
if (at < 0)
|
|
384
|
+
return undefined;
|
|
385
|
+
const name = tokens[at].toLowerCase();
|
|
386
|
+
const element = el.name.toLowerCase();
|
|
387
|
+
if (!(DB_COMPONENTS[element] ?? []).includes(name)) {
|
|
388
|
+
ctx.note("@outputclass", `"${tokens[at]}" is not a component on <${element}> — converted as plain DITA`);
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
// One component per element: a second `db-*` token is noted, not a class
|
|
392
|
+
const extra = tokens.filter((t, i) => i !== at && t.startsWith("db-"));
|
|
393
|
+
if (extra.length)
|
|
394
|
+
ctx.note("@outputclass", `${extra.map((t) => `"${t}"`).join(", ")} ignored: <${element}> is already ${name}`);
|
|
395
|
+
const rest = tokens.filter((t, i) => i !== at && !t.startsWith("db-"));
|
|
396
|
+
const safe = rest.filter((t) => /^[a-z0-9][a-z0-9-]*$/.test(t));
|
|
397
|
+
const dropped = rest.filter((t) => !safe.includes(t));
|
|
398
|
+
if (dropped.length)
|
|
399
|
+
ctx.note("@outputclass", `class ${dropped.map((t) => `"${t}"`).join(", ")} on ${name} dropped: classes are lowercase letters, digits and hyphens`);
|
|
400
|
+
return { name, ...(safe.length ? { classes: safe.join(" ") } : {}) };
|
|
401
|
+
};
|
|
402
|
+
export { DB_QUALIFIERS };
|
|
403
|
+
/**
|
|
404
|
+
* A component's qualifier tokens as props: `pills`/`underline` on db-tabs
|
|
405
|
+
* (`variant`), `tone-*` on db-cards (`tone`) and db-details (its colour,
|
|
406
|
+
* `variant`). Props, not classes: a sanitized single page strips classes, and
|
|
407
|
+
* every renderer and exporter can implement or declare a prop. Anything else
|
|
408
|
+
* stays a free class; a `tone-` token outside the tones is noted.
|
|
409
|
+
*/
|
|
410
|
+
const qualify = (component, classes, ctx) => {
|
|
411
|
+
const props = {};
|
|
412
|
+
const rest = [];
|
|
413
|
+
for (const t of (classes ?? "").split(" ").filter(Boolean)) {
|
|
414
|
+
if (component === "db-tabs" && DB_QUALIFIERS.tabVariants.includes(t)) {
|
|
415
|
+
props.variant = t;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (component === "db-button" && DB_QUALIFIERS.buttonVariants.includes(t)) {
|
|
419
|
+
if (t !== "primary")
|
|
420
|
+
props.variant = t;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const cols = /^cols-(\d+)$/.exec(t);
|
|
424
|
+
if (component === "db-grid" && cols) {
|
|
425
|
+
if (DB_QUALIFIERS.gridColumns.includes(Number(cols[1])))
|
|
426
|
+
props.cols = Number(cols[1]);
|
|
427
|
+
else
|
|
428
|
+
ctx.note("@outputclass", `"${t}": db-grid takes cols-1 to cols-6 — ignored`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const tone = /^tone-(.+)$/.exec(t);
|
|
432
|
+
if (tone && (component === "db-cards" || component === "db-details")) {
|
|
433
|
+
if (DB_QUALIFIERS.tones.includes(tone[1]))
|
|
434
|
+
props[component === "db-details" ? "variant" : "tone"] = tone[1];
|
|
435
|
+
else
|
|
436
|
+
ctx.note("@outputclass", `"${t}" is not a tone (${DB_QUALIFIERS.tones.join(", ")}) — ignored`);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (tone) {
|
|
440
|
+
ctx.note("@outputclass", `"${t}": ${component} takes no tone (db-cards and db-details do) — ignored`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
rest.push(t);
|
|
444
|
+
}
|
|
445
|
+
return { props, ...(rest.length ? { classes: rest.join(" ") } : {}) };
|
|
446
|
+
};
|
|
447
|
+
/** Elements a scoped theme (`db-theme-<name>`) applies to: containers of blocks. */
|
|
448
|
+
// section and its task/reference specialisations, plus the generic containers
|
|
449
|
+
const THEMED = new Set(["section", "sectiondiv", "div", "bodydiv", "example", "context", "prereq", "result", "postreq", "refsyn"]);
|
|
450
|
+
/**
|
|
451
|
+
* The scoped theme an element asks for (`outputclass="db-theme-dark"`),
|
|
452
|
+
* independent of its component token. Noted when it is unknown, on an element
|
|
453
|
+
* that is not a block container, or inside a table cell.
|
|
454
|
+
*/
|
|
455
|
+
const themeOf = (el, ctx) => {
|
|
456
|
+
const tokens = (el.attribs?.outputclass ?? "").split(/\s+/).filter((t) => /^db-theme-/i.test(t));
|
|
457
|
+
const token = tokens[0];
|
|
458
|
+
if (!token)
|
|
459
|
+
return undefined;
|
|
460
|
+
if (tokens.length > 1)
|
|
461
|
+
ctx.note("@outputclass", `${tokens.slice(1).map((t) => `"${t}"`).join(", ")} ignored: one theme per element`);
|
|
462
|
+
const name = token.slice("db-theme-".length).toLowerCase();
|
|
463
|
+
// By @class in publish mode, so a specialised section is still a section
|
|
464
|
+
const element = blockName(el, ctx).toLowerCase();
|
|
465
|
+
if (!THEMED.has(element)) {
|
|
466
|
+
ctx.note("@outputclass", `"${token}" applies to section, sectiondiv, div, bodydiv and example — not <${element}>`);
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
if (!DB_QUALIFIERS.themes.includes(name)) {
|
|
470
|
+
ctx.note("@outputclass", `"${token}" is not a theme (${DB_QUALIFIERS.themes.join(", ")}) — content kept, unthemed`);
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
if (ctx.inTableCell) {
|
|
474
|
+
ctx.note("@outputclass", `"${token}" inside a table cell — content kept, unthemed`);
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
return name;
|
|
478
|
+
};
|
|
479
|
+
/** An `id` anchor the converter wrote (`<a id="x"></a>`), as opposed to other inline HTML. */
|
|
480
|
+
const isIdAnchor = (n) => n.type === "html-inline" && /^<a id="[^"]*"><\/a>$/.test(n.html);
|
|
481
|
+
const decodeEntities = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (m, e) => {
|
|
482
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
483
|
+
if (named[e.toLowerCase()])
|
|
484
|
+
return named[e.toLowerCase()];
|
|
485
|
+
const code = e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
486
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : m;
|
|
487
|
+
});
|
|
488
|
+
/**
|
|
489
|
+
* An inline run as plain text, for a label: markup is flattened, keys keep
|
|
490
|
+
* their `+`, an image gives its alt text, a line break a space, and inline
|
|
491
|
+
* HTML (`<sub>`, a code phrase holding a link) its text. Id anchors give
|
|
492
|
+
* nothing; the caller moves them.
|
|
493
|
+
*/
|
|
494
|
+
const inlineLabel = (inline) => (inline ?? [])
|
|
495
|
+
.map((n) => {
|
|
496
|
+
if (isIdAnchor(n))
|
|
497
|
+
return "";
|
|
498
|
+
if (n.type === "kbd")
|
|
499
|
+
return n.keys.join("+");
|
|
500
|
+
if (n.type === "image")
|
|
501
|
+
return n.alt ?? "";
|
|
502
|
+
if (n.type === "break")
|
|
503
|
+
return " ";
|
|
504
|
+
if (n.type === "html-inline")
|
|
505
|
+
return decodeEntities(n.html.replace(/<[^>]*>/g, ""));
|
|
506
|
+
const node = n;
|
|
507
|
+
if (typeof node.text === "string")
|
|
508
|
+
return node.text;
|
|
509
|
+
if (Array.isArray(node.children))
|
|
510
|
+
return inlineLabel(node.children);
|
|
511
|
+
return "";
|
|
512
|
+
})
|
|
513
|
+
.join("")
|
|
514
|
+
.replace(/\s+/g, " ")
|
|
515
|
+
.trim();
|
|
516
|
+
/**
|
|
517
|
+
* The entries of a component `<dl>` (`db-tabs`, `db-accordion`, `db-cards`),
|
|
518
|
+
* in the shape dogsbay-md's directives parse from a definition list. Each
|
|
519
|
+
* entry is converted on its own (as convertDefList converts it: conrefs,
|
|
520
|
+
* anchors, publish filtering), since the merged list loses where an entry
|
|
521
|
+
* whose definitions were filtered out ends. Id anchors in a term (an entry
|
|
522
|
+
* `@id`) move to the start of the entry's content; those of an entry that is
|
|
523
|
+
* dropped go to the next one, or the end of the last, so a cross-reference
|
|
524
|
+
* to them still lands. A second term in one entry is noted. An entry with no
|
|
525
|
+
* content is dropped unless `keepEmpty` (a card can be a bare link).
|
|
526
|
+
* `anchorsOut` collects the anchors instead (cards: a card's body sits inside
|
|
527
|
+
* its link, and an `<a id>` inside an `<a href>` splits the link).
|
|
528
|
+
*/
|
|
529
|
+
const dlComponentEntries = (el, ctx, component, keepEmpty = false, anchorsOut) => {
|
|
530
|
+
const out = [];
|
|
531
|
+
let pending = [];
|
|
532
|
+
const entries = el.children.filter((c) => isElement(c) && c.name.toLowerCase() === "dlentry");
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
const items = convertDefList({ ...el, children: [entry] }, ctx).children ?? [];
|
|
535
|
+
const terms = items.filter((n) => n.type === "dt");
|
|
536
|
+
for (const t of terms)
|
|
537
|
+
(anchorsOut ?? pending).push(...(t.inline ?? []).filter(isIdAnchor));
|
|
538
|
+
for (const extra of terms.slice(1)) {
|
|
539
|
+
ctx.note("@outputclass", `${component} entry with several terms: "${inlineLabel(extra.inline)}" is not its label`);
|
|
540
|
+
}
|
|
541
|
+
const dt = terms[0];
|
|
542
|
+
const content = [];
|
|
543
|
+
for (const dd of items.filter((n) => n.type === "dd")) {
|
|
544
|
+
if (dd.inline?.length)
|
|
545
|
+
content.push({ type: "paragraph", inline: dd.inline });
|
|
546
|
+
content.push(...(dd.children ?? []));
|
|
547
|
+
}
|
|
548
|
+
if (!dt)
|
|
549
|
+
continue;
|
|
550
|
+
// A term whose definitions were all filtered out is not an entry
|
|
551
|
+
if (content.length === 0 && !keepEmpty)
|
|
552
|
+
continue;
|
|
553
|
+
const anchored = pending.length ? [{ type: "paragraph", inline: pending }, ...content] : content;
|
|
554
|
+
pending = [];
|
|
555
|
+
out.push({ term: (dt.inline ?? []).filter((n) => !isIdAnchor(n)), label: inlineLabel(dt.inline), content: anchored });
|
|
556
|
+
}
|
|
557
|
+
const last = out[out.length - 1];
|
|
558
|
+
if (pending.length && last)
|
|
559
|
+
last.content.push({ type: "paragraph", inline: pending });
|
|
560
|
+
return out;
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Labels for tabs and accordion items, which a component matches by label
|
|
564
|
+
* (tabs) or value (accordion): an empty one is numbered, a repeated one
|
|
565
|
+
* suffixed, each with a note.
|
|
566
|
+
*/
|
|
567
|
+
const uniqueLabels = (entries, ctx, component, unit) => {
|
|
568
|
+
const used = new Set();
|
|
569
|
+
return entries.map((entry, i) => {
|
|
570
|
+
let label = entry.label;
|
|
571
|
+
if (!label) {
|
|
572
|
+
label = `${unit} ${i + 1}`;
|
|
573
|
+
ctx.note("@outputclass", `${component} term with no text — labelled "${label}"`);
|
|
574
|
+
}
|
|
575
|
+
if (used.has(label)) {
|
|
576
|
+
let n = 2;
|
|
577
|
+
while (used.has(`${label} (${n})`))
|
|
578
|
+
n++;
|
|
579
|
+
ctx.note("@outputclass", `${component} label "${label}" repeated — shown as "${label} (${n})"`);
|
|
580
|
+
label = `${label} (${n})`;
|
|
581
|
+
}
|
|
582
|
+
used.add(label);
|
|
583
|
+
return label;
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
/** `<dl outputclass="db-tabs">`: each entry is a tab, its term the label. */
|
|
587
|
+
const dlToTabs = (el, ctx) => {
|
|
588
|
+
const entries = dlComponentEntries(el, ctx, "db-tabs");
|
|
589
|
+
const labels = uniqueLabels(entries, ctx, "db-tabs", "Tab");
|
|
590
|
+
const tabs = entries.map((e, i) => ({ type: "tab", props: { label: labels[i] }, children: e.content }));
|
|
591
|
+
return tabs.length ? tabs : undefined;
|
|
592
|
+
};
|
|
593
|
+
/**
|
|
594
|
+
* A value for an accordion item: its label as a slug, by the rule dogsbay-md's
|
|
595
|
+
* `:::accordion` uses (`uniqueAccordionValue` in its parse.ts), so a published
|
|
596
|
+
* page and a migrated one give an item the same value. Empty → `item-N`.
|
|
597
|
+
*/
|
|
598
|
+
const itemValue = (label) => label.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
599
|
+
/** `<dl outputclass="db-accordion">`: each entry is a collapsible item, its term the label. */
|
|
600
|
+
const dlToAccordion = (el, ctx) => {
|
|
601
|
+
const entries = dlComponentEntries(el, ctx, "db-accordion");
|
|
602
|
+
const labels = uniqueLabels(entries, ctx, "db-accordion", "Item");
|
|
603
|
+
const values = new Set();
|
|
604
|
+
let fallback = 1;
|
|
605
|
+
const items = entries.map((e, i) => {
|
|
606
|
+
const slug = itemValue(labels[i]) || `item-${fallback++}`;
|
|
607
|
+
let value = slug;
|
|
608
|
+
for (let n = 2; values.has(value); n++)
|
|
609
|
+
value = `${slug}-${n}`;
|
|
610
|
+
values.add(value);
|
|
611
|
+
return { type: "accordion-item", props: { value, label: labels[i] }, children: e.content };
|
|
612
|
+
});
|
|
613
|
+
return items.length ? items : undefined;
|
|
614
|
+
};
|
|
615
|
+
/**
|
|
616
|
+
* `<dl outputclass="db-cards">`: each entry is a card, its term the title.
|
|
617
|
+
* A term that is exactly one link (`<dt><xref href="install.dita">Install</xref></dt>`)
|
|
618
|
+
* makes the card a link to that target; any other term is a title only. A
|
|
619
|
+
* card may have no definition (a bare link card), but not no title.
|
|
620
|
+
*/
|
|
621
|
+
const dlToCards = (el, ctx, anchors) => {
|
|
622
|
+
const cards = [];
|
|
623
|
+
for (const e of dlComponentEntries(el, ctx, "db-cards", true, anchors)) {
|
|
624
|
+
const parts = e.term.filter((n) => !(n.type === "text" && !n.text.trim()));
|
|
625
|
+
const link = parts.length === 1 && parts[0].type === "link" ? parts[0] : undefined;
|
|
626
|
+
if (!e.label) {
|
|
627
|
+
ctx.note("@outputclass", `db-cards term with no text — entry dropped`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const props = { title: e.label };
|
|
631
|
+
if (link?.href)
|
|
632
|
+
props.href = link.href;
|
|
633
|
+
cards.push({ type: "card", props, ...(e.content.length ? { children: e.content } : {}) });
|
|
634
|
+
}
|
|
635
|
+
return cards.length ? cards : undefined;
|
|
636
|
+
};
|
|
227
637
|
const convertDefList = (el, ctx, tags = { entry: "dlentry", term: "dt", def: "dd" }) => {
|
|
228
638
|
const children = [];
|
|
229
639
|
for (const entry of el.children.filter(isElement)) {
|
|
@@ -354,7 +764,7 @@ const convertTable = (el, ctx) => {
|
|
|
354
764
|
// Cell content always lives in children (leading inline wrapped as a
|
|
355
765
|
// paragraph): the serializer's HTML-table path reads only children,
|
|
356
766
|
// and its pipe path reads children paragraphs too
|
|
357
|
-
const { inline, children } = mixedContent(cellEl, { ...ctx, constrained: true });
|
|
767
|
+
const { inline, children } = mixedContent(cellEl, { ...ctx, constrained: true, inTableCell: true });
|
|
358
768
|
const cellChildren = [];
|
|
359
769
|
if (inline)
|
|
360
770
|
cellChildren.push({ type: "paragraph", inline });
|
|
@@ -709,6 +1119,17 @@ const ANCHORED_ELSEWHERE = new Set(["title", "related-links", "prolog", "data",
|
|
|
709
1119
|
* DITA-OT's pages. (Headings carry their own id.)
|
|
710
1120
|
*/
|
|
711
1121
|
export function convertBlock(el, ctx) {
|
|
1122
|
+
const out = convertBlockAnchored(el, ctx);
|
|
1123
|
+
// A scoped theme wraps the element's whole output, its anchor included
|
|
1124
|
+
const theme = /(^|\s)db-theme-/i.test(el.attribs?.outputclass ?? "") ? themeOf(el, ctx) : undefined;
|
|
1125
|
+
if (!theme || !out.length)
|
|
1126
|
+
return out;
|
|
1127
|
+
// The element's own id anchor stays outside the band: inside, it would be
|
|
1128
|
+
// the band's first child and take the first-child margin trim meant for content
|
|
1129
|
+
const lead = out[0].type === "html" && /^<a id="[^"]*"><\/a>$/.test(out[0].html ?? "") ? [out[0]] : [];
|
|
1130
|
+
return [...lead, { type: "theme", props: { name: theme }, children: out.slice(lead.length) }];
|
|
1131
|
+
}
|
|
1132
|
+
function convertBlockAnchored(el, ctx) {
|
|
712
1133
|
const out = convertBlockContent(el, ctx);
|
|
713
1134
|
const id = el.attribs?.id;
|
|
714
1135
|
if (!ctx.publish || !id || ANCHORED_ELSEWHERE.has(blockName(el, ctx)))
|
|
@@ -717,8 +1138,8 @@ export function convertBlock(el, ctx) {
|
|
|
717
1138
|
// An empty paragraph or division still keeps its id (DITA-OT writes the empty element)
|
|
718
1139
|
if (out.length === 0)
|
|
719
1140
|
return KEEPS_EMPTY.has(blockName(el, ctx)) ? [{ type: "html", html: idAnchor(anchor) }] : out;
|
|
720
|
-
// A titled section's heading carries the id
|
|
721
|
-
if (out.some((n) => n.
|
|
1141
|
+
// A titled section's heading (or a db-details block) carries the id itself
|
|
1142
|
+
if (out.some((n) => n.props?.id === anchor))
|
|
722
1143
|
return out;
|
|
723
1144
|
// Before a paragraph, the anchor is the start of its text (a separate block would be a paragraph of its own)
|
|
724
1145
|
if (out[0].type === "paragraph" && out[0].inline) {
|
|
@@ -816,6 +1237,33 @@ function convertBlockContent(el, ctx) {
|
|
|
816
1237
|
];
|
|
817
1238
|
}
|
|
818
1239
|
}
|
|
1240
|
+
// `<div|sectiondiv|bodydiv outputclass="db-details">`, titled by a leading
|
|
1241
|
+
// `<p outputclass="db-title">` (a visible first line in DITA-OT's output)
|
|
1242
|
+
if (UNTITLED_DETAILS.has(name) && /(^|\s)db-/.test(el.attribs?.outputclass ?? "")) {
|
|
1243
|
+
const component = dbComponent(el, ctx);
|
|
1244
|
+
if (component?.name === "db-details" && ctx.inTableCell) {
|
|
1245
|
+
// A table cell cannot hold a collapsible block (the HTML-table form
|
|
1246
|
+
// dropped it, title and all): keep its content, title included
|
|
1247
|
+
ctx.note("@outputclass", `db-details inside a table cell — kept as its content`);
|
|
1248
|
+
}
|
|
1249
|
+
else if (component?.name === "db-details") {
|
|
1250
|
+
// The first element, past whitespace, comments and processing
|
|
1251
|
+
// instructions (an editor's <?oxy_comment_start?>), may be the title
|
|
1252
|
+
const first = el.children.find((c) => isElement(c) || (c.type === "text" && (c.data ?? "").trim() !== ""));
|
|
1253
|
+
const titleP = first && isElement(first) && blockName(first, ctx) === "p" && /(^|\s)db-title(\s|$)/.test(first.attribs?.outputclass ?? "") ? first : undefined;
|
|
1254
|
+
// A conref'd title is pulled like a section title's
|
|
1255
|
+
const title = titleP ? inlineLabel(constrainedConrefInline(titleP, ctx) ?? convertInline(titleP.children, ctx)) : "";
|
|
1256
|
+
if (!title)
|
|
1257
|
+
ctx.note("@outputclass", `db-details on <${name}> has no leading <p outputclass="db-title"> with text — titled "Details"`);
|
|
1258
|
+
return [toDetails(el, ctx, component.classes, title, new Set(titleP ? [titleP] : []), titleP?.attribs?.id)];
|
|
1259
|
+
}
|
|
1260
|
+
else if (component?.name === "db-grid" && ctx.inTableCell) {
|
|
1261
|
+
ctx.note("@outputclass", `db-grid inside a table cell — kept as its content`);
|
|
1262
|
+
}
|
|
1263
|
+
else if (component?.name === "db-grid") {
|
|
1264
|
+
return [toGrid(el, ctx, component.classes)];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
819
1267
|
if (TRANSPARENT_CONTAINERS.has(name)) {
|
|
820
1268
|
// Publishing: a topic body starts with the topic's "Prerequisites" links, when it has any
|
|
821
1269
|
const topicId = el.parent?.attribs?.id;
|
|
@@ -830,6 +1278,13 @@ function convertBlockContent(el, ctx) {
|
|
|
830
1278
|
switch (name) {
|
|
831
1279
|
case "p":
|
|
832
1280
|
case "shortdesc": {
|
|
1281
|
+
// A link card, button or video: an xref component alone in its paragraph
|
|
1282
|
+
const xc = name === "p" ? xrefComponent(el, ctx) : undefined;
|
|
1283
|
+
if (xc) {
|
|
1284
|
+
// Publishing: the xref's own id (a link target) as an anchor before the component
|
|
1285
|
+
const anchors = anchorNodes([el.children.find((c) => isElement(c) && c.name.toLowerCase() === "xref")?.attribs?.id], ctx);
|
|
1286
|
+
return anchors.length ? [{ type: "paragraph", inline: anchors }, xc] : [xc];
|
|
1287
|
+
}
|
|
833
1288
|
// DITA allows blocks (ul, note, pre, …) inside <p>: leading inline
|
|
834
1289
|
// becomes the paragraph, block children follow as siblings
|
|
835
1290
|
const { inline, children } = mixedContent(el, ctx);
|
|
@@ -864,11 +1319,13 @@ function convertBlockContent(el, ctx) {
|
|
|
864
1319
|
case "screen":
|
|
865
1320
|
case "msgblock": {
|
|
866
1321
|
const outputclass = el.attribs?.outputclass ?? "";
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1322
|
+
// DITA-OT's language classes (language-xml, syntax-bash) name the
|
|
1323
|
+
// language in both modes. Publishing reads only those; migration also
|
|
1324
|
+
// takes a bare first token (outputclass="python"), the older convention
|
|
1325
|
+
const prefixed = /(?:^|\s)(?:language|syntax)-(\S+)/.exec(outputclass)?.[1];
|
|
1326
|
+
const bare = outputclass.trim().split(/\s+/)[0] ?? "";
|
|
1327
|
+
const language = prefixed
|
|
1328
|
+
?? (!ctx.publish && /^[A-Za-z0-9][\w+#.-]*$/.test(bare) ? bare : "");
|
|
872
1329
|
const content = textOf(el.children).replace(/^\n/, "").replace(/\s+$/, "");
|
|
873
1330
|
// Content in BOTH html and props.code: the serializer's block path
|
|
874
1331
|
// reads html, its HTML-table path (code inside cells) reads props.code
|
|
@@ -879,10 +1336,29 @@ function convertBlockContent(el, ctx) {
|
|
|
879
1336
|
};
|
|
880
1337
|
return [withProvenance(node, el)];
|
|
881
1338
|
}
|
|
882
|
-
case "ul":
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1339
|
+
case "ul": {
|
|
1340
|
+
const list = withProvenance({ type: "unordered-list", children: convertListItems(el, ctx, "li") }, el);
|
|
1341
|
+
// A file tree: folders are items with a nested list, or end in "/"
|
|
1342
|
+
if (dbComponent(el, ctx)?.name === "db-files") {
|
|
1343
|
+
if (!ctx.inTableCell)
|
|
1344
|
+
return [{ type: "files", children: [list] }];
|
|
1345
|
+
ctx.note("@outputclass", `db-files inside a table cell — kept as a list`);
|
|
1346
|
+
}
|
|
1347
|
+
return [list];
|
|
1348
|
+
}
|
|
1349
|
+
case "ol": {
|
|
1350
|
+
const items = convertListItems(el, ctx, "li");
|
|
1351
|
+
// Steps outside a task: each item a numbered, connected step
|
|
1352
|
+
if (dbComponent(el, ctx)?.name === "db-steps") {
|
|
1353
|
+
if (!ctx.inTableCell) {
|
|
1354
|
+
// An item stays itself (its id, its provenance) as a step
|
|
1355
|
+
const steps = items.map((it) => ({ ...it, type: "step" }));
|
|
1356
|
+
return [withProvenance({ type: "steps", children: steps }, el)];
|
|
1357
|
+
}
|
|
1358
|
+
ctx.note("@outputclass", `db-steps inside a table cell — kept as a numbered list`);
|
|
1359
|
+
}
|
|
1360
|
+
return [withProvenance({ type: "ordered-list", children: items }, el)];
|
|
1361
|
+
}
|
|
886
1362
|
case "sl": {
|
|
887
1363
|
// Simple list → unordered list of <sli> items (same conref hooks
|
|
888
1364
|
// as <li>: referencing items include or ledger, defining items
|
|
@@ -898,8 +1374,27 @@ function convertBlockContent(el, ctx) {
|
|
|
898
1374
|
});
|
|
899
1375
|
return [withProvenance({ type: "unordered-list", children: items }, el)];
|
|
900
1376
|
}
|
|
901
|
-
case "dl":
|
|
1377
|
+
case "dl": {
|
|
1378
|
+
const component = dbComponent(el, ctx);
|
|
1379
|
+
if (component) {
|
|
1380
|
+
// Card entries' id anchors go before the grid, not inside a card's link
|
|
1381
|
+
const anchors = [];
|
|
1382
|
+
const [type, build] = {
|
|
1383
|
+
"db-tabs": ["tabs", dlToTabs],
|
|
1384
|
+
"db-accordion": ["accordion", dlToAccordion],
|
|
1385
|
+
"db-cards": ["cards", (e, c) => dlToCards(e, c, anchors)],
|
|
1386
|
+
}[component.name];
|
|
1387
|
+
const children = build(el, ctx);
|
|
1388
|
+
if (children) {
|
|
1389
|
+
const q = qualify(component.name, component.classes, ctx);
|
|
1390
|
+
const props = { ...q.props, ...(q.classes ? { class: q.classes } : {}) };
|
|
1391
|
+
const group = withProvenance({ type, ...(Object.keys(props).length ? { props } : {}), children }, el);
|
|
1392
|
+
return anchors.length ? [{ type: "paragraph", inline: anchors }, group] : [group];
|
|
1393
|
+
}
|
|
1394
|
+
ctx.note("@outputclass", `${component.name} on a <dl> with no usable entry — kept as a definition list`);
|
|
1395
|
+
}
|
|
902
1396
|
return [withProvenance(convertDefList(el, ctx), el)];
|
|
1397
|
+
}
|
|
903
1398
|
case "parml":
|
|
904
1399
|
// Parameter list — structurally a definition list
|
|
905
1400
|
return [withProvenance(convertDefList(el, ctx, { entry: "plentry", term: "pt", def: "pd" }), el)];
|
|
@@ -996,6 +1491,18 @@ function convertBlockContent(el, ctx) {
|
|
|
996
1491
|
const titleEl = el.children
|
|
997
1492
|
.filter(isElement)
|
|
998
1493
|
.find((c) => blockName(c, ctx) === "title");
|
|
1494
|
+
// `<section outputclass="db-details">`: a collapsible block titled by
|
|
1495
|
+
// the section title (flattened to text)
|
|
1496
|
+
const component = name === "section" ? dbComponent(el, ctx) : undefined;
|
|
1497
|
+
if (component?.name === "db-details") {
|
|
1498
|
+
if (titleEl) {
|
|
1499
|
+
const title = inlineLabel(constrainedConrefInline(titleEl, ctx) ?? convertInline(titleEl.children, ctx));
|
|
1500
|
+
if (!title)
|
|
1501
|
+
ctx.note("@outputclass", `db-details section title has no text — titled "Details"`);
|
|
1502
|
+
return [toDetails(el, ctx, component.classes, title, new Set(["title"]))];
|
|
1503
|
+
}
|
|
1504
|
+
ctx.note("@outputclass", `db-details on a <section> with no <title> — converted as a plain section`);
|
|
1505
|
+
}
|
|
999
1506
|
if (titleEl) {
|
|
1000
1507
|
// <title conref=…/> — heading text pulled from elsewhere
|
|
1001
1508
|
const titleConref = constrainedConrefInline(titleEl, ctx);
|
|
@@ -1098,7 +1605,9 @@ function convertBlockContent(el, ctx) {
|
|
|
1098
1605
|
}
|
|
1099
1606
|
}
|
|
1100
1607
|
}
|
|
1101
|
-
export function convertChildren(el, ctx,
|
|
1608
|
+
export function convertChildren(el, ctx,
|
|
1609
|
+
/** Element names, or particular child nodes, to leave out */
|
|
1610
|
+
skip = new Set()) {
|
|
1102
1611
|
const out = [];
|
|
1103
1612
|
let inlineRun = [];
|
|
1104
1613
|
const flushInline = () => {
|
|
@@ -1110,7 +1619,7 @@ export function convertChildren(el, ctx, skip = new Set()) {
|
|
|
1110
1619
|
for (const child of el.children) {
|
|
1111
1620
|
if (isElement(child)) {
|
|
1112
1621
|
const name = blockName(child, ctx);
|
|
1113
|
-
if (skip.has(name))
|
|
1622
|
+
if (skip.has(name) || skip.has(child))
|
|
1114
1623
|
continue;
|
|
1115
1624
|
if (isBlockChild(child, ctx) || TRANSPARENT_CONTAINERS.has(name) || name === "title" ||
|
|
1116
1625
|
name === "prolog" || name === "related-links" || name === "draft-comment") {
|
|
@@ -1128,4 +1637,3 @@ export function convertChildren(el, ctx, skip = new Set()) {
|
|
|
1128
1637
|
flushInline();
|
|
1129
1638
|
return out;
|
|
1130
1639
|
}
|
|
1131
|
-
//# sourceMappingURL=blocks.js.map
|
package/dist/conref.d.ts
CHANGED
package/dist/conref.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* definition-list entries).
|
|
20
20
|
*/
|
|
21
21
|
import { parseDocument } from "htmlparser2";
|
|
22
|
-
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
22
|
+
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
|
|
23
23
|
import { convertBlock, convertItemContent, isBlockTarget, } from "./blocks.js";
|
|
24
24
|
import { convertInline, trimInline } from "./inline.js";
|
|
25
25
|
import { DITA_TOPIC_TYPES, } from "./types.js";
|
|
@@ -300,4 +300,3 @@ export function planConrefs(files) {
|
|
|
300
300
|
},
|
|
301
301
|
};
|
|
302
302
|
}
|
|
303
|
-
//# sourceMappingURL=conref.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -16,4 +16,5 @@ export { DITA_TOPIC_TYPES, renameDitaHref, relativeIncludePath, type DitaConvers
|
|
|
16
16
|
export { ditaTopicToTree } from "./parse.js";
|
|
17
17
|
export { ditaTopicToDogsbayMd, type DitaMarkdownResult } from "./markdown.js";
|
|
18
18
|
export { planConrefs, type ConrefCorpusFile, type ConrefPlan, type ConrefSnippet, } from "./conref.js";
|
|
19
|
-
|
|
19
|
+
export { DB_QUALIFIERS } from "./blocks.js";
|
|
20
|
+
export { checkWellFormed, type WellFormedProblem } from "./well-formed.js";
|