@dogsbay/format-dita 0.2.0-beta.124 → 0.2.0-beta.126
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 +5 -0
- package/dist/blocks.js +55 -6
- package/dist/class-defaults-apply.d.ts +2 -2
- package/dist/class-defaults-apply.js +4 -2
- package/dist/inline.d.ts +10 -0
- package/dist/inline.js +40 -28
- package/dist/lines.d.ts +5 -0
- package/dist/lines.js +24 -0
- package/dist/parse.js +23 -6
- package/dist/types.d.ts +12 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,6 +38,11 @@ emit `{% include %}` so one canonical copy of reused text survives. Use
|
|
|
38
38
|
`dogsbay import-dita --no-conref-includes` to keep conrefs on the ledger
|
|
39
39
|
instead.
|
|
40
40
|
|
|
41
|
+
**Lines.** Every conversion note carries `line`, the source line of the
|
|
42
|
+
element that raised it (or of the block being converted), so a host can point
|
|
43
|
+
at it. `{ sourceLines: true }` also puts each block's line on `meta.line`, which
|
|
44
|
+
`@dogsbay/render-html` writes as `data-line` when asked (`dataLines: true`).
|
|
45
|
+
|
|
41
46
|
**Specializations in raw source:** a file as authored has no `@class`, so
|
|
42
47
|
an element is known only by its name, and a specialization the converter
|
|
43
48
|
does not know by name (a `<troubleshooting>` topic, a hazard statement's
|
package/dist/blocks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inlineToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
|
|
2
|
-
import { convertInline, elementName, idAnchor, publishImage, trimInline, textOf, inlineSafeSnippet, conrefLedgerDetail, } from "./inline.js";
|
|
2
|
+
import { convertInline, elementName, isKnownInline, 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
5
|
import { DB_QUALIFIERS, outputTokens, youtubeId } from "./qualifiers.js";
|
|
@@ -52,6 +52,13 @@ const withProvenance = (node, el) => {
|
|
|
52
52
|
}
|
|
53
53
|
return node;
|
|
54
54
|
};
|
|
55
|
+
/** A list item or step: its source line (ParseOptions.sourceLines), as convertBlock gives a block. */
|
|
56
|
+
const atLine = (node, el, ctx) => {
|
|
57
|
+
const line = ctx.lineOf?.(el);
|
|
58
|
+
if (line !== undefined && node.meta?.line === undefined)
|
|
59
|
+
node.meta = { ...node.meta, line };
|
|
60
|
+
return node;
|
|
61
|
+
};
|
|
55
62
|
const noteFilterAttrs = (el, ctx) => {
|
|
56
63
|
for (const attr of ["audience", "platform", "product", "props", "otherprops", "rev"]) {
|
|
57
64
|
if (el.attribs?.[attr]) {
|
|
@@ -77,8 +84,29 @@ const isBlockChild = (el, ctx) => {
|
|
|
77
84
|
const name = blockName(el, ctx);
|
|
78
85
|
if (ctx.publish && name === "image")
|
|
79
86
|
return el.attribs?.placement === "break";
|
|
80
|
-
return BLOCK_ELEMENTS.has(name);
|
|
87
|
+
return BLOCK_ELEMENTS.has(name) || (isUnknownElement(el, ctx) && holdsBlocks(el, ctx));
|
|
81
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* An element the converter knows by neither a block nor an inline name (a
|
|
91
|
+
* custom specialization). Both resolutions count: with @class, a
|
|
92
|
+
* specialization of <fn> or <ph> resolves to an inline name, not a block one.
|
|
93
|
+
*/
|
|
94
|
+
const isUnknownElement = (el, ctx) => !isKnownBlock(elementName(el, ctx, isKnownBlock)) && !isKnownInline(elementName(el, ctx, isKnownInline));
|
|
95
|
+
/**
|
|
96
|
+
* Whether an unknown element holds block content (a <p>, a list), directly or
|
|
97
|
+
* through other unknown wrappers. Such an element is a block wrapper: read as
|
|
98
|
+
* inline, its paragraphs and lists were flattened into one run of text, with
|
|
99
|
+
* an "unrecognized inline element" note for each of them. An image does not
|
|
100
|
+
* count: it sits in a sentence as often as not (an icon in a UI label).
|
|
101
|
+
*/
|
|
102
|
+
const holdsBlocks = (el, ctx) => el.children.some((c) => {
|
|
103
|
+
if (!isElement(c))
|
|
104
|
+
return false;
|
|
105
|
+
const name = blockName(c, ctx);
|
|
106
|
+
if (name === "image")
|
|
107
|
+
return false;
|
|
108
|
+
return BLOCK_ELEMENTS.has(name) || TRANSPARENT_CONTAINERS.has(name) || (isUnknownElement(c, ctx) && holdsBlocks(c, ctx));
|
|
109
|
+
});
|
|
82
110
|
const paragraphFrom = (nodes, ctx) => {
|
|
83
111
|
const inline = trimInline(convertInline(nodes, ctx));
|
|
84
112
|
if (inline.length === 0)
|
|
@@ -221,7 +249,7 @@ const convertListItems = (el, ctx, itemTag) => el.children
|
|
|
221
249
|
.map((li) => {
|
|
222
250
|
const viaConref = itemConref(li, ctx, "list-item") ?? definingItemNode(li, ctx, "list-item");
|
|
223
251
|
if (viaConref)
|
|
224
|
-
return withProvenance(viaConref, li);
|
|
252
|
+
return atLine(withProvenance(viaConref, li), li, ctx);
|
|
225
253
|
const { inline: content, children: blocks } = mixedContent(li, { ...ctx, constrained: true });
|
|
226
254
|
const { inline, children } = withItemAnchor(content, blocks, [li.attribs?.id], ctx);
|
|
227
255
|
const item = { type: "list-item" };
|
|
@@ -229,7 +257,7 @@ const convertListItems = (el, ctx, itemTag) => el.children
|
|
|
229
257
|
item.inline = inline;
|
|
230
258
|
if (children.length > 0)
|
|
231
259
|
item.children = children;
|
|
232
|
-
return withProvenance(item, li);
|
|
260
|
+
return atLine(withProvenance(item, li), li, ctx);
|
|
233
261
|
});
|
|
234
262
|
/** Components an `@outputclass` `db-*` token selects, per element. */
|
|
235
263
|
const DB_COMPONENTS = {
|
|
@@ -966,7 +994,7 @@ const convertSteps = (el, ctx, type) => {
|
|
|
966
994
|
.map((stepEl) => {
|
|
967
995
|
const viaConref = itemConref(stepEl, ctx, "step") ?? definingItemNode(stepEl, ctx, "step");
|
|
968
996
|
if (viaConref)
|
|
969
|
-
return withProvenance(viaConref, stepEl);
|
|
997
|
+
return atLine(withProvenance(viaConref, stepEl), stepEl, ctx);
|
|
970
998
|
const { inline: content, children: blocks } = convertStepContent(stepEl, { ...ctx, constrained: true });
|
|
971
999
|
// Publishing: DITA-OT labels optional and required steps
|
|
972
1000
|
const importance = stepEl.attribs?.importance;
|
|
@@ -982,7 +1010,7 @@ const convertSteps = (el, ctx, type) => {
|
|
|
982
1010
|
step.inline = inline;
|
|
983
1011
|
if (children.length > 0)
|
|
984
1012
|
step.children = children;
|
|
985
|
-
return withProvenance(step, stepEl);
|
|
1013
|
+
return atLine(withProvenance(step, stepEl), stepEl, ctx);
|
|
986
1014
|
});
|
|
987
1015
|
return { type, children };
|
|
988
1016
|
};
|
|
@@ -1141,6 +1169,27 @@ const ANCHORED_ELSEWHERE = new Set(["title", "related-links", "prolog", "data",
|
|
|
1141
1169
|
* DITA-OT's pages. (Headings carry their own id.)
|
|
1142
1170
|
*/
|
|
1143
1171
|
export function convertBlock(el, ctx) {
|
|
1172
|
+
// A note raised while converting this element takes its line
|
|
1173
|
+
const outer = ctx.position?.el;
|
|
1174
|
+
if (ctx.position)
|
|
1175
|
+
ctx.position.el = el;
|
|
1176
|
+
try {
|
|
1177
|
+
const out = convertBlockThemed(el, ctx);
|
|
1178
|
+
// Source lines: each node this element became starts at its line
|
|
1179
|
+
// (nested blocks came through here first and keep their own)
|
|
1180
|
+
const line = ctx.lineOf?.(el);
|
|
1181
|
+
if (line !== undefined)
|
|
1182
|
+
for (const node of out)
|
|
1183
|
+
if (node.meta?.line === undefined)
|
|
1184
|
+
node.meta = { ...node.meta, line };
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
|
1187
|
+
finally {
|
|
1188
|
+
if (ctx.position)
|
|
1189
|
+
ctx.position.el = outer;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function convertBlockThemed(el, ctx) {
|
|
1144
1193
|
const out = convertBlockAnchored(el, ctx);
|
|
1145
1194
|
// A scoped theme wraps the element's whole output, its anchor included
|
|
1146
1195
|
const theme = /(^|\s)db-theme-/i.test(el.attribs?.outputclass ?? "") ? themeOf(el, ctx) : undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyNode } from "domhandler";
|
|
1
|
+
import type { AnyNode, Element } from "domhandler";
|
|
2
2
|
/** An element's DITA 1.3 default @class (`inMap`: inside a map document), or undefined for a name the DTDs do not declare. */
|
|
3
3
|
export declare const classDefault: (name: string, inMap?: boolean) => string | undefined;
|
|
4
4
|
/**
|
|
@@ -12,4 +12,4 @@ export declare const classDefault: (name: string, inMap?: boolean) => string | u
|
|
|
12
12
|
* specialization: MathML, SVG), which are not DITA elements and are left
|
|
13
13
|
* untouched. A DITA element inside one (`<mathmlref>`, `<svgref>`) is classed.
|
|
14
14
|
*/
|
|
15
|
-
export declare function applyClassDefaults(nodes: AnyNode[]): string[];
|
|
15
|
+
export declare function applyClassDefaults(nodes: AnyNode[], firstOf?: Map<string, Element>): string[];
|
|
@@ -14,7 +14,7 @@ export const classDefault = (name, inMap = false) => (inMap ? own(DITA_MAP_CLASS
|
|
|
14
14
|
* specialization: MathML, SVG), which are not DITA elements and are left
|
|
15
15
|
* untouched. A DITA element inside one (`<mathmlref>`, `<svgref>`) is classed.
|
|
16
16
|
*/
|
|
17
|
-
export function applyClassDefaults(nodes) {
|
|
17
|
+
export function applyClassDefaults(nodes, firstOf) {
|
|
18
18
|
const root = nodes.find(isTag);
|
|
19
19
|
const inMap = !!root && (["map", "bookmap", "subjectScheme"].includes(root.name) || ` ${root.attribs?.class ?? ""} `.includes(" map/map "));
|
|
20
20
|
const unknown = new Set();
|
|
@@ -25,8 +25,10 @@ export function applyClassDefaults(nodes) {
|
|
|
25
25
|
const value = classDefault(el.name, inMap);
|
|
26
26
|
if (value)
|
|
27
27
|
el.attribs = { ...el.attribs, class: value };
|
|
28
|
-
else if (el.name !== "dita" && !inForeign)
|
|
28
|
+
else if (el.name !== "dita" && !inForeign && !unknown.has(el.name)) {
|
|
29
29
|
unknown.add(el.name);
|
|
30
|
+
firstOf?.set(el.name, el);
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
const foreign = inForeign || ` ${el.attribs?.class ?? ""} `.includes(" topic/foreign ");
|
|
32
34
|
for (const child of el.children)
|
package/dist/inline.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export interface InlineContext {
|
|
|
11
11
|
publish?: PublishContext;
|
|
12
12
|
/** Every element carries @class (DTD defaults filled in): resolve unknown names by it, as publishing does. */
|
|
13
13
|
classes?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The element being converted, shared by every context derived from the
|
|
16
|
+
* topic's (a note takes its line from it). Set by convertBlock and, for each
|
|
17
|
+
* element, convertInline.
|
|
18
|
+
*/
|
|
19
|
+
position?: {
|
|
20
|
+
el?: Element;
|
|
21
|
+
};
|
|
22
|
+
/** ParseOptions.sourceLines: an element's 1-based source line. */
|
|
23
|
+
lineOf?: (el: Element) => number | undefined;
|
|
14
24
|
/** Publishing: trademarks already marked in this topic (DITA-OT marks only the first). */
|
|
15
25
|
trademarks?: Set<string>;
|
|
16
26
|
/** Publishing: the number label of each titled table and figure ("Table 1. "). */
|
package/dist/inline.js
CHANGED
|
@@ -216,6 +216,19 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
216
216
|
}
|
|
217
217
|
if (!isElement(node))
|
|
218
218
|
continue;
|
|
219
|
+
const outer = ctx.position?.el;
|
|
220
|
+
if (ctx.position)
|
|
221
|
+
ctx.position.el = node;
|
|
222
|
+
try {
|
|
223
|
+
convertInlineElement(node);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
if (ctx.position)
|
|
227
|
+
ctx.position.el = outer;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return mergeAdjacentText(out);
|
|
231
|
+
function convertInlineElement(node) {
|
|
219
232
|
const name = elementName(node, ctx, isKnownInline);
|
|
220
233
|
const children = node.children;
|
|
221
234
|
// Publishing: an inline element with an @id gets an anchor, as DITA-OT's HTML keeps the id
|
|
@@ -238,7 +251,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
238
251
|
const defining = ctx.conref.definingSnippet(node.attribs.id);
|
|
239
252
|
if (defining && inlineSafeSnippet(defining)) {
|
|
240
253
|
pushText(`{% include "${ctx.conref.includePath(defining)}" %}`);
|
|
241
|
-
|
|
254
|
+
return;
|
|
242
255
|
}
|
|
243
256
|
}
|
|
244
257
|
// Inline conrefs (<ph conref="…"/>) are the standard DITA reused-text
|
|
@@ -254,7 +267,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
254
267
|
// Local children are the stale preview copy in DITA conref
|
|
255
268
|
// semantics — the include replaces the whole element
|
|
256
269
|
pushText(`{% include "${ctx.conref.includePath(snippet)}" %}`);
|
|
257
|
-
|
|
270
|
+
return;
|
|
258
271
|
}
|
|
259
272
|
ctx.note(node.attribs.conref ? "@conref" : "@conkeyref", `<${name}> pulls content from "${ref}" — ${conrefLedgerDetail(node.attribs, snippet, ctx.conref !== undefined)}`);
|
|
260
273
|
}
|
|
@@ -275,7 +288,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
275
288
|
noteOnce(ctx, `db-badge ${t}`, `"${t}" is not a tone (${DB_QUALIFIERS.tones.join(", ")}) — default badge`);
|
|
276
289
|
}
|
|
277
290
|
out.push({ type: "badge", text, ...(variant ? { variant } : {}) });
|
|
278
|
-
|
|
291
|
+
return;
|
|
279
292
|
}
|
|
280
293
|
// A content-less keyref falls through to the key's text ({{ key }}): the badge look is what is lost
|
|
281
294
|
if (node.attribs?.keyref)
|
|
@@ -309,7 +322,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
309
322
|
: [];
|
|
310
323
|
// An unresolved key with no content renders nothing, as in DITA-OT's output
|
|
311
324
|
if (content.length === 0)
|
|
312
|
-
|
|
325
|
+
return;
|
|
313
326
|
const href = node.attribs?.href;
|
|
314
327
|
if (href) {
|
|
315
328
|
// An external link keeps its href as written, even to a .dita file
|
|
@@ -319,14 +332,14 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
319
332
|
else {
|
|
320
333
|
out.push(...content);
|
|
321
334
|
}
|
|
322
|
-
|
|
335
|
+
return;
|
|
323
336
|
}
|
|
324
337
|
if (!ctx.publish && node.attribs?.keyref && name !== "xref" && name !== "link" && name !== "image") {
|
|
325
338
|
const hasFallback = children.some((c) => isElement(c) || (isText(c) && c.data.trim().length > 0));
|
|
326
339
|
if (!hasFallback) {
|
|
327
340
|
noteOnce(ctx, "@keyref", "content-less keyref emitted as {{ key }} minja variable — resolves only for TEXT keys (keys.yaml entries with text:); href-only keys need manual attention");
|
|
328
341
|
pushText(`{{ ${node.attribs.keyref} }}`);
|
|
329
|
-
|
|
342
|
+
return;
|
|
330
343
|
}
|
|
331
344
|
// Fallback content present: note the dropped indirection, then fall
|
|
332
345
|
// through so the element converts normally (term stays italic, …)
|
|
@@ -334,12 +347,12 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
334
347
|
}
|
|
335
348
|
if (TRANSPARENT.has(name) && !isCodeLike(name, ctx)) {
|
|
336
349
|
out.push(...convertInline(children, ctx, formatting));
|
|
337
|
-
|
|
350
|
+
return;
|
|
338
351
|
}
|
|
339
352
|
// Publishing: content DITA-OT's HTML does not show (foreign content such as MathML without
|
|
340
353
|
// a plugin, and required-cleanup without args.draft)
|
|
341
354
|
if (ctx.publish && (name === "foreign" || name === "unknown" || name === "required-cleanup"))
|
|
342
|
-
|
|
355
|
+
return;
|
|
343
356
|
if ((name === "fn" || name === "synnote") && ctx.publish && ctx.footnotes) {
|
|
344
357
|
// Publishing: a numbered footnote, listed at the end of the page as DITA-OT does. Only
|
|
345
358
|
// footnotes without @callout are numbered; a callout is the label when it is a valid,
|
|
@@ -351,14 +364,14 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
351
364
|
// Converted by parse.ts, which can convert block content (a list in a footnote)
|
|
352
365
|
ctx.footnotes.push({ label, el: node, ...(callout ? { callout: true } : {}) });
|
|
353
366
|
out.push({ type: "footnote-ref", label });
|
|
354
|
-
|
|
367
|
+
return;
|
|
355
368
|
}
|
|
356
369
|
if (name === "q" && ctx.publish) {
|
|
357
370
|
// Publishing: a quotation in quotation marks, as a browser renders DITA-OT's <q>
|
|
358
371
|
pushText("\u201C");
|
|
359
372
|
out.push(...convertInline(children, ctx, formatting));
|
|
360
373
|
pushText("\u201D");
|
|
361
|
-
|
|
374
|
+
return;
|
|
362
375
|
}
|
|
363
376
|
if (isCodeLike(name, ctx) && ctx.publish && children.some((c) => isElement(c) && ["xref", "link"].includes(c.name.toLowerCase()))) {
|
|
364
377
|
// Publishing: a code phrase containing a link, as DITA-OT renders it: one code
|
|
@@ -374,7 +387,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
374
387
|
out.push({ type: "html-inline", html: `<code>${collapseWhitespace(html).trim()}</code>` });
|
|
375
388
|
if (raw.endsWith(" "))
|
|
376
389
|
pushText(" ");
|
|
377
|
-
|
|
390
|
+
return;
|
|
378
391
|
}
|
|
379
392
|
if (isCodeLike(name, ctx)) {
|
|
380
393
|
if (name !== "codeph" && name !== "tt") {
|
|
@@ -394,7 +407,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
394
407
|
});
|
|
395
408
|
if (ctx.publish && raw.endsWith(" ") && raw.trim())
|
|
396
409
|
pushText(" ");
|
|
397
|
-
|
|
410
|
+
return;
|
|
398
411
|
}
|
|
399
412
|
if (name === "menucascade") {
|
|
400
413
|
// "File > Save Project", as DITA-OT's HTML renders it: each step keeps
|
|
@@ -412,7 +425,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
412
425
|
out.push(...converted);
|
|
413
426
|
any = true;
|
|
414
427
|
}
|
|
415
|
-
|
|
428
|
+
return;
|
|
416
429
|
}
|
|
417
430
|
if (name === "shortcut") {
|
|
418
431
|
// A keystroke (`<shortcut>Ctrl+S</shortcut>`) is a keycap. Inside a
|
|
@@ -424,21 +437,21 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
424
437
|
out.push({ type: "kbd", keys });
|
|
425
438
|
else
|
|
426
439
|
out.push(...convertInline(children, ctx, formatting));
|
|
427
|
-
|
|
440
|
+
return;
|
|
428
441
|
}
|
|
429
442
|
if (BOLD_LIKE.has(name)) {
|
|
430
443
|
if (name !== "b" && name !== "strong") {
|
|
431
444
|
noteOnce(ctx, `<${name}>`, `rendered as bold — semantic identity not preserved`);
|
|
432
445
|
}
|
|
433
446
|
out.push(...convertInline(children, ctx, { ...formatting, bold: true }));
|
|
434
|
-
|
|
447
|
+
return;
|
|
435
448
|
}
|
|
436
449
|
if (ITALIC_LIKE.has(name)) {
|
|
437
450
|
if (name !== "i" && name !== "em") {
|
|
438
451
|
noteOnce(ctx, `<${name}>`, `rendered as italic — semantic identity not preserved`);
|
|
439
452
|
}
|
|
440
453
|
out.push(...convertInline(children, ctx, { ...formatting, italic: true }));
|
|
441
|
-
|
|
454
|
+
return;
|
|
442
455
|
}
|
|
443
456
|
if (name === "tm" && ctx.publish) {
|
|
444
457
|
// Publishing: the trademark symbol DITA-OT's HTML adds, on a trademark's first use in the topic
|
|
@@ -448,18 +461,18 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
448
461
|
if (symbol && !ctx.trademarks?.has(trademark))
|
|
449
462
|
pushText(symbol);
|
|
450
463
|
ctx.trademarks?.add(trademark);
|
|
451
|
-
|
|
464
|
+
return;
|
|
452
465
|
}
|
|
453
466
|
// Highlighting Markdown has no syntax for: inline HTML around the converted content
|
|
454
467
|
if (name === "u" || name === "sub" || name === "sup" || name === "overline") {
|
|
455
468
|
const open = name === "overline" ? '<span style="text-decoration: overline">' : `<${name}>`;
|
|
456
469
|
const close = name === "overline" ? "</span>" : `</${name}>`;
|
|
457
470
|
out.push({ type: "html-inline", html: open }, ...convertInline(children, ctx, formatting), { type: "html-inline", html: close });
|
|
458
|
-
|
|
471
|
+
return;
|
|
459
472
|
}
|
|
460
473
|
if (name === "line-through") {
|
|
461
474
|
out.push(...convertInline(children, ctx, { ...formatting, strikethrough: true }));
|
|
462
|
-
|
|
475
|
+
return;
|
|
463
476
|
}
|
|
464
477
|
if (name === "xref" || name === "link") {
|
|
465
478
|
const href = node.attribs?.href;
|
|
@@ -476,7 +489,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
476
489
|
out.push(...linkChildren);
|
|
477
490
|
else if (text)
|
|
478
491
|
pushText(text);
|
|
479
|
-
|
|
492
|
+
return;
|
|
480
493
|
}
|
|
481
494
|
if (keyref && !ctx.publish) {
|
|
482
495
|
// Key-based links keep their indirection via the key: scheme
|
|
@@ -486,7 +499,7 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
486
499
|
href: `key:${keyref}`,
|
|
487
500
|
children: linkChildren.length > 0 ? linkChildren : [{ type: "text", text: keyref }],
|
|
488
501
|
});
|
|
489
|
-
|
|
502
|
+
return;
|
|
490
503
|
}
|
|
491
504
|
if (href) {
|
|
492
505
|
// An external link keeps its href as written, even to a .dita file
|
|
@@ -498,11 +511,11 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
498
511
|
? linkChildren
|
|
499
512
|
: [{ type: "text", text: fallbackText || (ctx.publish ? ditaOtHrefText(href) : href) }],
|
|
500
513
|
});
|
|
501
|
-
|
|
514
|
+
return;
|
|
502
515
|
}
|
|
503
516
|
noteOnce(ctx, `<${name}>`, "xref without href/keyref — emitted as plain text");
|
|
504
517
|
pushText(fallbackText);
|
|
505
|
-
|
|
518
|
+
return;
|
|
506
519
|
}
|
|
507
520
|
if (name === "image") {
|
|
508
521
|
const imgHref = node.attribs?.href;
|
|
@@ -513,13 +526,13 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
513
526
|
else {
|
|
514
527
|
noteOnce(ctx, "<image>", "image without href dropped");
|
|
515
528
|
}
|
|
516
|
-
|
|
529
|
+
return;
|
|
517
530
|
}
|
|
518
531
|
if (ctx.publish) {
|
|
519
532
|
// Publishing: size, alignment and the <alt> element, as DITA-OT's HTML renders them
|
|
520
533
|
const { alt, attrs } = publishImage(node, ctx.publish);
|
|
521
534
|
out.push({ type: "image", src: imgHref, ...(alt ? { alt } : {}), ...(attrs ? { attrs } : {}) });
|
|
522
|
-
|
|
535
|
+
return;
|
|
523
536
|
}
|
|
524
537
|
// The <alt> element (DITA 1.1+) wins over the deprecated @alt, as in publishing.
|
|
525
538
|
const altEl = (children).find((c) => isElement(c) && c.name.toLowerCase() === "alt");
|
|
@@ -529,17 +542,16 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
|
|
|
529
542
|
src: imgHref,
|
|
530
543
|
...(alt ? { alt } : {}),
|
|
531
544
|
});
|
|
532
|
-
|
|
545
|
+
return;
|
|
533
546
|
}
|
|
534
547
|
if (name === "indexterm" || name === "draft-comment" || name === "data") {
|
|
535
548
|
noteOnce(ctx, `<${name}>`, "metadata element dropped from output");
|
|
536
|
-
|
|
549
|
+
return;
|
|
537
550
|
}
|
|
538
551
|
// Unknown inline element: keep the text, note the loss
|
|
539
552
|
noteOnce(ctx, `<${name}>`, "unrecognized inline element — text kept, markup dropped");
|
|
540
553
|
out.push(...convertInline(children, ctx, formatting));
|
|
541
554
|
}
|
|
542
|
-
return mergeAdjacentText(out);
|
|
543
555
|
};
|
|
544
556
|
/** Merge neighbouring text spans with identical formatting (recursive calls
|
|
545
557
|
* push whole runs, so pushText's local merging alone is not enough). */
|
package/dist/lines.d.ts
ADDED
package/dist/lines.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offsets → 1-based line numbers, counting lines as checkWellFormed does:
|
|
3
|
+
* `\n`, `\r\n` and a lone `\r` each end one.
|
|
4
|
+
*/
|
|
5
|
+
export function lineLocator(text) {
|
|
6
|
+
const starts = [0];
|
|
7
|
+
for (let i = 0; i < text.length; i++) {
|
|
8
|
+
const c = text.charCodeAt(i);
|
|
9
|
+
if (c === 10 || (c === 13 && text.charCodeAt(i + 1) !== 10))
|
|
10
|
+
starts.push(i + 1);
|
|
11
|
+
}
|
|
12
|
+
return (offset) => {
|
|
13
|
+
let lo = 0;
|
|
14
|
+
let hi = starts.length - 1;
|
|
15
|
+
while (lo < hi) {
|
|
16
|
+
const mid = (lo + hi + 1) >> 1;
|
|
17
|
+
if (starts[mid] <= offset)
|
|
18
|
+
lo = mid;
|
|
19
|
+
else
|
|
20
|
+
hi = mid - 1;
|
|
21
|
+
}
|
|
22
|
+
return lo + 1;
|
|
23
|
+
};
|
|
24
|
+
}
|
package/dist/parse.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { checkWellFormed } from "./well-formed.js";
|
|
2
2
|
import { applyClassDefaults } from "./class-defaults-apply.js";
|
|
3
|
+
import { lineLocator } from "./lines.js";
|
|
3
4
|
import { parseDocument } from "htmlparser2";
|
|
4
5
|
import { convertChildren, convertItemContent } from "./blocks.js";
|
|
5
6
|
import { textOf, convertInline, trimInline, collapseWhitespace } from "./inline.js";
|
|
@@ -89,6 +90,8 @@ const convertSecondaryTopic = (el, ctx, level, bindConrefSite) => {
|
|
|
89
90
|
for (const child of el.children) {
|
|
90
91
|
if (!isElement(child))
|
|
91
92
|
continue;
|
|
93
|
+
if (ctx.position)
|
|
94
|
+
ctx.position.el = child;
|
|
92
95
|
const name = child.name.toLowerCase();
|
|
93
96
|
if (name === "title" || name === "glossterm") {
|
|
94
97
|
out.push({
|
|
@@ -98,7 +101,9 @@ const convertSecondaryTopic = (el, ctx, level, bindConrefSite) => {
|
|
|
98
101
|
...(el.attribs?.id ? { id: el.attribs.id } : {}),
|
|
99
102
|
},
|
|
100
103
|
inline: trimInline(convertInline(child.children, inner)),
|
|
101
|
-
...(el.attribs?.id
|
|
104
|
+
...(el.attribs?.id || ctx.lineOf?.(child) !== undefined
|
|
105
|
+
? { meta: { ...(el.attribs?.id ? { dita: { id: el.attribs.id } } : {}), ...(ctx.lineOf?.(child) !== undefined ? { line: ctx.lineOf(child) } : {}) } }
|
|
106
|
+
: {}),
|
|
102
107
|
});
|
|
103
108
|
headingEmitted = true;
|
|
104
109
|
continue;
|
|
@@ -220,17 +225,23 @@ const extractProlog = (el, meta, ctx) => {
|
|
|
220
225
|
export function ditaTopicToTree(xml, options = {}) {
|
|
221
226
|
const topicPath = options.topicPath ?? "";
|
|
222
227
|
const notes = [];
|
|
223
|
-
|
|
228
|
+
// Every note carries the line of the element converting when it was raised
|
|
229
|
+
// (a shared holder: contexts are copied, the holder is not)
|
|
230
|
+
const lineOf = lineLocator(xml);
|
|
231
|
+
const position = {};
|
|
232
|
+
const lineAt = (el) => el?.startIndex != null ? lineOf(el.startIndex) : undefined;
|
|
233
|
+
const note = (construct, detail, line = lineAt(position.el)) => notes.push({ topicPath, construct, detail, ...(line ? { line } : {}) });
|
|
224
234
|
// The parser repairs malformed XML silently: say so, with a line
|
|
225
235
|
const malformed = checkWellFormed(xml);
|
|
226
236
|
if (malformed)
|
|
227
|
-
note("<xml>", `not well-formed XML, line ${malformed.line}: ${malformed.message} — the converted content may be incomplete
|
|
228
|
-
const doc = parseDocument(xml, { xmlMode: true });
|
|
237
|
+
note("<xml>", `not well-formed XML, line ${malformed.line}: ${malformed.message} — the converted content may be incomplete`, malformed.line);
|
|
238
|
+
const doc = parseDocument(xml, { xmlMode: true, withStartIndices: true });
|
|
229
239
|
// Raw source: fill in the @class the DTDs default, so specializations convert as their base
|
|
230
240
|
if (options.classDefaults) {
|
|
231
|
-
const
|
|
241
|
+
const firstOf = new Map();
|
|
242
|
+
const undeclared = applyClassDefaults(doc.children, firstOf);
|
|
232
243
|
if (undeclared.length) {
|
|
233
|
-
note("@class", `no DITA 1.3 class default for ${undeclared.map((n) => `<${n}>`).join(", ")} — converted by name only
|
|
244
|
+
note("@class", `no DITA 1.3 class default for ${undeclared.map((n) => `<${n}>`).join(", ")} — converted by name only`, lineAt(firstOf.get(undeclared[0])));
|
|
234
245
|
}
|
|
235
246
|
}
|
|
236
247
|
const byClass = !!options.publish || !!options.classDefaults;
|
|
@@ -283,7 +294,9 @@ export function ditaTopicToTree(xml, options = {}) {
|
|
|
283
294
|
note,
|
|
284
295
|
seenConstructs: new Set(),
|
|
285
296
|
headingLevel: 2,
|
|
297
|
+
position,
|
|
286
298
|
...(options.classDefaults ? { classes: true } : {}),
|
|
299
|
+
...(options.sourceLines ? { lineOf: lineAt } : {}),
|
|
287
300
|
...(bindConrefSite ? { conref: bindConrefSite(root.attribs?.id) } : {}),
|
|
288
301
|
...(options.publish
|
|
289
302
|
? { publish: options.publish, trademarks: new Set(), footnotes: [], topicId: root.attribs?.id, anchorIds: renamedElementIds(doc.children) }
|
|
@@ -297,6 +310,7 @@ export function ditaTopicToTree(xml, options = {}) {
|
|
|
297
310
|
for (const child of root.children) {
|
|
298
311
|
if (!isElement(child))
|
|
299
312
|
continue;
|
|
313
|
+
position.el = child;
|
|
300
314
|
const name = child.name.toLowerCase();
|
|
301
315
|
if (name === "title" || name === "glossterm") {
|
|
302
316
|
// The page title is plain text: a badge in it keeps only its words
|
|
@@ -351,9 +365,12 @@ export function ditaTopicToTree(xml, options = {}) {
|
|
|
351
365
|
}
|
|
352
366
|
// Sibling topics under a composite wrapper convert the same way
|
|
353
367
|
for (const sibling of roots.slice(1)) {
|
|
368
|
+
position.el = sibling;
|
|
354
369
|
// Publishing: DITA-OT titles sibling topics in one file like the first (h1)
|
|
355
370
|
tree.push(...convertSecondaryTopic(sibling, ctx, options.publish ? 1 : 2, bindConrefSite));
|
|
356
371
|
}
|
|
372
|
+
// Notes about the whole file (duplicate ids) are about no one element
|
|
373
|
+
position.el = undefined;
|
|
357
374
|
// DITA element ids are unique per TOPIC, not per file — after inlining
|
|
358
375
|
// multiple topics, colliding ids mean fragment links can mis-target.
|
|
359
376
|
// Known loss, never silent loss: report every duplicate anchor.
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface DitaConversionNote {
|
|
|
10
10
|
/** Construct that triggered the note (element/attribute name). */
|
|
11
11
|
construct: string;
|
|
12
12
|
detail: string;
|
|
13
|
+
/**
|
|
14
|
+
* The 1-based source line the note is about: the element that raised it, or
|
|
15
|
+
* failing that the block being converted (absent when there is none, as for
|
|
16
|
+
* "no topic root").
|
|
17
|
+
*/
|
|
18
|
+
line?: number;
|
|
13
19
|
}
|
|
14
20
|
/** Topic-level metadata extracted alongside the block tree. */
|
|
15
21
|
export interface DitaTopicMeta {
|
|
@@ -162,6 +168,12 @@ export interface DitaParseOptions {
|
|
|
162
168
|
* noted once. Off by default (it changes the output for such elements).
|
|
163
169
|
*/
|
|
164
170
|
classDefaults?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Put each block node's 1-based source line on `meta.line` (the line of the
|
|
173
|
+
* element it came from), so a host can map the rendered page back to the
|
|
174
|
+
* source; render-html writes it as `data-line` when asked. Off by default.
|
|
175
|
+
*/
|
|
176
|
+
sourceLines?: boolean;
|
|
165
177
|
}
|
|
166
178
|
/** DITA topic root elements this importer understands. */
|
|
167
179
|
export declare const DITA_TOPIC_TYPES: readonly ["concept", "task", "reference", "topic", "glossentry"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-dita",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.126",
|
|
4
4
|
"description": "DITA topic XML → TreeNode importer with conversion-note ledger; composes with format-dogsbay-md for .dita → .md migration. Browser-safe: no Node built-ins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"domhandler": "^5.0.3",
|
|
30
30
|
"htmlparser2": "^10.1.0",
|
|
31
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
32
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
31
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.126",
|
|
32
|
+
"@dogsbay/types": "0.2.0-beta.126"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|