@miragon/design-iq-validator 5.3.0 → 5.5.0
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
CHANGED
|
@@ -5,7 +5,9 @@ structural checks (flow soundness, complete BPMNDI coverage), DMN structure and
|
|
|
5
5
|
the generic cross-model reference rule (every required reference — `callActivity` calls,
|
|
6
6
|
`businessRuleTask` decides — must resolve in the repo), and a per-mediaKind parse baseline for
|
|
7
7
|
every other registered notation (a broken `.tt`/`.vc.json` is an ERROR; DSL notations are
|
|
8
|
-
lenient by design
|
|
8
|
+
lenient by design; a `mermaid` fence in a Markdown document that does not start with a diagram
|
|
9
|
+
type such as `flowchart` or `sequenceDiagram` is a WARN, `mermaid/diagram-type`, naming the
|
|
10
|
+
document's line). Every finding carries a stable `ruleId` (e.g. `bpmn/flow`, `dmn/di`,
|
|
9
11
|
`refs/dangling`). It treats the target repo as pure data — it never executes content-repo code.
|
|
10
12
|
Exit code 0 = no errors (warnings allowed), 1 = errors.
|
|
11
13
|
|
package/dist/cli.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-Bsv9ZLFd.js";
|
|
@@ -84,6 +84,457 @@ const notContentRepoError = (root) => {
|
|
|
84
84
|
return found === void 0 ? `[ERROR] ${root}: no ${CONTENT_CONFIG_NAMES} at the root — not a content repo (or wrong --root)` : `[ERROR] ${root}: ${found} at the root names no models folder — not a content repo (or wrong --root)`;
|
|
85
85
|
};
|
|
86
86
|
//#endregion
|
|
87
|
+
//#region ../notations/markdown.ts
|
|
88
|
+
const NONE = { kind: "none" };
|
|
89
|
+
const PARAGRAPH = { kind: "paragraph" };
|
|
90
|
+
const CODE = { kind: "code" };
|
|
91
|
+
const FENCE_OPEN = /^(`{3,}|~{3,})(.*)$/;
|
|
92
|
+
const THEMATIC_BREAK = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
93
|
+
const ATX_HEADING = /^#{1,6}(?:[ \t]|$)/;
|
|
94
|
+
const SETEXT_UNDERLINE = /^(?:=+|-+)[ \t]*$/;
|
|
95
|
+
const BULLET = /^[-+*](?=[ \t]|$)/;
|
|
96
|
+
const ORDERED = /^(\d{1,9})[.)](?=[ \t]|$)/;
|
|
97
|
+
const BLANK = /^[ \t]*$/;
|
|
98
|
+
/** a link reference definition on one line: [label]: destination "title" (a
|
|
99
|
+
* "[^" label is a footnote, which render.ts keeps as paragraph text; a bare
|
|
100
|
+
* destination ends at a space or a control character, as in markdown-it) */
|
|
101
|
+
const LINK_DEFINITION = /^\[(?!\^)((?:[^[\]\\]|\\.)*)\]:[ \t]*(<(?:[^<>\\]|\\.)*>|[^ <\x00-\x1f\x7f][^ \x00-\x1f\x7f]*)(?:[ \t]+(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\((?:[^()\\]|\\.)*\)))?[ \t]*$/;
|
|
102
|
+
/** the destinations markdown-it's validateLink refuses: no definition then */
|
|
103
|
+
const BAD_LINK = /^(?:vbscript|javascript|file|data):/i;
|
|
104
|
+
const GOOD_DATA_LINK = /^data:image\/(?:gif|png|jpeg|webp);/i;
|
|
105
|
+
/** markdown-it's maxNesting: content nested this deep (a Container's `level`)
|
|
106
|
+
* it does not parse, and drops the rest of the innermost quote around it,
|
|
107
|
+
* or of the document */
|
|
108
|
+
const MAX_NESTING = 100;
|
|
109
|
+
/** start, end and whether it may interrupt a paragraph; an end of /^$/ is the next blank line */
|
|
110
|
+
const HTML_BLOCKS = [
|
|
111
|
+
[
|
|
112
|
+
/^<(?:script|pre|style|textarea)(?=\s|>|$)/i,
|
|
113
|
+
/<\/(?:script|pre|style|textarea)>/i,
|
|
114
|
+
true
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
/^<!--/,
|
|
118
|
+
/-->/,
|
|
119
|
+
true
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
/^<\?/,
|
|
123
|
+
/\?>/,
|
|
124
|
+
true
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
/^<![A-Za-z]/,
|
|
128
|
+
/>/,
|
|
129
|
+
true
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
/^<!\[CDATA\[/,
|
|
133
|
+
/\]\]>/,
|
|
134
|
+
true
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
new RegExp(`^</?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?=\\s|/?>|$)`, "i"),
|
|
138
|
+
/^$/,
|
|
139
|
+
true
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
new RegExp(`^(?:<[A-Za-z][A-Za-z0-9\\-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^"'=<>\`\\x00-\\x20]+|'[^']*'|"[^"]*"))?)*\\s*\\/?>|<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>)\\s*$`),
|
|
143
|
+
/^$/,
|
|
144
|
+
false
|
|
145
|
+
]
|
|
146
|
+
];
|
|
147
|
+
/** the leading blanks of a rest */
|
|
148
|
+
function leading(rest) {
|
|
149
|
+
let col = rest.col;
|
|
150
|
+
let i = 0;
|
|
151
|
+
for (; i < rest.text.length; i++) {
|
|
152
|
+
const ch = rest.text[i];
|
|
153
|
+
if (ch === " ") col++;
|
|
154
|
+
else if (ch === " ") col += 4 - col % 4;
|
|
155
|
+
else break;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
indent: col - rest.col,
|
|
159
|
+
blank: i === rest.text.length
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** the columns of leading blanks */
|
|
163
|
+
const indentOf = (rest) => leading(rest).indent;
|
|
164
|
+
/** drop up to `n` columns of leading blanks; a tab cut in two leaves its other columns as spaces */
|
|
165
|
+
function dropIndent(rest, n) {
|
|
166
|
+
const target = rest.col + n;
|
|
167
|
+
let col = rest.col;
|
|
168
|
+
let i = 0;
|
|
169
|
+
while (i < rest.text.length && col < target) {
|
|
170
|
+
const ch = rest.text[i];
|
|
171
|
+
if (ch === " ") col++;
|
|
172
|
+
else if (ch === " ") {
|
|
173
|
+
const next = col + 4 - col % 4;
|
|
174
|
+
if (next > target) return {
|
|
175
|
+
text: " ".repeat(next - target) + rest.text.slice(i + 1),
|
|
176
|
+
col: target
|
|
177
|
+
};
|
|
178
|
+
col = next;
|
|
179
|
+
} else break;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
text: rest.text.slice(i),
|
|
184
|
+
col
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** the line after its indent, when that is at most three columns (more is indented code) */
|
|
188
|
+
function blockStart(rest) {
|
|
189
|
+
const indent = indentOf(rest);
|
|
190
|
+
return indent < 4 ? dropIndent(rest, indent) : void 0;
|
|
191
|
+
}
|
|
192
|
+
/** a quote's ">" and the one column of blank after it: a space, or the first
|
|
193
|
+
* column of a tab (which stays, a column narrower, as in markdown-it) */
|
|
194
|
+
function afterQuoteMarker(at) {
|
|
195
|
+
const rest = {
|
|
196
|
+
text: at.text.slice(1),
|
|
197
|
+
col: at.col + 1
|
|
198
|
+
};
|
|
199
|
+
if (rest.text.startsWith(" ") || rest.text.startsWith(" ") && rest.col % 4 === 3) return {
|
|
200
|
+
text: rest.text.slice(1),
|
|
201
|
+
col: rest.col + 1
|
|
202
|
+
};
|
|
203
|
+
return rest.text.startsWith(" ") ? {
|
|
204
|
+
text: rest.text,
|
|
205
|
+
col: rest.col + 1
|
|
206
|
+
} : rest;
|
|
207
|
+
}
|
|
208
|
+
/** the rest of the line when it continues the container, undefined when the
|
|
209
|
+
* container ends. `lead` is the rest's leading blanks, kept up to date across
|
|
210
|
+
* the columns a list item drops, so a line is not measured once per container */
|
|
211
|
+
function continues(container, rest, lead) {
|
|
212
|
+
if (container.kind === "quote") {
|
|
213
|
+
const at = dropIndent(rest, lead.indent);
|
|
214
|
+
return at.text.startsWith(">") ? afterQuoteMarker(at) : void 0;
|
|
215
|
+
}
|
|
216
|
+
const fresh = container.fresh;
|
|
217
|
+
container.fresh = false;
|
|
218
|
+
if (lead.blank ? fresh : lead.indent < container.indent) return void 0;
|
|
219
|
+
lead.indent = Math.max(0, lead.indent - container.indent);
|
|
220
|
+
return dropIndent(rest, container.indent);
|
|
221
|
+
}
|
|
222
|
+
/** a list item opening at `at` (the line after its indent of `indent` columns),
|
|
223
|
+
* inside containers nested `level` deep */
|
|
224
|
+
function listItem(at, indent, level, interruptsParagraph) {
|
|
225
|
+
const ordered = ORDERED.exec(at.text);
|
|
226
|
+
const marker = ordered ?? BULLET.exec(at.text);
|
|
227
|
+
if (!marker) return void 0;
|
|
228
|
+
const after = {
|
|
229
|
+
text: at.text.slice(marker[0].length),
|
|
230
|
+
col: at.col + marker[0].length
|
|
231
|
+
};
|
|
232
|
+
const empty = BLANK.test(after.text);
|
|
233
|
+
if (interruptsParagraph && (empty || ordered && Number(ordered[1]) !== 1)) return void 0;
|
|
234
|
+
const blanks = indentOf(after);
|
|
235
|
+
const gap = empty || blanks > 4 ? 1 : blanks;
|
|
236
|
+
return {
|
|
237
|
+
item: {
|
|
238
|
+
kind: "item",
|
|
239
|
+
level: level + 2,
|
|
240
|
+
indent: indent + marker[0].length + gap,
|
|
241
|
+
fresh: empty
|
|
242
|
+
},
|
|
243
|
+
rest: dropIndent(after, gap)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** the HTML block starting at `text` (a line after its indent), if any */
|
|
247
|
+
function htmlBlock(text) {
|
|
248
|
+
return text.startsWith("<") ? HTML_BLOCKS.find(([start]) => start.test(text)) : void 0;
|
|
249
|
+
}
|
|
250
|
+
/** whether a line that does not continue a paragraph's containers starts a
|
|
251
|
+
* block of its own (else it is a lazy continuation line of the paragraph) */
|
|
252
|
+
function startsBlock(rest) {
|
|
253
|
+
const at = blockStart(rest);
|
|
254
|
+
if (!at) return false;
|
|
255
|
+
const text = at.text;
|
|
256
|
+
return text.startsWith(">") || fenceOpen(text) !== void 0 || THEMATIC_BREAK.test(text) || BULLET.test(text) || ORDERED.test(text) || ATX_HEADING.test(text) || htmlBlock(text)?.[2] === true;
|
|
257
|
+
}
|
|
258
|
+
/** whether `text` (a line after its indent) is a link reference definition that
|
|
259
|
+
* fits on it and that markdown-it takes: a label with more than blanks, a
|
|
260
|
+
* destination with balanced parentheses and a scheme its validateLink allows */
|
|
261
|
+
function linkDefinition(text) {
|
|
262
|
+
const definition = LINK_DEFINITION.exec(text);
|
|
263
|
+
if (!definition || definition[1].trim() === "") return false;
|
|
264
|
+
let destination = definition[2];
|
|
265
|
+
if (destination.startsWith("<")) destination = destination.slice(1, -1);
|
|
266
|
+
else {
|
|
267
|
+
let depth = 0;
|
|
268
|
+
for (let i = 0; i < destination.length && depth >= 0; i++) {
|
|
269
|
+
const ch = destination[i];
|
|
270
|
+
if (ch === "\\") i++;
|
|
271
|
+
else if (ch === "(") depth = depth < 32 ? depth + 1 : -1;
|
|
272
|
+
else if (ch === ")") depth--;
|
|
273
|
+
}
|
|
274
|
+
if (depth !== 0) return false;
|
|
275
|
+
}
|
|
276
|
+
destination = destination.trim();
|
|
277
|
+
return !BAD_LINK.test(destination) || GOOD_DATA_LINK.test(destination);
|
|
278
|
+
}
|
|
279
|
+
/** the opening run of a fence at `text` (a line after its indent), if any */
|
|
280
|
+
function fenceOpen(text) {
|
|
281
|
+
const open = FENCE_OPEN.exec(text);
|
|
282
|
+
if (!open) return void 0;
|
|
283
|
+
const run = open[1];
|
|
284
|
+
const info = open[2];
|
|
285
|
+
if (run.startsWith("`") && info.includes("`")) return void 0;
|
|
286
|
+
return {
|
|
287
|
+
char: run[0],
|
|
288
|
+
size: run.length,
|
|
289
|
+
info: info.trim()
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/** whether the line is the closing run of `fence` */
|
|
293
|
+
function closes(rest, fence) {
|
|
294
|
+
const at = blockStart(rest);
|
|
295
|
+
if (!at) return false;
|
|
296
|
+
let n = 0;
|
|
297
|
+
while (at.text[n] === fence.char) n++;
|
|
298
|
+
return n >= fence.size && BLANK.test(at.text.slice(n));
|
|
299
|
+
}
|
|
300
|
+
/** every fenced code block of a Markdown document, in document order */
|
|
301
|
+
function markdownFences(source) {
|
|
302
|
+
const lines = source.split(/\r\n|\r|\n/);
|
|
303
|
+
const brokenLast = BLANK.test(lines[lines.length - 1]);
|
|
304
|
+
if (brokenLast) lines.pop();
|
|
305
|
+
const fences = [];
|
|
306
|
+
const stack = [];
|
|
307
|
+
const level = () => stack.length > 0 ? stack[stack.length - 1].level : 0;
|
|
308
|
+
let leaf = NONE;
|
|
309
|
+
let dropping;
|
|
310
|
+
const finish = (fence, closed) => {
|
|
311
|
+
fences.push({
|
|
312
|
+
line: fence.line,
|
|
313
|
+
endLine: fence.line + fence.content.length + (closed ? 1 : 0),
|
|
314
|
+
info: fence.info,
|
|
315
|
+
lang: (fence.info.split(/\s+/)[0] ?? "").toLowerCase(),
|
|
316
|
+
content: fence.content.join(""),
|
|
317
|
+
closed
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
for (let i = 0; i < lines.length; i++) {
|
|
321
|
+
let rest = {
|
|
322
|
+
text: lines[i],
|
|
323
|
+
col: 0
|
|
324
|
+
};
|
|
325
|
+
const lineBreak = i < lines.length - 1 || brokenLast ? "\n" : "";
|
|
326
|
+
let matched = 0;
|
|
327
|
+
let lead;
|
|
328
|
+
for (const container of stack) {
|
|
329
|
+
const next = continues(container, rest, lead ??= leading(rest));
|
|
330
|
+
if (!next) break;
|
|
331
|
+
rest = next;
|
|
332
|
+
if (container.kind === "quote") lead = void 0;
|
|
333
|
+
matched++;
|
|
334
|
+
}
|
|
335
|
+
const all = matched === stack.length;
|
|
336
|
+
if (dropping) {
|
|
337
|
+
if (all) {
|
|
338
|
+
dropping.lastBlank = BLANK.test(rest.text);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!dropping.lastBlank && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
|
|
342
|
+
dropping = void 0;
|
|
343
|
+
}
|
|
344
|
+
if (leaf.kind === "fence" && all) {
|
|
345
|
+
if (closes(rest, leaf)) {
|
|
346
|
+
finish(leaf, true);
|
|
347
|
+
leaf = NONE;
|
|
348
|
+
} else leaf.content.push(dropIndent(rest, leaf.indent).text + lineBreak);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (leaf.kind === "html" && all) {
|
|
352
|
+
if (leaf.end.test(dropIndent(rest, indentOf(rest)).text)) leaf = NONE;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!all) {
|
|
356
|
+
if (leaf.kind === "paragraph" && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
|
|
357
|
+
if (leaf.kind === "fence") finish(leaf, false);
|
|
358
|
+
stack.length = matched;
|
|
359
|
+
leaf = NONE;
|
|
360
|
+
}
|
|
361
|
+
while (level() < MAX_NESTING && !BLANK.test(rest.text)) {
|
|
362
|
+
const at = blockStart(rest);
|
|
363
|
+
if (!at) break;
|
|
364
|
+
if (at.text.startsWith(">")) {
|
|
365
|
+
stack.push({
|
|
366
|
+
kind: "quote",
|
|
367
|
+
level: level() + 1
|
|
368
|
+
});
|
|
369
|
+
rest = afterQuoteMarker(at);
|
|
370
|
+
} else {
|
|
371
|
+
if (THEMATIC_BREAK.test(at.text)) break;
|
|
372
|
+
const opening = listItem(at, indentOf(rest), level(), leaf.kind === "paragraph");
|
|
373
|
+
if (!opening) break;
|
|
374
|
+
stack.push(opening.item);
|
|
375
|
+
rest = opening.rest;
|
|
376
|
+
}
|
|
377
|
+
leaf = NONE;
|
|
378
|
+
}
|
|
379
|
+
if (BLANK.test(rest.text)) {
|
|
380
|
+
if (leaf.kind === "paragraph") leaf = NONE;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (level() >= MAX_NESTING) {
|
|
384
|
+
const quote = stack.findLastIndex((container) => container.kind === "quote");
|
|
385
|
+
if (quote < 0) break;
|
|
386
|
+
stack.length = quote + 1;
|
|
387
|
+
leaf = NONE;
|
|
388
|
+
dropping = { lastBlank: false };
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const at = blockStart(rest);
|
|
392
|
+
if (!at) {
|
|
393
|
+
if (leaf.kind !== "paragraph") leaf = CODE;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const open = fenceOpen(at.text);
|
|
397
|
+
if (open) {
|
|
398
|
+
leaf = {
|
|
399
|
+
kind: "fence",
|
|
400
|
+
...open,
|
|
401
|
+
indent: indentOf(rest),
|
|
402
|
+
line: i + 1,
|
|
403
|
+
content: []
|
|
404
|
+
};
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const html = htmlBlock(at.text);
|
|
408
|
+
if (html && (html[2] || leaf.kind !== "paragraph")) {
|
|
409
|
+
leaf = html[1].test(at.text) ? NONE : {
|
|
410
|
+
kind: "html",
|
|
411
|
+
end: html[1]
|
|
412
|
+
};
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (leaf.kind !== "paragraph" && linkDefinition(at.text)) leaf = NONE;
|
|
416
|
+
else if (ATX_HEADING.test(at.text) || THEMATIC_BREAK.test(at.text)) leaf = NONE;
|
|
417
|
+
else if (leaf.kind === "paragraph" && SETEXT_UNDERLINE.test(at.text)) leaf = NONE;
|
|
418
|
+
else leaf = PARAGRAPH;
|
|
419
|
+
}
|
|
420
|
+
if (leaf.kind === "fence") finish(leaf, false);
|
|
421
|
+
return fences;
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region ../notations/mermaid.ts
|
|
425
|
+
/**
|
|
426
|
+
* Every diagram type MERMAID_VERSION renders, spelled as its header keyword
|
|
427
|
+
* (its diagram detectors, minus the spellings the detector matches but the
|
|
428
|
+
* grammar then refuses: a bare `architecture` or `requirement`). zenuml is
|
|
429
|
+
* an external plugin mermaid does not bundle, so it is not here.
|
|
430
|
+
*/
|
|
431
|
+
const MERMAID_DIAGRAM_TYPES = [
|
|
432
|
+
"flowchart",
|
|
433
|
+
"flowchart-elk",
|
|
434
|
+
"graph",
|
|
435
|
+
"sequenceDiagram",
|
|
436
|
+
"classDiagram",
|
|
437
|
+
"classDiagram-v2",
|
|
438
|
+
"stateDiagram",
|
|
439
|
+
"stateDiagram-v2",
|
|
440
|
+
"erDiagram",
|
|
441
|
+
"journey",
|
|
442
|
+
"gantt",
|
|
443
|
+
"pie",
|
|
444
|
+
"quadrantChart",
|
|
445
|
+
"requirementDiagram",
|
|
446
|
+
"gitGraph",
|
|
447
|
+
"C4Context",
|
|
448
|
+
"C4Container",
|
|
449
|
+
"C4Component",
|
|
450
|
+
"C4Dynamic",
|
|
451
|
+
"C4Deployment",
|
|
452
|
+
"mindmap",
|
|
453
|
+
"timeline",
|
|
454
|
+
"sankey",
|
|
455
|
+
"sankey-beta",
|
|
456
|
+
"xychart",
|
|
457
|
+
"xychart-beta",
|
|
458
|
+
"block",
|
|
459
|
+
"block-beta",
|
|
460
|
+
"packet",
|
|
461
|
+
"packet-beta",
|
|
462
|
+
"kanban",
|
|
463
|
+
"architecture-beta",
|
|
464
|
+
"radar-beta",
|
|
465
|
+
"treemap",
|
|
466
|
+
"treemap-beta",
|
|
467
|
+
"info",
|
|
468
|
+
"swimlane-beta",
|
|
469
|
+
"treeView-beta",
|
|
470
|
+
"eventmodeling",
|
|
471
|
+
"ishikawa",
|
|
472
|
+
"ishikawa-beta",
|
|
473
|
+
"venn-beta",
|
|
474
|
+
"wardley-beta",
|
|
475
|
+
"cynefin-beta",
|
|
476
|
+
"railroad-beta",
|
|
477
|
+
"railroad-ebnf-beta",
|
|
478
|
+
"railroad-abnf-beta",
|
|
479
|
+
"railroad-peg-beta"
|
|
480
|
+
];
|
|
481
|
+
const FRONT_MATTER = /^([^\S\n\r]*)-{3}\s*[\n\r](.*?)[\n\r]\1-{3}\s*[\n\r]+/s;
|
|
482
|
+
const DIRECTIVE = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
|
483
|
+
const COMMENT_LINE = /^\s*%%(?!{)[^\n]+\n?/gm;
|
|
484
|
+
/** a header keyword, followed by the end of the line, a space, ":" or ";" */
|
|
485
|
+
const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map((t) => t.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")).join("|")})(?=$|[\\s:;])`);
|
|
486
|
+
/** mermaid's cleanupText: line breaks to \n, tag attributes single-quoted;
|
|
487
|
+
* the line structure stays, so the source lines can be matched against it */
|
|
488
|
+
function cleanup(source) {
|
|
489
|
+
return source.replace(/\r\n?/g, "\n").replace(/<(\w+)([^>]*)>/g, (_tag, name, attrs) => `<${name}${attrs.replace(/="([^"]*)"/g, "='$1'")}>`);
|
|
490
|
+
}
|
|
491
|
+
/** The text mermaid's diagram parser reads: the source after mermaid's
|
|
492
|
+
* preprocess (front matter, directives and comment lines removed, leading
|
|
493
|
+
* whitespace trimmed). */
|
|
494
|
+
function mermaidCode(source) {
|
|
495
|
+
return cleanup(source).replace(FRONT_MATTER, "").replace(DIRECTIVE, "").replace(COMMENT_LINE, "").trimStart();
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The 1-based source line of the 1-based line `codeLine` of mermaidCode(source).
|
|
499
|
+
* The preprocess only removes lines (and joins the two ends of a directive
|
|
500
|
+
* that spans lines), so the code is the source's lines in order: each code
|
|
501
|
+
* line is matched to the next source line with the same text. A line the
|
|
502
|
+
* preprocess rewrote matches nothing and takes the next line; a line past the
|
|
503
|
+
* end clamps to the last one.
|
|
504
|
+
*/
|
|
505
|
+
function mermaidSourceLine(source, codeLine) {
|
|
506
|
+
const cleaned = cleanup(source);
|
|
507
|
+
const sourceLines = cleaned.split("\n").map((l) => l.trim());
|
|
508
|
+
const codeLines = mermaidCode(source).split("\n");
|
|
509
|
+
let next = (cleaned.match(FRONT_MATTER)?.[0] ?? "").split("\n").length - 1;
|
|
510
|
+
let at = next;
|
|
511
|
+
for (let i = 0; i < Math.min(codeLine, codeLines.length); i++) {
|
|
512
|
+
const text = codeLines[i].trim();
|
|
513
|
+
let k = next;
|
|
514
|
+
while (k < sourceLines.length && sourceLines[k] !== text) k++;
|
|
515
|
+
at = k < sourceLines.length ? k : next;
|
|
516
|
+
next = at + 1;
|
|
517
|
+
}
|
|
518
|
+
return Math.min(at, sourceLines.length - 1) + 1;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The line that names the diagram type: the first line of mermaidCode that
|
|
522
|
+
* is neither blank nor a bare `%%` comment (mermaid's type detection skips
|
|
523
|
+
* those too). undefined when there is none: the source is empty, or nothing
|
|
524
|
+
* but front matter, directives and comments.
|
|
525
|
+
*/
|
|
526
|
+
function mermaidHeader(source) {
|
|
527
|
+
const lines = mermaidCode(source).split("\n");
|
|
528
|
+
const index = lines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("%%"));
|
|
529
|
+
if (index < 0) return void 0;
|
|
530
|
+
const text = lines[index].trim();
|
|
531
|
+
return {
|
|
532
|
+
line: mermaidSourceLine(source, index + 1),
|
|
533
|
+
text,
|
|
534
|
+
diagramType: HEADER.exec(text)?.[0]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
87
538
|
//#region src/validate.ts
|
|
88
539
|
/**
|
|
89
540
|
* Deterministic validation of a content repository (the slim contract).
|
|
@@ -335,6 +786,24 @@ function checkDmnXml(raw, opts = {}) {
|
|
|
335
786
|
else for (const id of diRequired) if (!di.has(id)) err("dmn/di", `${id} has no DMNDI shape (breaks the visual editor)`);
|
|
336
787
|
return { findings };
|
|
337
788
|
}
|
|
789
|
+
/** the Mermaid types the header message names: the common ones, not every one */
|
|
790
|
+
const COMMON_MERMAID_TYPES = "flowchart, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt, pie, mindmap";
|
|
791
|
+
/**
|
|
792
|
+
* Mermaid's own first step (its type detection, @designiq/notations/mermaid
|
|
793
|
+
* mirrors it) for a ```mermaid fence opening on document line `line`: after
|
|
794
|
+
* the front matter, directives and comments, the diagram's first line must
|
|
795
|
+
* start with a diagram type. Light on purpose: the grammar itself needs
|
|
796
|
+
* mermaid and a DOM, which the validator never loads; the Markdown view
|
|
797
|
+
* shows those errors live. The message names the document's line (a Finding
|
|
798
|
+
* has no line of its own).
|
|
799
|
+
*/
|
|
800
|
+
function mermaidFenceProblem(content, line) {
|
|
801
|
+
const header = mermaidHeader(content);
|
|
802
|
+
if (!header) return `line ${line}: empty Mermaid diagram: the first line must name a diagram type (${COMMON_MERMAID_TYPES}, …)`;
|
|
803
|
+
if (header.diagramType) return void 0;
|
|
804
|
+
const word = header.text.split(/[\s:;]/, 1)[0];
|
|
805
|
+
return `line ${line + header.line}: '${word}' is not a Mermaid diagram type (${COMMON_MERMAID_TYPES}, …)`;
|
|
806
|
+
}
|
|
338
807
|
/**
|
|
339
808
|
* Baseline parse gate for models WITHOUT a dedicated checker (any registered
|
|
340
809
|
* notation that is not .bpmn/.dmn): a discovered model must at minimum parse
|
|
@@ -368,10 +837,21 @@ function checkModelBaseline(raw, opts) {
|
|
|
368
837
|
if (wf !== true) err(`not well-formed XML: ${wf.err.msg}`);
|
|
369
838
|
break;
|
|
370
839
|
}
|
|
371
|
-
case "dsl":
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
840
|
+
case "dsl":
|
|
841
|
+
try {
|
|
842
|
+
extractModelGraph(opts.notation, raw);
|
|
843
|
+
} catch (e) {
|
|
844
|
+
err(`not parseable: ${e.message}`);
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
case "markdown": for (const fence of markdownFences(raw)) {
|
|
848
|
+
const problem = fence.lang === "mermaid" ? mermaidFenceProblem(fence.content, fence.line) : void 0;
|
|
849
|
+
if (problem) findings.push({
|
|
850
|
+
severity: "WARN",
|
|
851
|
+
ruleId: "mermaid/diagram-type",
|
|
852
|
+
file,
|
|
853
|
+
message: problem
|
|
854
|
+
});
|
|
375
855
|
}
|
|
376
856
|
}
|
|
377
857
|
return findings;
|
|
@@ -436,7 +916,7 @@ async function runCli() {
|
|
|
436
916
|
/** optional single-model filter (a model file stem) */
|
|
437
917
|
const only = argv[0];
|
|
438
918
|
const rel = (p) => p.startsWith("/") ? relative(ROOT, p) : p;
|
|
439
|
-
const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-
|
|
919
|
+
const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-wjC_epKd.js");
|
|
440
920
|
const cfg = loadContentConfig(ROOT);
|
|
441
921
|
if (!cfg) {
|
|
442
922
|
console.error(notContentRepoError(ROOT));
|
package/dist/validate.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { checkBpmnXml, checkDmnXml, checkModel, checkModelBaseline, runCli };
|
|
1
|
+
export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as runCli } from "./validate-DjtZJTGk.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miragon/design-iq-validator",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Platform-owned validator for designIQ content repos — designiq.yml discovery, BPMN/DMN structure and diagram coverage, cross-model references, and a parse check for every other notation. Runs against any checkout via --root; never executes content-repo code.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"yaml": "2.9.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"tsdown": "0.23.
|
|
41
|
+
"tsdown": "0.23.1",
|
|
42
42
|
"@designiq/notations": "0.1.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
package/dist/content-DgyuLYdD.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { a as discoverModels, i as contentConfigConflict, o as legacyContentConfigFile, s as loadContentConfig, t as CONTENT_CONFIG_FILE } from "./content-Bsv9ZLFd.js";
|
|
2
|
-
export { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig };
|