@dogsbay/format-dita 0.2.0-beta.122 → 0.2.0-beta.124

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.
Files changed (46) hide show
  1. package/README.md +19 -1
  2. package/dist/anchors.d.ts +0 -1
  3. package/dist/anchors.js +0 -1
  4. package/dist/blocks.d.ts +2 -1
  5. package/dist/blocks.js +289 -37
  6. package/dist/class-defaults-apply.d.ts +15 -0
  7. package/dist/class-defaults-apply.js +40 -0
  8. package/dist/class-defaults.d.ts +8 -0
  9. package/dist/class-defaults.js +625 -0
  10. package/dist/conref.d.ts +0 -1
  11. package/dist/conref.js +1 -2
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +5 -1
  14. package/dist/inline.d.ts +2 -9
  15. package/dist/inline.js +50 -8
  16. package/dist/markdown.d.ts +0 -1
  17. package/dist/markdown.js +1 -2
  18. package/dist/parse.d.ts +0 -1
  19. package/dist/parse.js +24 -4
  20. package/dist/publish-support.d.ts +0 -1
  21. package/dist/publish-support.js +0 -1
  22. package/dist/qualifiers.d.ts +25 -0
  23. package/dist/qualifiers.js +40 -0
  24. package/dist/types.d.ts +10 -1
  25. package/dist/types.js +0 -1
  26. package/dist/well-formed.d.ts +14 -0
  27. package/dist/well-formed.js +110 -0
  28. package/package.json +5 -4
  29. package/dist/anchors.d.ts.map +0 -1
  30. package/dist/anchors.js.map +0 -1
  31. package/dist/blocks.d.ts.map +0 -1
  32. package/dist/blocks.js.map +0 -1
  33. package/dist/conref.d.ts.map +0 -1
  34. package/dist/conref.js.map +0 -1
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/inline.d.ts.map +0 -1
  38. package/dist/inline.js.map +0 -1
  39. package/dist/markdown.d.ts.map +0 -1
  40. package/dist/markdown.js.map +0 -1
  41. package/dist/parse.d.ts.map +0 -1
  42. package/dist/parse.js.map +0 -1
  43. package/dist/publish-support.d.ts.map +0 -1
  44. package/dist/publish-support.js.map +0 -1
  45. package/dist/types.d.ts.map +0 -1
  46. package/dist/types.js.map +0 -1
package/dist/conref.d.ts CHANGED
@@ -25,4 +25,3 @@ export interface ConrefPlan extends ConrefPlanApi {
25
25
  * snippet naming) follows sorted keys, not discovery order.
26
26
  */
27
27
  export declare function planConrefs(files: ConrefCorpusFile[]): ConrefPlan;
28
- //# sourceMappingURL=conref.d.ts.map
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,7 @@ 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
- //# sourceMappingURL=index.d.ts.map
19
+ export { DB_QUALIFIERS } from "./blocks.js";
20
+ export { checkWellFormed, type WellFormedProblem } from "./well-formed.js";
21
+ export { DITA_CLASS_DEFAULTS, DITA_MAP_CLASS_DEFAULTS } from "./class-defaults.js";
22
+ export { applyClassDefaults, classDefault } from "./class-defaults-apply.js";
package/dist/index.js CHANGED
@@ -16,4 +16,8 @@ export { DITA_TOPIC_TYPES, renameDitaHref, relativeIncludePath, } from "./types.
16
16
  export { ditaTopicToTree } from "./parse.js";
17
17
  export { ditaTopicToDogsbayMd } from "./markdown.js";
18
18
  export { planConrefs, } from "./conref.js";
