@dogsbay/format-dogsbay-md 0.2.0-beta.137 → 0.2.0-beta.138
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/dist/data-directives.js +10 -0
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +28 -0
- package/dist/directive-props.d.ts +15 -0
- package/dist/directive-props.js +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parse-attrs.d.ts +6 -0
- package/dist/parse-attrs.js +17 -0
- package/dist/parse.js +8 -6
- package/dist/plugin-inline-directives.js +8 -3
- package/dist/serialize.js +2 -1
- package/package.json +5 -5
package/dist/data-directives.js
CHANGED
|
@@ -192,9 +192,19 @@ export function literalCode(value) {
|
|
|
192
192
|
}
|
|
193
193
|
const list = (v) => typeof v === "string" && v.trim() ? v.split(",").map((s) => s.trim()) : undefined;
|
|
194
194
|
function tableNode(rows, props, file) {
|
|
195
|
+
// A page never shows an empty table (docs/data-files.md): a file with no entries is reported.
|
|
196
|
+
if (rows.length === 0)
|
|
197
|
+
throw new DirectiveError(`${file} has no entries to show`);
|
|
195
198
|
const columns = list(props.columns) ?? [...new Set(rows.flatMap((r) => Object.keys(r)))];
|
|
196
199
|
if (columns.length === 0)
|
|
197
200
|
throw new DirectiveError(`${file} has no columns to show`);
|
|
201
|
+
// A column no row has is a typo, not an optional field: it showed as an empty column.
|
|
202
|
+
const known = [...new Set(rows.flatMap((r) => Object.keys(r)))];
|
|
203
|
+
const missing = columns.filter((c) => !known.includes(c));
|
|
204
|
+
if (missing.length > 0) {
|
|
205
|
+
const has = known.length === 0 ? "it has no fields" : `it has ${known.slice(0, 12).join(", ")}${known.length > 12 ? `, … ${known.length - 12} more` : ""}`;
|
|
206
|
+
throw new DirectiveError(`${file} has no ${missing.map((c) => `"${c}"`).join(", ")} column (${has})`);
|
|
207
|
+
}
|
|
198
208
|
const headers = list(props.headers) ?? columns;
|
|
199
209
|
if (headers.length !== columns.length) {
|
|
200
210
|
throw new DirectiveError(`headers names ${headers.length} column(s), columns ${columns.length}`);
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface ParseDiagnostic {
|
|
|
2
2
|
/** 1-based line in the source (front matter counted). */
|
|
3
3
|
line: number;
|
|
4
4
|
/** Stable kind, for filtering. */
|
|
5
|
-
code: "unclosed-container" | "stray-closer" | "unknown-container" | "unknown-inline" | "block-directive-inline" | "bad-attributes" | "closer-mismatch" | "dropped-attributes" | "missing-label" | "leaf-as-block" | "nesting-unchecked";
|
|
5
|
+
code: "unclosed-container" | "stray-closer" | "unknown-container" | "unknown-inline" | "block-directive-inline" | "bad-attributes" | "closer-mismatch" | "dropped-attributes" | "missing-label" | "leaf-as-block" | "nesting-unchecked" | "unknown-prop";
|
|
6
6
|
message: string;
|
|
7
7
|
/**
|
|
8
8
|
* The directive's name, without colons, on `unknown-container` and `unknown-inline`: a host that
|
package/dist/diagnostics.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { BLOCK_LEAF_DIRECTIVES, GENERIC_CONTAINER } from "./plugin-containers.js";
|
|
12
12
|
import { LABEL_PROP } from "./plugin-inline-directives.js";
|
|
13
|
+
import { unknownProps } from "./directive-props.js";
|
|
14
|
+
import * as yaml from "js-yaml";
|
|
13
15
|
/**
|
|
14
16
|
* Whether a source line closes a container opened with `colons` colons. The
|
|
15
17
|
* line may sit in a blockquote or a list item: its `>` markers and
|
|
@@ -119,6 +121,22 @@ const clip = (line) => {
|
|
|
119
121
|
const t = (line ?? "").trim();
|
|
120
122
|
return t.length > 60 ? `${t.slice(0, 59)}…` : t;
|
|
121
123
|
};
|
|
124
|
+
/** The keys of a known container's parsed attrs (`tok.meta.attrs`), the lifted YAML block's included. */
|
|
125
|
+
function propKeysOfAttrs(props) {
|
|
126
|
+
const keys = Object.keys(props).filter((k) => k !== "__yaml__");
|
|
127
|
+
const encoded = props.__yaml__;
|
|
128
|
+
if (typeof encoded === "string") {
|
|
129
|
+
try {
|
|
130
|
+
const loaded = yaml.load(decodeURIComponent(encoded));
|
|
131
|
+
if (loaded && typeof loaded === "object" && !Array.isArray(loaded))
|
|
132
|
+
keys.push(...Object.keys(loaded));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* not YAML: nothing to check */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return keys;
|
|
139
|
+
}
|
|
122
140
|
export function collectDiagnostics(tokens, lines, lineOf) {
|
|
123
141
|
const out = [];
|
|
124
142
|
const add = (line0, code, message, name) => {
|
|
@@ -167,6 +185,16 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
167
185
|
else if (generic) {
|
|
168
186
|
add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`, name);
|
|
169
187
|
}
|
|
188
|
+
else {
|
|
189
|
+
// A prop no renderer of this directive reads is carried, never drawn: say so (a typo
|
|
190
|
+
// in a `{…}` or `---` block was silent). Keys from both forms; the YAML block arrives
|
|
191
|
+
// lifted into `__yaml__` by preprocessYamlBodies.
|
|
192
|
+
// markdown-it-attrs put the opener's `{…}` on the token's attrs; the lifted YAML block is `__yaml__`
|
|
193
|
+
const pairs = (tok.attrs ?? []);
|
|
194
|
+
for (const key of unknownProps(name, propKeysOfAttrs(Object.fromEntries(pairs)))) {
|
|
195
|
+
add(tok.map[0], "unknown-prop", `${key} is not a prop of :::${name} — it is kept but not drawn`, name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
170
198
|
if (tok.meta?.unverified) {
|
|
171
199
|
add(tok.map[0], "nesting-unchecked", `the parser stopped re-checking the nested blocks on this page here, so where :::${name} ends may be wrong — check that each block inside it has its own closer, or split the page`);
|
|
172
200
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The props each known directive DRAWS, for the `unknown-prop` diagnostic (2026-10-27): a key
|
|
3
|
+
* a renderer does not read is carried through every round trip but drawn by nothing, and a
|
|
4
|
+
* typo in a `{…}` block or a `---` props block was silent. The lists are the union of what
|
|
5
|
+
* docs/dogsbay-md.md documents and what format-astro's and render-html's renderers read; a
|
|
6
|
+
* directive not listed here is not checked (api-*, autodoc, openapi, config, theme, example:
|
|
7
|
+
* their props are open-ended or read from a spec).
|
|
8
|
+
*
|
|
9
|
+
* Always allowed on anything: `id`, `class`, `style`, `role`, `data-*`, `aria-*` (passed
|
|
10
|
+
* through to the element), and `type`/`title`/`icon` on the callout names.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PASSTHROUGH_PROP: RegExp;
|
|
13
|
+
export declare const DIRECTIVE_PROPS: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
/** Keys of `props` that `name` does not draw; empty when the directive is not checked. */
|
|
15
|
+
export declare function unknownProps(name: string, props: Iterable<string>): string[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The props each known directive DRAWS, for the `unknown-prop` diagnostic (2026-10-27): a key
|
|
3
|
+
* a renderer does not read is carried through every round trip but drawn by nothing, and a
|
|
4
|
+
* typo in a `{…}` block or a `---` props block was silent. The lists are the union of what
|
|
5
|
+
* docs/dogsbay-md.md documents and what format-astro's and render-html's renderers read; a
|
|
6
|
+
* directive not listed here is not checked (api-*, autodoc, openapi, config, theme, example:
|
|
7
|
+
* their props are open-ended or read from a spec).
|
|
8
|
+
*
|
|
9
|
+
* Always allowed on anything: `id`, `class`, `style`, `role`, `data-*`, `aria-*` (passed
|
|
10
|
+
* through to the element), and `type`/`title`/`icon` on the callout names.
|
|
11
|
+
*/
|
|
12
|
+
export const PASSTHROUGH_PROP = /^(?:id|class|style|role|data-[\w-]+|aria-[\w-]+)$/;
|
|
13
|
+
const CALLOUT = ["type", "title", "icon", "collapsed", "collapsible", "open", "tone", "variant", "href"];
|
|
14
|
+
const CALLOUT_NAMES = ["callout", "note", "tip", "info", "important", "warning", "caution", "danger", "success", "hint", "attention", "error", "failure", "bug", "question", "abstract", "quote"];
|
|
15
|
+
export const DIRECTIVE_PROPS = {
|
|
16
|
+
...Object.fromEntries(CALLOUT_NAMES.map((n) => [n, CALLOUT])),
|
|
17
|
+
details: ["title", "summary", "icon", "open", "variant", "type", "sheet"],
|
|
18
|
+
tabs: ["variant", "sync", "syncKey", "group", "title", "value", "label"],
|
|
19
|
+
tab: ["title", "label", "icon", "value"],
|
|
20
|
+
steps: ["title"],
|
|
21
|
+
step: ["title"],
|
|
22
|
+
cards: ["cols", "tone", "variant"],
|
|
23
|
+
card: ["title", "href", "icon", "description", "tone", "variant", "image", "alt"],
|
|
24
|
+
"link-card": ["href", "title", "description", "icon"],
|
|
25
|
+
grid: ["cols", "gap"],
|
|
26
|
+
"grid-item": ["span", "rowSpan", "label", "tone"],
|
|
27
|
+
figure: ["caption", "src", "alt"],
|
|
28
|
+
files: [],
|
|
29
|
+
button: ["href", "variant", "label", "size"],
|
|
30
|
+
youtube: ["id", "title", "poster"],
|
|
31
|
+
avatar: ["src", "alt", "fallback", "source", "size"],
|
|
32
|
+
accordion: ["type", "defaultValue", "variant"],
|
|
33
|
+
"accordion-item": ["title", "label", "value", "open"],
|
|
34
|
+
math: [],
|
|
35
|
+
"data-table": ["src", "columns", "caption", "sortable", "filter", "paginate"],
|
|
36
|
+
"data-cards": ["src", "cols", "tone"],
|
|
37
|
+
"child-links": ["depth", "title"],
|
|
38
|
+
};
|
|
39
|
+
/** Keys of `props` that `name` does not draw; empty when the directive is not checked. */
|
|
40
|
+
export function unknownProps(name, props) {
|
|
41
|
+
const known = DIRECTIVE_PROPS[name];
|
|
42
|
+
if (!known)
|
|
43
|
+
return [];
|
|
44
|
+
const set = new Set(known);
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const key of props)
|
|
47
|
+
if (!set.has(key) && !PASSTHROUGH_PROP.test(key) && key !== "__yaml__")
|
|
48
|
+
out.push(key);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcess
|
|
|
9
9
|
export type { ParseResult, ParseOptions } from "./parse.js";
|
|
10
10
|
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
11
11
|
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
12
|
+
export { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
12
13
|
export { buildNavFromDirectory } from "./nav.js";
|
|
13
14
|
export type { BuildNavOptions } from "./nav.js";
|
|
14
15
|
export { loadNavFile } from "./nav-file.js";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcess
|
|
|
12
12
|
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
13
13
|
// The `:::name` blocks the parser knows (an editor completes names from it)
|
|
14
14
|
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
15
|
+
export { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
15
16
|
export { buildNavFromDirectory } from "./nav.js";
|
|
16
17
|
export { loadNavFile } from "./nav-file.js";
|
|
17
18
|
export { renderDiagrams } from "./renderers/diagrams.js";
|
package/dist/parse-attrs.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface ParsedAttrs {
|
|
|
16
16
|
props: Record<string, string | boolean>;
|
|
17
17
|
}
|
|
18
18
|
export declare function parseAttrs(inner: string): ParsedAttrs;
|
|
19
|
+
/**
|
|
20
|
+
* Whether `inner` is ONLY a list of attributes, as parseAttrs reads them. parseAttrs never fails,
|
|
21
|
+
* so text that only looks like a block (CSS in prose, `{--x: 1}`) comes out reordered; a reader
|
|
22
|
+
* that may meet such text (the label-less inline form) asks this first and leaves it as text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isAttrList(inner: string): boolean;
|
|
19
25
|
/**
|
|
20
26
|
* Merge ParsedAttrs into a props dict suitable for TreeNode.props.
|
|
21
27
|
* `id` and `classes` become `id` and `class`; other props flow through.
|
package/dist/parse-attrs.js
CHANGED
|
@@ -99,6 +99,23 @@ export function parseAttrs(inner) {
|
|
|
99
99
|
}
|
|
100
100
|
return result;
|
|
101
101
|
}
|
|
102
|
+
/** One item of an attribute list: `#id`, `.class`, `key`, `key=bare`, `key="…"`, `key='…'`. */
|
|
103
|
+
const ATTR_ITEM = /\s*(?:[#.][\w-]+|[A-Za-z_][\w-]*(?:=(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"'=]+))?)(?=\s|$)/y;
|
|
104
|
+
/**
|
|
105
|
+
* Whether `inner` is ONLY a list of attributes, as parseAttrs reads them. parseAttrs never fails,
|
|
106
|
+
* so text that only looks like a block (CSS in prose, `{--x: 1}`) comes out reordered; a reader
|
|
107
|
+
* that may meet such text (the label-less inline form) asks this first and leaves it as text.
|
|
108
|
+
*/
|
|
109
|
+
export function isAttrList(inner) {
|
|
110
|
+
let pos = 0;
|
|
111
|
+
ATTR_ITEM.lastIndex = 0;
|
|
112
|
+
while (ATTR_ITEM.lastIndex < inner.length) {
|
|
113
|
+
if (!ATTR_ITEM.test(inner))
|
|
114
|
+
return /^\s*$/.test(inner.slice(pos));
|
|
115
|
+
pos = ATTR_ITEM.lastIndex;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
102
119
|
/**
|
|
103
120
|
* Merge ParsedAttrs into a props dict suitable for TreeNode.props.
|
|
104
121
|
* `id` and `classes` become `id` and `class`; other props flow through.
|
package/dist/parse.js
CHANGED
|
@@ -1795,12 +1795,14 @@ function tokensToInline(tokens, inheritedFmt = []) {
|
|
|
1795
1795
|
}
|
|
1796
1796
|
const labelInline = tokensToInline(labelTokens);
|
|
1797
1797
|
const labelText = inlineText(labelInline);
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
//
|
|
1802
|
-
|
|
1803
|
-
|
|
1798
|
+
const meta = tok.meta;
|
|
1799
|
+
const labelless = meta?.labelless === true;
|
|
1800
|
+
let node = meta?.literal && meta.source ? { type: "text", text: meta.source } : inlineDirectiveToNode(name, labelText, labelInline, attrs, labelless);
|
|
1801
|
+
// A label-less directive that makes nothing (unknown, or a known one without its naming
|
|
1802
|
+
// prop) stays exactly as written, as TEXT: its attrs may carry a tag, html-inline is written
|
|
1803
|
+
// raw by every consumer, and re-rendering the attrs reordered them
|
|
1804
|
+
if (labelless && meta?.source && ((node?.type === "html-inline" && node.html.startsWith(`:${name}[]`)) || (node?.type === "text" && node.text.startsWith(`:${name}{`))))
|
|
1805
|
+
node = { type: "text", text: meta.source };
|
|
1804
1806
|
if (node)
|
|
1805
1807
|
result.push(node);
|
|
1806
1808
|
break;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attrBlockEnd, parseAttrs } from "./parse-attrs.js";
|
|
1
|
+
import { attrBlockEnd, isAttrList, parseAttrs } from "./parse-attrs.js";
|
|
2
2
|
/** A character a directive cannot follow directly (see inlineDirectiveRule). */
|
|
3
3
|
export const PRECEDES_TEXT = /[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}0-9:]/u;
|
|
4
4
|
/** The prop that labels a known inline directive written without `[text]` (`:kbd{value="Ctrl"}`). */
|
|
@@ -115,7 +115,11 @@ function inlineDirectiveRule(state, silent) {
|
|
|
115
115
|
state.pos = pos;
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// `:root{--x: 1}` in prose is CSS, not attributes: read leniently it came out reordered
|
|
119
|
+
// (`:root{1 --x:}`). It is still taken here, as literal text: left to the paragraph, a trailing
|
|
120
|
+
// `{…}` would be read as the paragraph's attributes and vanish.
|
|
121
|
+
const literal = labelless && !isAttrList(attrs);
|
|
122
|
+
const parsedAttrs = literal ? { classes: [], props: {} } : parseAttrs(attrs);
|
|
119
123
|
const attrsStr = JSON.stringify(parsedAttrs);
|
|
120
124
|
if (isLeaf) {
|
|
121
125
|
const t = state.push("dogsbay_leaf_directive", "", 0);
|
|
@@ -126,8 +130,9 @@ function inlineDirectiveRule(state, silent) {
|
|
|
126
130
|
const openT = state.push("dogsbay_inline_directive_open", "", 1);
|
|
127
131
|
openT.tag = name;
|
|
128
132
|
openT.info = attrsStr;
|
|
133
|
+
// the source as written: an unknown label-less directive stays these bytes, not a re-rendering
|
|
129
134
|
if (labelless)
|
|
130
|
-
openT.meta = { labelless: true };
|
|
135
|
+
openT.meta = { labelless: true, literal, source: src.slice(start, pos) };
|
|
131
136
|
if (!labelless) {
|
|
132
137
|
// Parse label as inline content (recursive)
|
|
133
138
|
const oldPos = state.pos;
|
package/dist/serialize.js
CHANGED
|
@@ -618,7 +618,8 @@ function renderCallout(node, ctx) {
|
|
|
618
618
|
const labelled = typeof node.meta?.closer === "string";
|
|
619
619
|
if (ctx.githubCallouts && hasOnlyTypeProp && !labelled && GITHUB_CALLOUT_TYPES.has(type)) {
|
|
620
620
|
const tag = `[!${type.toUpperCase()}]`;
|
|
621
|
-
|
|
621
|
+
// a body opening with `---` under `[!NOTE]` made a setext heading of the tag: the callout was lost
|
|
622
|
+
return [tag, guardRule(body)]
|
|
622
623
|
.join("\n")
|
|
623
624
|
.split("\n")
|
|
624
625
|
.map((l) => (l ? `> ${l}` : ">"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-dogsbay-md",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.138",
|
|
4
4
|
"description": "Dogsbay Markdown dialect — canonical format for TreeNode[] with directives, YAML bodies, and intelligent fallbacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"markdown-it-footnote": "^4.0.0",
|
|
41
41
|
"markdown-it-front-matter": "^0.2.4",
|
|
42
42
|
"pele": "1.6.0",
|
|
43
|
-
"@dogsbay/corpus": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
45
|
-
"@dogsbay/
|
|
46
|
-
"@dogsbay/
|
|
43
|
+
"@dogsbay/corpus": "0.2.0-beta.138",
|
|
44
|
+
"@dogsbay/minja": "0.2.0-beta.138",
|
|
45
|
+
"@dogsbay/serialize-core": "0.2.0-beta.138",
|
|
46
|
+
"@dogsbay/types": "0.2.0-beta.138"
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"repository": {
|