19
- //# sourceMappingURL=index.js.map
19
+ // The closed db- qualifier vocabulary (tones, tab variants, scoped themes)
20
+ export { DB_QUALIFIERS } from "./blocks.js";
21
+ export { checkWellFormed } from "./well-formed.js";
22
+ export { DITA_CLASS_DEFAULTS, DITA_MAP_CLASS_DEFAULTS } from "./class-defaults.js";
23
+ export { applyClassDefaults, classDefault } from "./class-defaults-apply.js";
package/dist/inline.d.ts CHANGED
@@ -1,11 +1,3 @@
1
- /**
2
- * DITA inline elements → InlineNode[] (Notion-style flat annotations).
3
- *
4
- * DITA's semantic inlines (uicontrol, filepath, apiname, …) map to their
5
- * conventional visual rendering; the semantic distinction itself cannot be
6
- * carried by the closed InlineNode union, so mappings that lose semantics
7
- * are recorded as conversion notes exactly once per construct per topic.
8
- */
9
1
  import type { Element, AnyNode } from "domhandler";
10
2
  import type { InlineNode } from "@dogsbay/types";
11
3
  import { type ConrefSiteContext, type ConrefSnippetRef, type PublishContext } from "./types.js";
@@ -17,6 +9,8 @@ export interface InlineContext {
17
9
  conref?: ConrefSiteContext;
18
10
  /** Publishing mode: DITA-OT preprocessed input (absent for migration). */
19
11
  publish?: PublishContext;
12
+ /** Every element carries @class (DTD defaults filled in): resolve unknown names by it, as publishing does. */
13
+ classes?: boolean;
20
14
  /** Publishing: trademarks already marked in this topic (DITA-OT marks only the first). */
21
15
  trademarks?: Set<string>;
22
16
  /** Publishing: the number label of each titled table and figure ("Table 1. "). */
@@ -78,4 +72,3 @@ export declare const convertInline: (nodes: AnyNode[], ctx: InlineContext, forma
78
72
  /** Trim leading/trailing whitespace-only fringe from an inline run. */
79
73
  export declare const trimInline: (nodes: InlineNode[]) => InlineNode[];
80
74
  export {};
81
- //# sourceMappingURL=inline.d.ts.map
package/dist/inline.js CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * DITA inline elements → InlineNode[] (Notion-style flat annotations).
3
+ *
4
+ * DITA's semantic inlines (uicontrol, filepath, apiname, …) map to their
5
+ * conventional visual rendering; the semantic distinction itself cannot be
6
+ * carried by the closed InlineNode union, so mappings that lose semantics
7
+ * are recorded as conversion notes exactly once per construct per topic.
8
+ */
9
+ import { DB_QUALIFIERS, outputTokens } from "./qualifiers.js";
1
10
  import { publishHref } from "./types.js";
2
11
  import { anchorId } from "./anchors.js";
3
12
  /**
@@ -74,7 +83,7 @@ const BOLD_LIKE = new Set(["b", "strong", "uicontrol", "wintitle", "menucascade"
74
83
  */
75
84
  export const elementName = (el, ctx, known) => {
76
85
  const raw = el.name.toLowerCase();
77
- if (!ctx.publish || known(raw))
86
+ if (!(ctx.publish || ctx.classes) || known(raw))
78
87
  return raw;
79
88
  const tokens = (el.attribs?.class ?? "").trim().split(/\s+/).slice(1).reverse();
80
89
  for (const token of tokens) {
@@ -249,6 +258,31 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
249
258
  }
250
259
  ctx.note(node.attribs.conref ? "@conref" : "@conkeyref", `<${name}> pulls content from "${ref}" — ${conrefLedgerDetail(node.attribs, snippet, ctx.conref !== undefined)}`);
251
260
  }
261
+ // `<ph outputclass="db-badge tone-success">Beta</ph>`: a badge, in running
262
+ // text or a title. DITA-OT shows the plain text.
263
+ if (name === "ph" && /(^|\s)db-badge(\s|$)/.test(node.attribs?.outputclass ?? "")) {
264
+ const key = node.attribs?.keyref && ctx.publish ? ctx.publish.keys.get(node.attribs.keyref) : undefined;
265
+ const text = collapseWhitespace(textOf(children)).trim() || key?.text || key?.linktext || "";
266
+ if (text) {
267
+ let variant;
268
+ for (const t of outputTokens(node.attribs?.outputclass)) {
269
+ const tone = /^tone-(.+)$/.exec(t);
270
+ if (!tone)
271
+ continue;
272
+ if (DB_QUALIFIERS.tones.includes(tone[1]))
273
+ variant = tone[1];
274
+ else
275
+ noteOnce(ctx, `db-badge ${t}`, `"${t}" is not a tone (${DB_QUALIFIERS.tones.join(", ")}) — default badge`);
276
+ }
277
+ out.push({ type: "badge", text, ...(variant ? { variant } : {}) });
278
+ continue;
279
+ }
280
+ // A content-less keyref falls through to the key's text ({{ key }}): the badge look is what is lost
281
+ if (node.attribs?.keyref)
282
+ noteOnce(ctx, "db-badge keyref", `db-badge with only a keyref ("${node.attribs.keyref}") — shown as the key's text, without the badge`);
283
+ else
284
+ noteOnce(ctx, "db-badge", "db-badge with no text — nothing to show");
285
+ }
252
286
  // Key-based text reuse (<ph keyref="product-name"/>, <keyword keyref=…>).
253
287
  // Authored fallback content always wins — it is the only text that is
254
288
  // guaranteed to exist (href-only keys have no text to resolve). Only
@@ -362,13 +396,22 @@ export const convertInline = (nodes, ctx, formatting = {}) => {
362
396
  pushText(" ");
363
397
  continue;
364
398
  }
365
- if (name === "menucascade" && ctx.publish) {
366
- // Publishing: "File > Save Project", as DITA-OT's HTML renders it
367
- node.children.filter(isElement).forEach((part, i) => {
368
- if (i > 0)
399
+ if (name === "menucascade") {
400
+ // "File > Save Project", as DITA-OT's HTML renders it: each step keeps
401
+ // its own look (a <uicontrol> is bold), joined by " > "
402
+ // Separators only between steps that produced something: a step that
403
+ // converts to nothing (an unresolved conref) left a leading " > ", which
404
+ // a line start reads as a blockquote
405
+ let any = false;
406
+ for (const part of node.children.filter(isElement)) {
407
+ const converted = convertInline([part], ctx, formatting);
408
+ if (!converted.length)
409
+ continue;
410
+ if (any)
369
411
  pushText(" > ");
370
- out.push(...convertInline([part], ctx, formatting));
371
- });
412
+ out.push(...converted);
413
+ any = true;
414
+ }
372
415
  continue;
373
416
  }
374
417
  if (name === "shortcut") {
@@ -549,4 +592,3 @@ export const trimInline = (nodes) => {
549
592
  }
550
593
  return result;
551
594
  };
552
- //# sourceMappingURL=inline.js.map
@@ -9,4 +9,3 @@ export interface DitaMarkdownResult extends DitaTopicResult {
9
9
  * (`dita.id`, original path) for round-trip.
10
10
  */
11
11
  export declare function ditaTopicToDogsbayMd(xml: string, options?: DitaParseOptions): DitaMarkdownResult;
12
- //# sourceMappingURL=markdown.d.ts.map
package/dist/markdown.js CHANGED
@@ -1,4 +1,4 @@
1
- import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
1
+ import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md/browser";
2
2
  import { ditaTopicToTree } from "./parse.js";
3
3
  /** DITA topic type → dogsbay contentType frontmatter value. */
4
4
  const CONTENT_TYPE_MAP = {
@@ -69,4 +69,3 @@ export function ditaTopicToDogsbayMd(xml, options = {}) {
69
69
  });
70
70
  return { ...result, markdown: markdown.endsWith("\n") ? markdown : `${markdown}\n` };
71
71
  }
72
- //# sourceMappingURL=markdown.js.map
package/dist/parse.d.ts CHANGED
@@ -10,4 +10,3 @@ import { type DitaParseOptions, type DitaTopicResult } from "./types.js";
10
10
  * so multi-topic (chunked) files lose nothing.
11
11
  */
12
12
  export declare function ditaTopicToTree(xml: string, options?: DitaParseOptions): DitaTopicResult;
13
- //# sourceMappingURL=parse.d.ts.map
package/dist/parse.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { checkWellFormed } from "./well-formed.js";
2
+ import { applyClassDefaults } from "./class-defaults-apply.js";
1
3
  import { parseDocument } from "htmlparser2";
2
4
  import { convertChildren, convertItemContent } from "./blocks.js";
3
5
  import { textOf, convertInline, trimInline, collapseWhitespace } from "./inline.js";
@@ -103,7 +105,7 @@ const convertSecondaryTopic = (el, ctx, level, bindConrefSite) => {
103
105
  }
104
106
  if (name === "titlealts")
105
107
  continue;
106
- if (isTopicElement(child, !!ctx.publish)) {
108
+ if (isTopicElement(child, !!ctx.publish || !!ctx.classes)) {
107
109
  out.push(...convertSecondaryTopic(child, ctx, bodyLevel, bindConrefSite));
108
110
  continue;
109
111
  }
@@ -219,8 +221,20 @@ export function ditaTopicToTree(xml, options = {}) {
219
221
  const topicPath = options.topicPath ?? "";
220
222
  const notes = [];
221
223
  const note = (construct, detail) => notes.push({ topicPath, construct, detail });
224
+ // The parser repairs malformed XML silently: say so, with a line
225
+ const malformed = checkWellFormed(xml);
226
+ if (malformed)
227
+ note("<xml>", `not well-formed XML, line ${malformed.line}: ${malformed.message} — the converted content may be incomplete`);
222
228
  const doc = parseDocument(xml, { xmlMode: true });
223
- const roots = findTopicRoots(doc.children, !!options.publish);
229
+ // Raw source: fill in the @class the DTDs default, so specializations convert as their base
230
+ if (options.classDefaults) {
231
+ const undeclared = applyClassDefaults(doc.children);
232
+ if (undeclared.length) {
233
+ note("@class", `no DITA 1.3 class default for ${undeclared.map((n) => `<${n}>`).join(", ")} — converted by name only`);
234
+ }
235
+ }
236
+ const byClass = !!options.publish || !!options.classDefaults;
237
+ const roots = findTopicRoots(doc.children, byClass);
224
238
  const root = roots[0] ?? null;
225
239
  if (!root) {
226
240
  note("<topic>", "no DITA topic root element found (concept/task/reference/topic/glossentry)");
@@ -269,6 +283,7 @@ export function ditaTopicToTree(xml, options = {}) {
269
283
  note,
270
284
  seenConstructs: new Set(),
271
285
  headingLevel: 2,
286
+ ...(options.classDefaults ? { classes: true } : {}),
272
287
  ...(bindConrefSite ? { conref: bindConrefSite(root.attribs?.id) } : {}),
273
288
  ...(options.publish
274
289
  ? { publish: options.publish, trademarks: new Set(), footnotes: [], topicId: root.attribs?.id, anchorIds: renamedElementIds(doc.children) }
@@ -284,6 +299,10 @@ export function ditaTopicToTree(xml, options = {}) {
284
299
  continue;
285
300
  const name = child.name.toLowerCase();
286
301
  if (name === "title" || name === "glossterm") {
302
+ // The page title is plain text: a badge in it keeps only its words
303
+ if (hasBadge(child)) {
304
+ note("@outputclass", `db-badge in the topic title is plain text in the page title — put it in a section title or the body to show it`);
305
+ }
287
306
  // glossentry topics carry their title in <glossterm>
288
307
  if (options.publish) {
289
308
  // Publishing: a footnote in the title is the page's first note; the title text leaves it out
@@ -323,7 +342,7 @@ export function ditaTopicToTree(xml, options = {}) {
323
342
  extractProlog(child, meta, ctx);
324
343
  continue;
325
344
  }
326
- if (isTopicElement(child, !!options.publish)) {
345
+ if (isTopicElement(child, byClass)) {
327
346
  // Nested topic: convert inline as a section (chunked authoring)
328
347
  tree.push(...convertSecondaryTopic(child, ctx, 2, bindConrefSite));
329
348
  continue;
@@ -392,4 +411,5 @@ export function ditaTopicToTree(xml, options = {}) {
392
411
  }
393
412
  return { tree, meta, notes };
394
413
  }
395
- //# sourceMappingURL=parse.js.map
414
+ /** A `<ph outputclass="db-badge">` anywhere inside an element. */
415
+ const hasBadge = (el) => el.children.some((c) => isElement(c) && (/(^|\s)db-badge(\s|$)/.test(c.attribs?.outputclass ?? "") || hasBadge(c)));
@@ -8,4 +8,3 @@ export { renamedElementIds } from "./anchors.js";
8
8
  export { convertInline, trimInline, type InlineContext } from "./inline.js";
9
9
  export { PUBLISH_PREREQUISITES_MARKER, PUBLISH_RELATED_LINKS_MARKER } from "./blocks.js";
10
10
  export { publishHref, publishLabel, publishLanguage, publishString, type PublishContext, type PublishKey, type PublishStrings, } from "./types.js";
11
- //# sourceMappingURL=publish-support.d.ts.map
@@ -8,4 +8,3 @@ export { renamedElementIds } from "./anchors.js";
8
8
  export { convertInline, trimInline } from "./inline.js";
9
9
  export { PUBLISH_PREREQUISITES_MARKER, PUBLISH_RELATED_LINKS_MARKER } from "./blocks.js";
10
10
  export { publishHref, publishLabel, publishLanguage, publishString, } from "./types.js";
11
- //# sourceMappingURL=publish-support.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The closed `db-` vocabulary (plans/dita-variants-themes.md,
3
+ * plans/dita-components-phase3.md): the looks a component may take and the
4
+ * scoped themes. Tones, tab variants and themes mirror format-astro's
5
+ * tone-classes.ts, which the renderers draw from; the CLI test
6
+ * db-qualifiers.test.ts fails when they drift. Closed lists, so a Schematron
7
+ * house rule can enforce them.
8
+ */
9
+ export declare const DB_QUALIFIERS: {
10
+ readonly tones: readonly ["info", "success", "warning", "danger"];
11
+ readonly tabVariants: readonly ["pills", "underline"];
12
+ readonly themes: readonly ["dark"];
13
+ /** db-button: Button's variants (the default needs no token; `primary` is its alias). */
14
+ readonly buttonVariants: readonly ["primary", "secondary", "outline", "ghost", "link", "destructive"];
15
+ /** db-grid `cols-N` and a cell's `span-N`. */
16
+ readonly gridColumns: readonly [1, 2, 3, 4, 5, 6];
17
+ };
18
+ /** The `@outputclass` tokens of an element, `db-theme-*` (a scope, not a component) excluded. */
19
+ export declare const outputTokens: (outputclass: string | undefined) => string[];
20
+ /**
21
+ * A YouTube video id from the URL forms people paste: watch?v=, youtu.be/,
22
+ * /embed/, /shorts/, /live/. Undefined for anything else (a db-video that is
23
+ * not YouTube stays a link).
24
+ */
25
+ export declare const youtubeId: (href: string) => string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The closed `db-` vocabulary (plans/dita-variants-themes.md,
3
+ * plans/dita-components-phase3.md): the looks a component may take and the
4
+ * scoped themes. Tones, tab variants and themes mirror format-astro's
5
+ * tone-classes.ts, which the renderers draw from; the CLI test
6
+ * db-qualifiers.test.ts fails when they drift. Closed lists, so a Schematron
7
+ * house rule can enforce them.
8
+ */
9
+ export const DB_QUALIFIERS = {
10
+ tones: ["info", "success", "warning", "danger"],
11
+ tabVariants: ["pills", "underline"],
12
+ themes: ["dark"],
13
+ /** db-button: Button's variants (the default needs no token; `primary` is its alias). */
14
+ buttonVariants: ["primary", "secondary", "outline", "ghost", "link", "destructive"],
15
+ /** db-grid `cols-N` and a cell's `span-N`. */
16
+ gridColumns: [1, 2, 3, 4, 5, 6],
17
+ };
18
+ /** The `@outputclass` tokens of an element, `db-theme-*` (a scope, not a component) excluded. */
19
+ export const outputTokens = (outputclass) => (outputclass ?? "").split(/\s+/).filter((t) => t && !/^db-theme-/i.test(t));
20
+ /**
21
+ * A YouTube video id from the URL forms people paste: watch?v=, youtu.be/,
22
+ * /embed/, /shorts/, /live/. Undefined for anything else (a db-video that is
23
+ * not YouTube stays a link).
24
+ */
25
+ export const youtubeId = (href) => {
26
+ let url;
27
+ try {
28
+ url = new URL(href);
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ const host = url.hostname.replace(/^(www|m)\./, "");
34
+ const id = host === "youtu.be"
35
+ ? url.pathname.slice(1).split("/")[0]
36
+ : host === "youtube.com" || host === "youtube-nocookie.com"
37
+ ? url.searchParams.get("v") ?? /^\/(?:embed|shorts|live)\/([^/]+)/.exec(url.pathname)?.[1]
38
+ : undefined;
39
+ return id && /^[A-Za-z0-9_-]{6,20}$/.test(id) ? id : undefined;
40
+ };
package/dist/types.d.ts CHANGED
@@ -152,6 +152,16 @@ export interface DitaParseOptions {
152
152
  * its behavior unchanged.
153
153
  */
154
154
  publish?: PublishContext;
155
+ /**
156
+ * Fill in each missing @class from the DITA 1.3 DTD defaults (612 elements,
157
+ * see class-defaults.ts) before converting, as parsing against the DTDs
158
+ * would. Raw source has no @class, so without it an element is known only by
159
+ * its name, and a specialization the converter does not know by name loses
160
+ * its markup; with it, it converts as what it specializes. An existing
161
+ * @class is kept; a name the DTDs do not declare stays unclassed and is
162
+ * noted once. Off by default (it changes the output for such elements).
163
+ */
164
+ classDefaults?: boolean;
155
165
  }
156
166
  /** DITA topic root elements this importer understands. */
157
167
  export declare const DITA_TOPIC_TYPES: readonly ["concept", "task", "reference", "topic", "glossentry"];
@@ -166,4 +176,3 @@ export declare const relativeIncludePath: (fromDir: string, targetFile: string)
166
176
  /** A link to a DITA topic as a page link: `renameDitaHref`, or the publisher's renaming, which also knows repeated element ids. */
167
177
  export declare const publishHref: (publish: PublishContext | undefined, href: string) => string;
168
178
  export declare const renameDitaHref: (href: string) => string;
169
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -71,4 +71,3 @@ export const renameDitaHref = (href) => {
71
71
  ? `${renamed}#${parts.slice(1).join("/")}`
72
72
  : `${renamed}#${fragment}`;
73
73
  };
74
- //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A well-formedness check for a topic's XML. htmlparser2 (xmlMode) is
3
+ * lenient: an unclosed element is closed for you and a stray end tag is
4
+ * dropped, so a malformed topic converts to a plausible partial tree with no
5
+ * sign of trouble. This scans the tags (skipping comments, CDATA, processing
6
+ * instructions and the DOCTYPE, quotes respected) and reports the FIRST
7
+ * problem with its line, which the converter puts on the note ledger.
8
+ * It checks tag balance and names, not the DTD.
9
+ */
10
+ export interface WellFormedProblem {
11
+ line: number;
12
+ message: string;
13
+ }
14
+ export declare function checkWellFormed(xml: string): WellFormedProblem | undefined;
@@ -0,0 +1,110 @@
1
+ // XML names are Unicode (<über>, <ñ>), not ASCII
2
+ const NAME = /^[\p{L}_:][\p{L}\p{M}\p{N}_:.\-\u00B7]*/u;
3
+ export function checkWellFormed(xml) {
4
+ const stack = [];
5
+ let line = 1;
6
+ let i = 0;
7
+ let roots = 0;
8
+ const advance = (to) => {
9
+ // \n, \r\n and a lone \r (old Mac files) each end a line
10
+ for (let k = i; k < to; k++) {
11
+ const c = xml.charCodeAt(k);
12
+ if (c === 10 || (c === 13 && xml.charCodeAt(k + 1) !== 10))
13
+ line++;
14
+ }
15
+ i = to;
16
+ };
17
+ const skipTo = (end, what) => {
18
+ const at = xml.indexOf(end, i);
19
+ if (at < 0)
20
+ return { line, message: `unterminated ${what}` };
21
+ advance(at + end.length);
22
+ return undefined;
23
+ };
24
+ while (i < xml.length) {
25
+ const lt = xml.indexOf("<", i);
26
+ if (lt < 0) {
27
+ advance(xml.length);
28
+ break;
29
+ }
30
+ if (!stack.length && roots > 0 && xml.slice(i, lt).trim())
31
+ return { line, message: "text after the root element" };
32
+ advance(lt);
33
+ let p;
34
+ if (xml.startsWith("<!--", i))
35
+ p = skipTo("-->", "comment");
36
+ else if (xml.startsWith("<![CDATA[", i))
37
+ p = skipTo("]]>", "CDATA section");
38
+ else if (xml.startsWith("<?", i))
39
+ p = skipTo("?>", "processing instruction");
40
+ else if (xml.startsWith("<!", i)) {
41
+ // <!DOCTYPE …>, possibly with an internal subset in [ … ]: quoted
42
+ // literals ("a[b.dtd", an ENTITY value holding '>') are skipped
43
+ let k = i + 2, quote = "", subset = false;
44
+ for (; k < xml.length; k++) {
45
+ const c = xml[k];
46
+ if (quote) {
47
+ if (c === quote)
48
+ quote = "";
49
+ }
50
+ else if (c === '"' || c === "'")
51
+ quote = c;
52
+ else if (c === "[")
53
+ subset = true;
54
+ else if (c === "]")
55
+ subset = false;
56
+ else if (c === ">" && !subset)
57
+ break;
58
+ }
59
+ if (k >= xml.length)
60
+ return { line, message: "unterminated DOCTYPE" };
61
+ advance(k + 1);
62
+ }
63
+ else {
64
+ const start = line;
65
+ const closing = xml[i + 1] === "/";
66
+ const name = NAME.exec(xml.slice(i + (closing ? 2 : 1)))?.[0];
67
+ if (!name)
68
+ return { line, message: "a '<' that starts no tag (write &lt; in text)" };
69
+ // The tag's end, past quoted attribute values (which may hold '>')
70
+ let k = i + (closing ? 2 : 1) + name.length;
71
+ let quote = "";
72
+ for (; k < xml.length; k++) {
73
+ const c = xml[k];
74
+ if (quote) {
75
+ if (c === quote)
76
+ quote = "";
77
+ }
78
+ else if (c === '"' || c === "'")
79
+ quote = c;
80
+ else if (c === "<")
81
+ return { line: start, message: `<${closing ? "/" : ""}${name}> is not closed with '>'` };
82
+ else if (c === ">")
83
+ break;
84
+ }
85
+ if (k >= xml.length)
86
+ return { line: start, message: `<${closing ? "/" : ""}${name}> is not closed with '>'` };
87
+ const selfClosing = !closing && xml[k - 1] === "/";
88
+ advance(k + 1);
89
+ if (closing) {
90
+ const open = stack.pop();
91
+ if (!open)
92
+ return { line: start, message: `</${name}> has no matching start tag` };
93
+ if (open.name !== name)
94
+ return { line: start, message: `</${name}> closes <${open.name}> (opened on line ${open.line})` };
95
+ }
96
+ else {
97
+ if (!stack.length && roots++ > 0)
98
+ return { line: start, message: `<${name}>: a second root element` };
99
+ if (!selfClosing)
100
+ stack.push({ name, line: start });
101
+ }
102
+ }
103
+ if (p)
104
+ return p;
105
+ }
106
+ const open = stack.pop();
107
+ if (open)
108
+ return { line: open.line, message: `<${open.name}> is never closed` };
109
+ return undefined;
110
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dogsbay/format-dita",
3
- "version": "0.2.0-beta.122",
4
- "description": "DITA topic XML → TreeNode importer with conversion-note ledger; composes with format-dogsbay-md for .dita → .md migration",
3
+ "version": "0.2.0-beta.124",
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",
7
7
  "types": "dist/index.d.ts",
@@ -21,14 +21,15 @@
21
21
  ],
22
22
  "devDependencies": {
23
23
  "@types/node": "^25.9.5",
24
+ "esbuild": "0.28.1",
24
25
  "typescript": "^5.9.3",
25
26
  "vitest": "^4.1.10"
26
27
  },
27
28
  "dependencies": {
28
29
  "domhandler": "^5.0.3",
29
30
  "htmlparser2": "^10.1.0",
30
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.122",
31
- "@dogsbay/types": "0.2.0-beta.122"
31
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.124",
32
+ "@dogsbay/types": "0.2.0-beta.124"
32
33
  },
33
34
  "license": "MIT",
34
35
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"anchors.d.ts","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,YAAY,CAAC;AAKnD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,EAAE,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAqBtE,CAAC;AAEF,6GAA6G;AAC7G,eAAO,MAAM,QAAQ,GAAI,KAAK;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,EAAE,IAAI,MAAM,KAAG,MACjB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"anchors.js","sourceRoot":"","sources":["../src/anchors.ts"],"names":[],"mappings":"AASA,MAAM,SAAS,GAAG,CAAC,IAAa,EAAmB,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC;AAC1E,MAAM,OAAO,GAAG,CAAC,EAAW,EAAW,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAgB,EAAuB,EAAE;IACzE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,KAAK,GAAG,CAAC,IAAe,EAAE,OAA2B,EAAQ,EAAE;QACnE,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;YACtB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC7B,IAAI,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChB,KAAK,CAAC,EAAE,CAAC,QAAqB,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBAChD,SAAS;YACX,CAAC;YACD,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1B,IAAI,EAAE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAChC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACjC,IAAI,KAAK,KAAK,SAAS;oBAAE,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;qBAChD,IAAI,KAAK,KAAK,OAAO;oBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,KAAK,EAAE,EAAE,CAAC,CAAC;YACnF,CAAC;YACD,KAAK,CAAC,EAAE,CAAC,QAAqB,EAAE,OAAO,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACxB,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,6GAA6G;AAC7G,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAA0D,EAAE,EAAU,EAAU,EAAE,CACzG,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAE3D,OAAO,EASL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAA6D,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGtI,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAClE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4CD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,gBAAgB,EACzB,MAAM,iBAAiB,EACvB,cAAc,MAAM,KACnB,MAOF,CAAC;AAysBF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,OAAO,EACf,KAAK,YAAY,KAChB;IAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,EAAE,CAAA;CA4BrD,CAAC;AAwCF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,OAAO,EAAE,KAAK,YAAY,KAAG,QAAQ,EAQ3E,CAAC;AAUF,0EAA0E;AAC1E,eAAO,MAAM,4BAA4B,mCAAmC,CAAC;AAE7E,qGAAqG;AACrG,eAAO,MAAM,4BAA4B,QAA4C,CAAC;AAiHtF,mFAAmF;AACnF,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,OAAmC,CAAC;AAKjF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,QAAQ,EAAE,CAcvE;AAsdD,wBAAgB,eAAe,CAC7B,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,YAAY;AACjB,6DAA6D;AAC7D,IAAI,GAAE,GAAG,CAAC,MAAM,GAAG,OAAO,CAAa,GACtC,QAAQ,EAAE,CA6BZ"}