docmeta 0.1.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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/chunk-NTXC32C4.js +1091 -0
- package/dist/chunk-NTXC32C4.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +280 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var DocmetaError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "DocmetaError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/core/config.ts
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
var CONFIG_NAMES = ["docmeta.config.yaml", "docmeta.config.yml"];
|
|
16
|
+
function asStringList(value, field, source) {
|
|
17
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
18
|
+
throw new DocmetaError(
|
|
19
|
+
`${source}: "${field}" must be a list of strings.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function parseConfig(text, source) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = parseYaml(text);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new DocmetaError(
|
|
30
|
+
`${source}: invalid YAML: ${err.message}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (raw == null) return {};
|
|
34
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
35
|
+
throw new DocmetaError(`${source}: top level must be a mapping.`);
|
|
36
|
+
}
|
|
37
|
+
const obj = raw;
|
|
38
|
+
const config = {};
|
|
39
|
+
if (obj.paths !== void 0) config.paths = asStringList(obj.paths, "paths", source);
|
|
40
|
+
if (obj.exclude !== void 0)
|
|
41
|
+
config.exclude = asStringList(obj.exclude, "exclude", source);
|
|
42
|
+
if (obj.schemas !== void 0)
|
|
43
|
+
config.schemas = asStringList(obj.schemas, "schemas", source);
|
|
44
|
+
if (obj.overrides !== void 0) {
|
|
45
|
+
if (!Array.isArray(obj.overrides)) {
|
|
46
|
+
throw new DocmetaError(`${source}: "overrides" must be a list.`);
|
|
47
|
+
}
|
|
48
|
+
config.overrides = obj.overrides.map((entry, i) => {
|
|
49
|
+
if (typeof entry !== "object" || entry === null) {
|
|
50
|
+
throw new DocmetaError(`${source}: overrides[${i}] must be a mapping.`);
|
|
51
|
+
}
|
|
52
|
+
const e = entry;
|
|
53
|
+
if (typeof e.files !== "string") {
|
|
54
|
+
throw new DocmetaError(
|
|
55
|
+
`${source}: overrides[${i}].files must be a string glob.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
files: e.files,
|
|
60
|
+
schemas: asStringList(e.schemas, `overrides[${i}].schemas`, source)
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
async function loadConfig(explicitPath, cwd = process.cwd()) {
|
|
67
|
+
if (explicitPath) {
|
|
68
|
+
let text;
|
|
69
|
+
try {
|
|
70
|
+
text = await readFile(explicitPath, "utf8");
|
|
71
|
+
} catch {
|
|
72
|
+
throw new DocmetaError(`Config file not found: "${explicitPath}".`);
|
|
73
|
+
}
|
|
74
|
+
return { config: parseConfig(text, explicitPath), path: explicitPath };
|
|
75
|
+
}
|
|
76
|
+
for (const name of CONFIG_NAMES) {
|
|
77
|
+
const p = resolve(cwd, name);
|
|
78
|
+
try {
|
|
79
|
+
const text = await readFile(p, "utf8");
|
|
80
|
+
return { config: parseConfig(text, name), path: p };
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/resolve-schema.ts
|
|
88
|
+
import picomatch from "picomatch";
|
|
89
|
+
var DEFAULT_SCHEMA = "google:okf:0.1";
|
|
90
|
+
var FILE_SCHEMA_KEY = "$schema";
|
|
91
|
+
var matcherCache = /* @__PURE__ */ new Map();
|
|
92
|
+
function matches(glob, filePath) {
|
|
93
|
+
let m = matcherCache.get(glob);
|
|
94
|
+
if (!m) {
|
|
95
|
+
m = picomatch(glob, { dot: true });
|
|
96
|
+
matcherCache.set(glob, m);
|
|
97
|
+
}
|
|
98
|
+
return m(filePath.replace(/\\/g, "/"));
|
|
99
|
+
}
|
|
100
|
+
function coerceFileSchema(value) {
|
|
101
|
+
if (value === void 0 || value === null) return void 0;
|
|
102
|
+
if (typeof value === "string") return [value];
|
|
103
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid "${FILE_SCHEMA_KEY}": must be a string or a list of strings.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function dedupe(refs) {
|
|
111
|
+
return [...new Set(refs)];
|
|
112
|
+
}
|
|
113
|
+
function resolveSchemaSet(params) {
|
|
114
|
+
const { filePath, fileSchema, cliSchemas, config } = params;
|
|
115
|
+
if (cliSchemas && cliSchemas.length > 0) return dedupe(cliSchemas);
|
|
116
|
+
const fromFile = coerceFileSchema(fileSchema);
|
|
117
|
+
if (fromFile && fromFile.length > 0) return dedupe(fromFile);
|
|
118
|
+
if (config?.overrides) {
|
|
119
|
+
for (const ov of config.overrides) {
|
|
120
|
+
if (matches(ov.files, filePath) && ov.schemas.length > 0) {
|
|
121
|
+
return dedupe(ov.schemas);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (config?.schemas && config.schemas.length > 0) return dedupe(config.schemas);
|
|
126
|
+
return [DEFAULT_SCHEMA];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/core/schema-registry.ts
|
|
130
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
131
|
+
|
|
132
|
+
// src/schemas/okf/0.1.json
|
|
133
|
+
var __default = {
|
|
134
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
135
|
+
$id: "google:okf:0.1",
|
|
136
|
+
title: "Open Knowledge Format (OKF) v0.1",
|
|
137
|
+
description: "Frontmatter schema for OKF v0.1 concept files. OKF requires only `type`; all other fields are recommended, and unknown keys are explicitly tolerated. See https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md",
|
|
138
|
+
type: "object",
|
|
139
|
+
required: ["type"],
|
|
140
|
+
additionalProperties: true,
|
|
141
|
+
properties: {
|
|
142
|
+
type: {
|
|
143
|
+
type: "string",
|
|
144
|
+
minLength: 1,
|
|
145
|
+
description: "A short string identifying the kind of concept. Used for routing, filtering, and presentation."
|
|
146
|
+
},
|
|
147
|
+
title: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "Human-readable display name. If omitted, consumers MAY derive a title from the filename."
|
|
150
|
+
},
|
|
151
|
+
description: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Single-sentence summary used for indexing and previews."
|
|
154
|
+
},
|
|
155
|
+
resource: {
|
|
156
|
+
type: "string",
|
|
157
|
+
format: "uri",
|
|
158
|
+
description: "A URI uniquely identifying the underlying asset. Absent for abstract concepts."
|
|
159
|
+
},
|
|
160
|
+
tags: {
|
|
161
|
+
type: "array",
|
|
162
|
+
items: { type: "string" },
|
|
163
|
+
description: "Short strings for categorization."
|
|
164
|
+
},
|
|
165
|
+
timestamp: {
|
|
166
|
+
type: "string",
|
|
167
|
+
format: "date-time",
|
|
168
|
+
description: "ISO 8601 datetime of last meaningful change."
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/core/schema-registry.ts
|
|
174
|
+
var BUILTINS = /* @__PURE__ */ new Map([
|
|
175
|
+
["google:okf:0.1", __default]
|
|
176
|
+
]);
|
|
177
|
+
function listBuiltins() {
|
|
178
|
+
return [...BUILTINS.entries()].map(([id, schema]) => ({
|
|
179
|
+
id,
|
|
180
|
+
title: typeof schema.title === "string" ? schema.title : id,
|
|
181
|
+
description: typeof schema.description === "string" ? schema.description : ""
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
var BUILTIN_ID = /^[a-z0-9][a-z0-9._-]*(?::[a-z0-9][a-z0-9._-]*)+$/i;
|
|
185
|
+
function classifyRef(ref) {
|
|
186
|
+
if (/^https?:\/\//i.test(ref)) return { kind: "url", ref };
|
|
187
|
+
if (!ref.includes("/") && !ref.includes("\\") && !ref.toLowerCase().endsWith(".json") && BUILTIN_ID.test(ref)) {
|
|
188
|
+
return { kind: "builtin", ref };
|
|
189
|
+
}
|
|
190
|
+
return { kind: "file", ref };
|
|
191
|
+
}
|
|
192
|
+
var urlCache = /* @__PURE__ */ new Map();
|
|
193
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
194
|
+
async function loadSchema(ref, options = {}) {
|
|
195
|
+
const { kind } = classifyRef(ref);
|
|
196
|
+
if (kind === "builtin") {
|
|
197
|
+
const schema = BUILTINS.get(ref);
|
|
198
|
+
if (!schema) {
|
|
199
|
+
const available = [...BUILTINS.keys()].join(", ");
|
|
200
|
+
throw new DocmetaError(
|
|
201
|
+
`Unknown built-in schema "${ref}". Available: ${available || "(none)"}.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return schema;
|
|
205
|
+
}
|
|
206
|
+
if (kind === "url") {
|
|
207
|
+
const cached = urlCache.get(ref);
|
|
208
|
+
if (cached) return cached;
|
|
209
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
210
|
+
let res;
|
|
211
|
+
try {
|
|
212
|
+
res = await fetch(ref, { signal: AbortSignal.timeout(timeoutMs) });
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const e = err;
|
|
215
|
+
if (e.name === "TimeoutError" || e.name === "AbortError") {
|
|
216
|
+
throw new DocmetaError(
|
|
217
|
+
`Failed to fetch schema "${ref}": timed out after ${timeoutMs}ms.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
throw new DocmetaError(`Failed to fetch schema "${ref}": ${e.message}`);
|
|
221
|
+
}
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
throw new DocmetaError(
|
|
224
|
+
`Failed to fetch schema "${ref}": HTTP ${res.status}.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
let json;
|
|
228
|
+
try {
|
|
229
|
+
json = await res.json();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
throw new DocmetaError(
|
|
232
|
+
`Schema "${ref}" did not return valid JSON: ${err.message}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
urlCache.set(ref, json);
|
|
236
|
+
return json;
|
|
237
|
+
}
|
|
238
|
+
let raw;
|
|
239
|
+
try {
|
|
240
|
+
raw = await readFile2(ref, "utf8");
|
|
241
|
+
} catch {
|
|
242
|
+
throw new DocmetaError(`Schema file not found: "${ref}".`);
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return JSON.parse(raw);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw new DocmetaError(
|
|
248
|
+
`Schema file "${ref}" is not valid JSON: ${err.message}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/core/validator.ts
|
|
254
|
+
import * as AjvDraft07Ns from "ajv";
|
|
255
|
+
import * as Ajv2019Ns from "ajv/dist/2019.js";
|
|
256
|
+
import * as Ajv2020Ns from "ajv/dist/2020.js";
|
|
257
|
+
import * as AjvDraft04Ns from "ajv-draft-04";
|
|
258
|
+
import * as addFormatsNs from "ajv-formats";
|
|
259
|
+
import { createRequire } from "module";
|
|
260
|
+
var draft06MetaSchema = createRequire(import.meta.url)(
|
|
261
|
+
"ajv/dist/refs/json-schema-draft-06.json"
|
|
262
|
+
);
|
|
263
|
+
var AjvDraft07 = AjvDraft07Ns.default;
|
|
264
|
+
var Ajv2019 = Ajv2019Ns.default;
|
|
265
|
+
var Ajv2020 = Ajv2020Ns.default;
|
|
266
|
+
var AjvDraft04 = AjvDraft04Ns.default;
|
|
267
|
+
var addFormats = addFormatsNs.default;
|
|
268
|
+
function dialectOf(schema) {
|
|
269
|
+
const meta = typeof schema.$schema === "string" ? schema.$schema : "";
|
|
270
|
+
if (meta.includes("2019-09")) return "2019";
|
|
271
|
+
if (meta.includes("draft-07") || meta.includes("draft/7") || meta.includes("draft-06") || meta.includes("draft/6")) {
|
|
272
|
+
return "draft7";
|
|
273
|
+
}
|
|
274
|
+
if (meta.includes("draft-04") || meta.includes("draft/4")) return "draft4";
|
|
275
|
+
return "2020";
|
|
276
|
+
}
|
|
277
|
+
function buildAjv(dialect) {
|
|
278
|
+
const opts = { allErrors: true, strict: false };
|
|
279
|
+
const ajv = dialect === "2019" ? new Ajv2019(opts) : dialect === "draft7" ? new AjvDraft07(opts) : dialect === "draft4" ? new AjvDraft04(opts) : new Ajv2020(opts);
|
|
280
|
+
addFormats(ajv);
|
|
281
|
+
if (dialect === "draft7") ajv.addMetaSchema(draft06MetaSchema);
|
|
282
|
+
return ajv;
|
|
283
|
+
}
|
|
284
|
+
var Validator = class {
|
|
285
|
+
ajvByDialect = /* @__PURE__ */ new Map();
|
|
286
|
+
cache = /* @__PURE__ */ new Map();
|
|
287
|
+
ajvFor(dialect) {
|
|
288
|
+
let ajv = this.ajvByDialect.get(dialect);
|
|
289
|
+
if (!ajv) {
|
|
290
|
+
ajv = buildAjv(dialect);
|
|
291
|
+
this.ajvByDialect.set(dialect, ajv);
|
|
292
|
+
}
|
|
293
|
+
return ajv;
|
|
294
|
+
}
|
|
295
|
+
async compile(ref) {
|
|
296
|
+
const cached = this.cache.get(ref);
|
|
297
|
+
if (cached) return cached;
|
|
298
|
+
const schema = await loadSchema(ref);
|
|
299
|
+
let fn;
|
|
300
|
+
try {
|
|
301
|
+
fn = this.ajvFor(dialectOf(schema)).compile(schema);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
throw new DocmetaError(
|
|
304
|
+
`Schema "${ref}" failed to compile: ${err.message}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
this.cache.set(ref, fn);
|
|
308
|
+
return fn;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Validate `data` against every schema in `refs`. Returns all violations,
|
|
312
|
+
* each tagged with the schema that produced it and a source line via
|
|
313
|
+
* `lineFor`.
|
|
314
|
+
*/
|
|
315
|
+
async validate(data, refs, lineFor) {
|
|
316
|
+
const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
|
|
317
|
+
void _omit;
|
|
318
|
+
const errors = [];
|
|
319
|
+
for (const ref of refs) {
|
|
320
|
+
const fn = await this.compile(ref);
|
|
321
|
+
const ok = fn(subject);
|
|
322
|
+
if (ok) continue;
|
|
323
|
+
for (const e of fn.errors ?? []) {
|
|
324
|
+
errors.push(toFieldError(ref, e, lineFor));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return errors;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
function toFieldError(schema, e, lineFor) {
|
|
331
|
+
const instancePath = e.instancePath;
|
|
332
|
+
let message = e.message ?? "is invalid";
|
|
333
|
+
if (e.keyword === "required" && e.params && "missingProperty" in e.params) {
|
|
334
|
+
message = `must have required property '${String(e.params.missingProperty)}'`;
|
|
335
|
+
} else if (e.keyword === "additionalProperties" && e.params && "additionalProperty" in e.params) {
|
|
336
|
+
message = `must NOT have additional property '${String(e.params.additionalProperty)}'`;
|
|
337
|
+
}
|
|
338
|
+
const line = lineFor(instancePath);
|
|
339
|
+
return { schema, instancePath, message, ...line != null ? { line } : {} };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/commands/validate.ts
|
|
343
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
344
|
+
import { resolve as resolve3, extname as extname2 } from "path";
|
|
345
|
+
|
|
346
|
+
// src/extractors/markdown.ts
|
|
347
|
+
import { parseDocument, LineCounter, isMap, isSeq, isScalar } from "yaml";
|
|
348
|
+
var OPENING = /^---\r?\n/;
|
|
349
|
+
function splitFrontmatter(content) {
|
|
350
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
351
|
+
if (!OPENING.test(body)) return null;
|
|
352
|
+
const lines = body.split(/\r?\n/);
|
|
353
|
+
let close = -1;
|
|
354
|
+
for (let i = 1; i < lines.length; i++) {
|
|
355
|
+
if (lines[i] === "---" || lines[i] === "...") {
|
|
356
|
+
close = i;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (close === -1) return null;
|
|
361
|
+
return { raw: lines.slice(1, close).join("\n"), firstYamlLine: 2 };
|
|
362
|
+
}
|
|
363
|
+
function escapePointerSegment(key) {
|
|
364
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
365
|
+
}
|
|
366
|
+
function buildLineMap(doc, lc, prefixLines) {
|
|
367
|
+
const map = /* @__PURE__ */ new Map();
|
|
368
|
+
map.set("", 1);
|
|
369
|
+
const lineAt = (offset) => offset == null ? void 0 : lc.linePos(offset).line + prefixLines;
|
|
370
|
+
const walk = (node, pointer) => {
|
|
371
|
+
if (isMap(node)) {
|
|
372
|
+
for (const pair of node.items) {
|
|
373
|
+
const key = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
|
|
374
|
+
const ptr = `${pointer}/${escapePointerSegment(key)}`;
|
|
375
|
+
const line = lineAt(
|
|
376
|
+
pair.key?.range?.[0]
|
|
377
|
+
);
|
|
378
|
+
if (line != null) map.set(ptr, line);
|
|
379
|
+
if (pair.value) walk(pair.value, ptr);
|
|
380
|
+
}
|
|
381
|
+
} else if (isSeq(node)) {
|
|
382
|
+
node.items.forEach((item, i) => {
|
|
383
|
+
const ptr = `${pointer}/${i}`;
|
|
384
|
+
const line = lineAt(
|
|
385
|
+
item?.range?.[0]
|
|
386
|
+
);
|
|
387
|
+
if (line != null) map.set(ptr, line);
|
|
388
|
+
walk(item, ptr);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
if (doc.contents) walk(doc.contents, "");
|
|
393
|
+
return map;
|
|
394
|
+
}
|
|
395
|
+
function lineForFactory(map) {
|
|
396
|
+
return (pointer) => {
|
|
397
|
+
const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment(pointer)}` : pointer;
|
|
398
|
+
if (map.has(start)) return map.get(start);
|
|
399
|
+
let p = start;
|
|
400
|
+
while (p.length > 0) {
|
|
401
|
+
const idx = p.lastIndexOf("/");
|
|
402
|
+
if (idx < 0) break;
|
|
403
|
+
p = p.slice(0, idx);
|
|
404
|
+
if (map.has(p)) return map.get(p);
|
|
405
|
+
}
|
|
406
|
+
return map.get("");
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function extractFrontmatter(content, format) {
|
|
410
|
+
const block = splitFrontmatter(content);
|
|
411
|
+
if (!block) {
|
|
412
|
+
return { data: {}, present: false, format, lineFor: () => void 0 };
|
|
413
|
+
}
|
|
414
|
+
const lc = new LineCounter();
|
|
415
|
+
const doc = parseDocument(block.raw, { lineCounter: lc });
|
|
416
|
+
if (doc.errors.length > 0) {
|
|
417
|
+
const e = doc.errors[0];
|
|
418
|
+
throw new Error(`Invalid YAML frontmatter: ${e?.message ?? "parse error"}`);
|
|
419
|
+
}
|
|
420
|
+
const js = doc.toJS({ maxAliasCount: 100 });
|
|
421
|
+
const data = js && typeof js === "object" && !Array.isArray(js) ? js : {};
|
|
422
|
+
const map = buildLineMap(doc, lc, block.firstYamlLine - 1);
|
|
423
|
+
return { data, present: true, format, lineFor: lineForFactory(map) };
|
|
424
|
+
}
|
|
425
|
+
var markdownExtractor = {
|
|
426
|
+
name: "markdown",
|
|
427
|
+
extensions: [".md", ".markdown"],
|
|
428
|
+
implemented: true,
|
|
429
|
+
extract: (content) => extractFrontmatter(content, "markdown")
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/extractors/mdx.ts
|
|
433
|
+
var mdxExtractor = {
|
|
434
|
+
name: "mdx",
|
|
435
|
+
extensions: [".mdx"],
|
|
436
|
+
implemented: true,
|
|
437
|
+
extract: (content) => extractFrontmatter(content, "mdx")
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/extractors/asciidoc.ts
|
|
441
|
+
import { parse as parseYamlScalar } from "yaml";
|
|
442
|
+
var OPENING2 = /^---\r?\n/;
|
|
443
|
+
var TITLE = /^=\s+(.+?)\s*$/;
|
|
444
|
+
var UNSET = /^:(?:!([^:\s]+)|([^:\s]+)!):\s*$/;
|
|
445
|
+
var SET = /^:([^:\s!][^:\s]*):(?:\s+(.*\S))?\s*$/;
|
|
446
|
+
function escapePointerSegment2(key) {
|
|
447
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
448
|
+
}
|
|
449
|
+
function typeValue(raw) {
|
|
450
|
+
try {
|
|
451
|
+
return parseYamlScalar(raw);
|
|
452
|
+
} catch {
|
|
453
|
+
return raw;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function lineForFactory2(map) {
|
|
457
|
+
return (pointer) => {
|
|
458
|
+
const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment2(pointer)}` : pointer;
|
|
459
|
+
if (map.has(start)) return map.get(start);
|
|
460
|
+
let p = start;
|
|
461
|
+
while (p.length > 0) {
|
|
462
|
+
const idx = p.lastIndexOf("/");
|
|
463
|
+
if (idx < 0) break;
|
|
464
|
+
p = p.slice(0, idx);
|
|
465
|
+
if (map.has(p)) return map.get(p);
|
|
466
|
+
}
|
|
467
|
+
return map.get("");
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function extractHeader(content) {
|
|
471
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
472
|
+
const lines = body.split(/\r?\n/);
|
|
473
|
+
const data = {};
|
|
474
|
+
const map = /* @__PURE__ */ new Map();
|
|
475
|
+
map.set("", 1);
|
|
476
|
+
const setKey = (key, value, line) => {
|
|
477
|
+
data[key] = value;
|
|
478
|
+
map.set(`/${escapePointerSegment2(key)}`, line);
|
|
479
|
+
};
|
|
480
|
+
for (const [i, line] of lines.entries()) {
|
|
481
|
+
if (line.trim() === "") break;
|
|
482
|
+
if (i === 0) {
|
|
483
|
+
const title = TITLE.exec(line);
|
|
484
|
+
if (title?.[1] != null) {
|
|
485
|
+
setKey("title", title[1], i + 1);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const unset = UNSET.exec(line);
|
|
490
|
+
if (unset) {
|
|
491
|
+
const name = unset[1] ?? unset[2];
|
|
492
|
+
if (name != null) setKey(name, false, i + 1);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const set = SET.exec(line);
|
|
496
|
+
if (set?.[1] != null) {
|
|
497
|
+
const value = set[2] === void 0 ? true : typeValue(set[2]);
|
|
498
|
+
setKey(set[1], value, i + 1);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const present = Object.keys(data).length > 0;
|
|
503
|
+
return {
|
|
504
|
+
data,
|
|
505
|
+
present,
|
|
506
|
+
format: "asciidoc",
|
|
507
|
+
lineFor: lineForFactory2(map)
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
var asciidocExtractor = {
|
|
511
|
+
name: "asciidoc",
|
|
512
|
+
extensions: [".adoc", ".asciidoc"],
|
|
513
|
+
implemented: true,
|
|
514
|
+
extract(content) {
|
|
515
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
516
|
+
if (OPENING2.test(body)) {
|
|
517
|
+
const fm = extractFrontmatter(content, "asciidoc");
|
|
518
|
+
if (fm.present) return fm;
|
|
519
|
+
}
|
|
520
|
+
return extractHeader(content);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/extractors/rst.ts
|
|
525
|
+
import { parse as parseYamlScalar2 } from "yaml";
|
|
526
|
+
var OPENING3 = /^---\r?\n/;
|
|
527
|
+
var FIELD = /^:([^:]+):(?:\s+(.*\S))?\s*$/;
|
|
528
|
+
var ADORN = /^([=\-~:.'"*+#_^<>])\1+$/;
|
|
529
|
+
function escapePointerSegment3(key) {
|
|
530
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
531
|
+
}
|
|
532
|
+
function typeValue2(raw) {
|
|
533
|
+
try {
|
|
534
|
+
return parseYamlScalar2(raw);
|
|
535
|
+
} catch {
|
|
536
|
+
return raw;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function lineForFactory3(map) {
|
|
540
|
+
return (pointer) => {
|
|
541
|
+
const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment3(pointer)}` : pointer;
|
|
542
|
+
if (map.has(start)) return map.get(start);
|
|
543
|
+
let p = start;
|
|
544
|
+
while (p.length > 0) {
|
|
545
|
+
const idx = p.lastIndexOf("/");
|
|
546
|
+
if (idx < 0) break;
|
|
547
|
+
p = p.slice(0, idx);
|
|
548
|
+
if (map.has(p)) return map.get(p);
|
|
549
|
+
}
|
|
550
|
+
return map.get("");
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function adornment(line) {
|
|
554
|
+
const m = ADORN.exec(line);
|
|
555
|
+
return m && m[1] != null ? { char: m[1], length: line.length } : null;
|
|
556
|
+
}
|
|
557
|
+
function parseTitle(lines) {
|
|
558
|
+
let i = 0;
|
|
559
|
+
while (i < lines.length && (lines[i] ?? "").trim() === "") i++;
|
|
560
|
+
if (i >= lines.length) return { next: i };
|
|
561
|
+
const first = lines[i] ?? "";
|
|
562
|
+
if (FIELD.test(first)) return { next: i };
|
|
563
|
+
const over = adornment(first);
|
|
564
|
+
if (over) {
|
|
565
|
+
const text = lines[i + 1] ?? "";
|
|
566
|
+
const under2 = adornment(lines[i + 2] ?? "");
|
|
567
|
+
if (text.trim() !== "" && !FIELD.test(text) && under2 != null && under2.char === over.char && under2.length === over.length && over.length >= text.trim().length) {
|
|
568
|
+
return { title: text.trim(), line: i + 2, next: i + 3 };
|
|
569
|
+
}
|
|
570
|
+
return { next: i };
|
|
571
|
+
}
|
|
572
|
+
const under = adornment(lines[i + 1] ?? "");
|
|
573
|
+
if (under != null && under.length >= first.trim().length) {
|
|
574
|
+
return { title: first.trim(), line: i + 1, next: i + 2 };
|
|
575
|
+
}
|
|
576
|
+
return { next: i };
|
|
577
|
+
}
|
|
578
|
+
function extractDocinfo(content) {
|
|
579
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
580
|
+
const lines = body.split(/\r?\n/);
|
|
581
|
+
const data = {};
|
|
582
|
+
const map = /* @__PURE__ */ new Map();
|
|
583
|
+
let blockStart = -1;
|
|
584
|
+
const titleInfo = parseTitle(lines);
|
|
585
|
+
if (titleInfo.title != null && titleInfo.line != null) {
|
|
586
|
+
data.title = titleInfo.title;
|
|
587
|
+
map.set("/title", titleInfo.line);
|
|
588
|
+
blockStart = titleInfo.line;
|
|
589
|
+
}
|
|
590
|
+
let i = titleInfo.next;
|
|
591
|
+
while (i < lines.length && (lines[i] ?? "").trim() === "") i++;
|
|
592
|
+
for (; i < lines.length; i++) {
|
|
593
|
+
const line = lines[i] ?? "";
|
|
594
|
+
if (line.trim() === "") break;
|
|
595
|
+
const field = FIELD.exec(line);
|
|
596
|
+
if (!field || field[1] == null) break;
|
|
597
|
+
const name = field[1].trim();
|
|
598
|
+
const value = field[2] === void 0 ? true : typeValue2(field[2]);
|
|
599
|
+
if (blockStart === -1) blockStart = i + 1;
|
|
600
|
+
data[name] = value;
|
|
601
|
+
map.set(`/${escapePointerSegment3(name)}`, i + 1);
|
|
602
|
+
}
|
|
603
|
+
const present = blockStart !== -1;
|
|
604
|
+
if (present) map.set("", blockStart);
|
|
605
|
+
return {
|
|
606
|
+
data,
|
|
607
|
+
present,
|
|
608
|
+
format: "rst",
|
|
609
|
+
lineFor: lineForFactory3(map)
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
var rstExtractor = {
|
|
613
|
+
name: "rst",
|
|
614
|
+
extensions: [".rst"],
|
|
615
|
+
implemented: true,
|
|
616
|
+
extract(content) {
|
|
617
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
618
|
+
if (OPENING3.test(body)) {
|
|
619
|
+
const fm = extractFrontmatter(content, "rst");
|
|
620
|
+
if (fm.present) return fm;
|
|
621
|
+
}
|
|
622
|
+
return extractDocinfo(content);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/extractors/xml.ts
|
|
627
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
628
|
+
import { parse as parseYamlScalar3 } from "yaml";
|
|
629
|
+
function escapePointerSegment4(key) {
|
|
630
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
631
|
+
}
|
|
632
|
+
function typeValue3(raw) {
|
|
633
|
+
if (raw === "") return "";
|
|
634
|
+
try {
|
|
635
|
+
return parseYamlScalar3(raw);
|
|
636
|
+
} catch {
|
|
637
|
+
return raw;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function lineForFactory4(map) {
|
|
641
|
+
return (pointer) => {
|
|
642
|
+
const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment4(pointer)}` : pointer;
|
|
643
|
+
if (map.has(start)) return map.get(start);
|
|
644
|
+
let p = start;
|
|
645
|
+
while (p.length > 0) {
|
|
646
|
+
const idx = p.lastIndexOf("/");
|
|
647
|
+
if (idx < 0) break;
|
|
648
|
+
p = p.slice(0, idx);
|
|
649
|
+
if (map.has(p)) return map.get(p);
|
|
650
|
+
}
|
|
651
|
+
return map.get("");
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
var xmlExtractor = {
|
|
655
|
+
name: "xml",
|
|
656
|
+
extensions: [".xml"],
|
|
657
|
+
implemented: true,
|
|
658
|
+
extract(content) {
|
|
659
|
+
const body = content.charCodeAt(0) === 65279 ? content.slice(1) : content;
|
|
660
|
+
const errors = [];
|
|
661
|
+
const doc = new DOMParser({
|
|
662
|
+
onError: (level, msg) => {
|
|
663
|
+
if (level === "error" || level === "fatalError") errors.push(msg);
|
|
664
|
+
}
|
|
665
|
+
}).parseFromString(body, "text/xml");
|
|
666
|
+
if (errors.length > 0) {
|
|
667
|
+
throw new Error(`Invalid XML: ${errors[0] ?? "parse error"}`);
|
|
668
|
+
}
|
|
669
|
+
const root = doc.documentElement;
|
|
670
|
+
if (!root) {
|
|
671
|
+
return { data: {}, present: false, format: "xml", lineFor: () => void 0 };
|
|
672
|
+
}
|
|
673
|
+
const data = {};
|
|
674
|
+
const map = /* @__PURE__ */ new Map();
|
|
675
|
+
const rootLine = root.lineNumber ?? 1;
|
|
676
|
+
map.set("", rootLine);
|
|
677
|
+
const attrs = root.attributes;
|
|
678
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
679
|
+
const attr = attrs.item(i);
|
|
680
|
+
if (!attr) continue;
|
|
681
|
+
const name = attr.name;
|
|
682
|
+
if (name === "xmlns" || name.startsWith("xmlns:")) continue;
|
|
683
|
+
data[name] = typeValue3(attr.value);
|
|
684
|
+
map.set(`/${escapePointerSegment4(name)}`, attr.lineNumber ?? rootLine);
|
|
685
|
+
}
|
|
686
|
+
const present = Object.keys(data).length > 0;
|
|
687
|
+
return { data, present, format: "xml", lineFor: lineForFactory4(map) };
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/extractors/html.ts
|
|
692
|
+
import { parse, defaultTreeAdapter } from "parse5";
|
|
693
|
+
import { parse as parseYamlScalar4 } from "yaml";
|
|
694
|
+
function escapePointerSegment5(key) {
|
|
695
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
696
|
+
}
|
|
697
|
+
function typeValue4(raw) {
|
|
698
|
+
if (raw === "") return "";
|
|
699
|
+
try {
|
|
700
|
+
return parseYamlScalar4(raw);
|
|
701
|
+
} catch {
|
|
702
|
+
return raw;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function lineForFactory5(map) {
|
|
706
|
+
return (pointer) => {
|
|
707
|
+
const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment5(pointer)}` : pointer;
|
|
708
|
+
if (map.has(start)) return map.get(start);
|
|
709
|
+
let p = start;
|
|
710
|
+
while (p.length > 0) {
|
|
711
|
+
const idx = p.lastIndexOf("/");
|
|
712
|
+
if (idx < 0) break;
|
|
713
|
+
p = p.slice(0, idx);
|
|
714
|
+
if (map.has(p)) return map.get(p);
|
|
715
|
+
}
|
|
716
|
+
return map.get("");
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function attrValue(el, name) {
|
|
720
|
+
return el.attrs.find((a) => a.name === name)?.value;
|
|
721
|
+
}
|
|
722
|
+
var htmlExtractor = {
|
|
723
|
+
name: "html",
|
|
724
|
+
extensions: [".html", ".htm"],
|
|
725
|
+
implemented: true,
|
|
726
|
+
extract(content) {
|
|
727
|
+
const doc = parse(content, { sourceCodeLocationInfo: true });
|
|
728
|
+
const data = {};
|
|
729
|
+
const map = /* @__PURE__ */ new Map();
|
|
730
|
+
map.set("", 1);
|
|
731
|
+
const setKey = (key, value, line) => {
|
|
732
|
+
data[key] = value;
|
|
733
|
+
if (line != null) map.set(`/${escapePointerSegment5(key)}`, line);
|
|
734
|
+
};
|
|
735
|
+
const visit = (node) => {
|
|
736
|
+
if (defaultTreeAdapter.isElementNode(node)) {
|
|
737
|
+
const line = node.sourceCodeLocation?.startLine;
|
|
738
|
+
if (node.tagName === "title") {
|
|
739
|
+
if (data.title === void 0) {
|
|
740
|
+
const first = node.childNodes[0];
|
|
741
|
+
const text = first && defaultTreeAdapter.isTextNode(first) ? first.value : "";
|
|
742
|
+
setKey("title", text, line);
|
|
743
|
+
}
|
|
744
|
+
} else if (node.tagName === "meta") {
|
|
745
|
+
const key = attrValue(node, "name") ?? attrValue(node, "property");
|
|
746
|
+
const value = attrValue(node, "content");
|
|
747
|
+
if (key != null && value != null) {
|
|
748
|
+
setKey(key, typeValue4(value), line);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (defaultTreeAdapter.isElementNode(node)) {
|
|
753
|
+
for (const child of node.childNodes) visit(child);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
for (const child of doc.childNodes) visit(child);
|
|
757
|
+
const present = Object.keys(data).length > 0;
|
|
758
|
+
return { data, present, format: "html", lineFor: lineForFactory5(map) };
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/extractors/index.ts
|
|
763
|
+
var EXTRACTORS = [
|
|
764
|
+
markdownExtractor,
|
|
765
|
+
mdxExtractor,
|
|
766
|
+
asciidocExtractor,
|
|
767
|
+
rstExtractor,
|
|
768
|
+
xmlExtractor,
|
|
769
|
+
htmlExtractor
|
|
770
|
+
];
|
|
771
|
+
var byExtension = /* @__PURE__ */ new Map();
|
|
772
|
+
var byName = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const ex of EXTRACTORS) {
|
|
774
|
+
byName.set(ex.name, ex);
|
|
775
|
+
for (const ext of ex.extensions) byExtension.set(ext.toLowerCase(), ex);
|
|
776
|
+
}
|
|
777
|
+
function extractorForExtension(ext) {
|
|
778
|
+
const ex = byExtension.get(ext.toLowerCase());
|
|
779
|
+
return ex?.implemented ? ex : void 0;
|
|
780
|
+
}
|
|
781
|
+
function extractorByName(name) {
|
|
782
|
+
return byName.get(name.toLowerCase());
|
|
783
|
+
}
|
|
784
|
+
function supportedExtensions() {
|
|
785
|
+
return EXTRACTORS.filter((e) => e.implemented).flatMap((e) => e.extensions);
|
|
786
|
+
}
|
|
787
|
+
function listFormats() {
|
|
788
|
+
return EXTRACTORS.map((e) => ({
|
|
789
|
+
name: e.name,
|
|
790
|
+
extensions: e.extensions,
|
|
791
|
+
implemented: e.implemented
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/core/load-files.ts
|
|
796
|
+
import { stat } from "fs/promises";
|
|
797
|
+
import { extname, relative, resolve as resolve2 } from "path";
|
|
798
|
+
import fg from "fast-glob";
|
|
799
|
+
var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
|
|
800
|
+
var STDIN_TOKEN = "-";
|
|
801
|
+
function toPosix(p) {
|
|
802
|
+
return p.replace(/\\/g, "/");
|
|
803
|
+
}
|
|
804
|
+
async function statOrNull(p) {
|
|
805
|
+
try {
|
|
806
|
+
return await stat(p);
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async function resolveTargets(opts) {
|
|
812
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
813
|
+
const exts = (opts.exts ?? supportedExtensions()).map(
|
|
814
|
+
(e) => e.toLowerCase().startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
815
|
+
);
|
|
816
|
+
const ignore = [...DEFAULT_IGNORE, ...opts.exclude ?? []];
|
|
817
|
+
const out = /* @__PURE__ */ new Set();
|
|
818
|
+
const keepByExt = (file) => exts.includes(extname(file).toLowerCase());
|
|
819
|
+
for (const input of opts.inputs) {
|
|
820
|
+
if (input === STDIN_TOKEN) continue;
|
|
821
|
+
const abs = resolve2(cwd, input);
|
|
822
|
+
const st = await statOrNull(abs);
|
|
823
|
+
if (st?.isFile()) {
|
|
824
|
+
out.add(toPosix(relative(cwd, abs)));
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (st?.isDirectory()) {
|
|
828
|
+
const found2 = await fg(`${toPosix(input)}/**/*`, {
|
|
829
|
+
cwd,
|
|
830
|
+
ignore,
|
|
831
|
+
onlyFiles: true,
|
|
832
|
+
dot: false
|
|
833
|
+
});
|
|
834
|
+
for (const f of found2) if (keepByExt(f)) out.add(f);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const found = await fg(toPosix(input), {
|
|
838
|
+
cwd,
|
|
839
|
+
ignore,
|
|
840
|
+
onlyFiles: true,
|
|
841
|
+
dot: false
|
|
842
|
+
});
|
|
843
|
+
for (const f of found) if (keepByExt(f)) out.add(f);
|
|
844
|
+
}
|
|
845
|
+
return [...out].sort();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/commands/validate.ts
|
|
849
|
+
function parseErrorResult(file, format, message) {
|
|
850
|
+
const err = { schema: "(parse)", instancePath: "", message };
|
|
851
|
+
return { file, format, ok: false, schemas: [], errors: [err] };
|
|
852
|
+
}
|
|
853
|
+
async function runValidate(opts) {
|
|
854
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
855
|
+
const loaded = await loadConfig(opts.configPath, cwd);
|
|
856
|
+
const config = loaded?.config ?? null;
|
|
857
|
+
const inputs = opts.inputs.length > 0 ? opts.inputs : config?.paths ?? [];
|
|
858
|
+
const usingStdin = inputs.includes(STDIN_TOKEN);
|
|
859
|
+
if (inputs.length === 0) {
|
|
860
|
+
throw new DocmetaError(
|
|
861
|
+
"No files to validate. Pass paths/globs, or add `paths:` to docmeta.config.yaml."
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
|
|
865
|
+
if (opts.as && !forcedExtractor) {
|
|
866
|
+
throw new DocmetaError(
|
|
867
|
+
`Unknown format "${opts.as}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const exts = opts.exts ?? (forcedExtractor ? forcedExtractor.extensions : void 0);
|
|
871
|
+
const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
|
|
872
|
+
const files = await resolveTargets({
|
|
873
|
+
inputs: fileInputs,
|
|
874
|
+
exts,
|
|
875
|
+
exclude: [...config?.exclude ?? [], ...opts.exclude ?? []],
|
|
876
|
+
cwd
|
|
877
|
+
});
|
|
878
|
+
const validator = new Validator();
|
|
879
|
+
const results = [];
|
|
880
|
+
const processOne = async (label, content, extension) => {
|
|
881
|
+
const extractor = forcedExtractor ?? extractorForExtension(extension);
|
|
882
|
+
if (!extractor) {
|
|
883
|
+
throw new DocmetaError(
|
|
884
|
+
`Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
let extracted;
|
|
888
|
+
try {
|
|
889
|
+
extracted = extractor.extract(content, label);
|
|
890
|
+
} catch (err) {
|
|
891
|
+
if (err instanceof DocmetaError) throw err;
|
|
892
|
+
results.push(parseErrorResult(label, extractor.name, err.message));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
let schemaSet;
|
|
896
|
+
try {
|
|
897
|
+
schemaSet = resolveSchemaSet({
|
|
898
|
+
filePath: label,
|
|
899
|
+
fileSchema: extracted.data[FILE_SCHEMA_KEY],
|
|
900
|
+
cliSchemas: opts.cliSchemas,
|
|
901
|
+
config
|
|
902
|
+
});
|
|
903
|
+
} catch (err) {
|
|
904
|
+
results.push(parseErrorResult(label, extractor.name, err.message));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const errors = await validator.validate(
|
|
908
|
+
extracted.data,
|
|
909
|
+
schemaSet,
|
|
910
|
+
extracted.lineFor
|
|
911
|
+
);
|
|
912
|
+
results.push({
|
|
913
|
+
file: label,
|
|
914
|
+
format: extractor.name,
|
|
915
|
+
ok: errors.length === 0,
|
|
916
|
+
schemas: schemaSet,
|
|
917
|
+
errors
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
if (usingStdin) {
|
|
921
|
+
const content = opts.stdinContent ?? "";
|
|
922
|
+
if (!forcedExtractor) {
|
|
923
|
+
throw new DocmetaError(
|
|
924
|
+
"Reading from stdin (`-`) requires --as <format> to choose an extractor."
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
await processOne("<stdin>", content, forcedExtractor.extensions[0] ?? "");
|
|
928
|
+
}
|
|
929
|
+
for (const file of files) {
|
|
930
|
+
const content = await readFile3(resolve3(cwd, file), "utf8");
|
|
931
|
+
await processOne(file, content, extname2(file));
|
|
932
|
+
}
|
|
933
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
934
|
+
const summary = {
|
|
935
|
+
files: results.length,
|
|
936
|
+
passed: results.length - failed,
|
|
937
|
+
failed,
|
|
938
|
+
errors: results.reduce((n, r) => n + r.errors.length, 0)
|
|
939
|
+
};
|
|
940
|
+
return { results, summary };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/commands/get.ts
|
|
944
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
945
|
+
import { resolve as resolve4, extname as extname3 } from "path";
|
|
946
|
+
async function runGet(opts) {
|
|
947
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
948
|
+
if (opts.fields.length === 0) {
|
|
949
|
+
throw new DocmetaError("Specify at least one field to get.");
|
|
950
|
+
}
|
|
951
|
+
const loaded = await loadConfig(opts.configPath, cwd);
|
|
952
|
+
const config = loaded?.config ?? null;
|
|
953
|
+
const inputs = opts.inputs.length > 0 ? opts.inputs : config?.paths ?? [];
|
|
954
|
+
const usingStdin = inputs.includes(STDIN_TOKEN);
|
|
955
|
+
if (inputs.length === 0) {
|
|
956
|
+
throw new DocmetaError(
|
|
957
|
+
"No files to read. Pass paths/globs, or add `paths:` to docmeta.config.yaml."
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const forced = opts.as ? extractorByName(opts.as) : void 0;
|
|
961
|
+
if (opts.as && !forced) {
|
|
962
|
+
throw new DocmetaError(
|
|
963
|
+
`Unknown format "${opts.as}". Supported extensions: ${supportedExtensions().join(", ")}.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const exts = opts.exts ?? (forced ? forced.extensions : void 0);
|
|
967
|
+
const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
|
|
968
|
+
const files = await resolveTargets({
|
|
969
|
+
inputs: fileInputs,
|
|
970
|
+
exts,
|
|
971
|
+
exclude: [...config?.exclude ?? [], ...opts.exclude ?? []],
|
|
972
|
+
cwd
|
|
973
|
+
});
|
|
974
|
+
const out = [];
|
|
975
|
+
const readOne = (label, content, extension) => {
|
|
976
|
+
const extractor = forced ?? extractorForExtension(extension);
|
|
977
|
+
if (!extractor) {
|
|
978
|
+
throw new DocmetaError(
|
|
979
|
+
`Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
const extracted = extractor.extract(content, label);
|
|
983
|
+
const values = {};
|
|
984
|
+
for (const f of opts.fields) values[f] = extracted.data[f];
|
|
985
|
+
out.push({ file: label, present: extracted.present, values });
|
|
986
|
+
};
|
|
987
|
+
if (usingStdin) {
|
|
988
|
+
if (!forced) {
|
|
989
|
+
throw new DocmetaError(
|
|
990
|
+
"Reading from stdin (`-`) requires --as <format> to choose an extractor."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
readOne("<stdin>", opts.stdinContent ?? "", forced.extensions[0] ?? "");
|
|
994
|
+
}
|
|
995
|
+
for (const file of files) {
|
|
996
|
+
const content = await readFile4(resolve4(cwd, file), "utf8");
|
|
997
|
+
readOne(file, content, extname3(file));
|
|
998
|
+
}
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/schemas.ts
|
|
1003
|
+
function getSchemasInfo() {
|
|
1004
|
+
return { builtins: listBuiltins(), formats: listFormats() };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/reporters/color.ts
|
|
1008
|
+
import pc from "picocolors";
|
|
1009
|
+
function palette(enabled) {
|
|
1010
|
+
return pc.createColors(enabled);
|
|
1011
|
+
}
|
|
1012
|
+
function shouldColor(opts) {
|
|
1013
|
+
const env = opts.env ?? process.env;
|
|
1014
|
+
if (opts.noColor) return false;
|
|
1015
|
+
if (env.NO_COLOR != null && env.NO_COLOR !== "") return false;
|
|
1016
|
+
return Boolean(opts.isTTY);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/reporters/index.ts
|
|
1020
|
+
function fieldLabel(instancePath) {
|
|
1021
|
+
return instancePath === "" ? "(root)" : instancePath;
|
|
1022
|
+
}
|
|
1023
|
+
function renderPretty(results, summary, opts = {}) {
|
|
1024
|
+
const c = palette(opts.color ?? false);
|
|
1025
|
+
const lines = [];
|
|
1026
|
+
for (const r of results) {
|
|
1027
|
+
if (r.ok) {
|
|
1028
|
+
if (!opts.quiet) lines.push(`${c.green("\u2713")} ${r.file}`);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
lines.push(`${c.red("\u2717")} ${r.file}`);
|
|
1032
|
+
for (const e of r.errors) {
|
|
1033
|
+
const loc = e.line != null ? c.dim(` (line ${e.line})`) : "";
|
|
1034
|
+
lines.push(
|
|
1035
|
+
` ${c.cyan(fieldLabel(e.instancePath))} ${e.message}${loc} ${c.dim(
|
|
1036
|
+
`[${e.schema}]`
|
|
1037
|
+
)}`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const summaryText = `${summary.files} file${summary.files === 1 ? "" : "s"} checked, ${summary.passed} passed, ${summary.failed} failed, ${summary.errors} error${summary.errors === 1 ? "" : "s"}`;
|
|
1042
|
+
if (lines.length > 0) lines.push("");
|
|
1043
|
+
lines.push(summary.failed > 0 ? c.red(summaryText) : c.green(summaryText));
|
|
1044
|
+
return lines.join("\n");
|
|
1045
|
+
}
|
|
1046
|
+
function renderJson(results, summary) {
|
|
1047
|
+
return JSON.stringify({ summary, results }, null, 2);
|
|
1048
|
+
}
|
|
1049
|
+
function renderGithub(results) {
|
|
1050
|
+
const lines = [];
|
|
1051
|
+
for (const r of results) {
|
|
1052
|
+
for (const e of r.errors) {
|
|
1053
|
+
const params = [`file=${r.file}`];
|
|
1054
|
+
if (e.line != null) params.push(`line=${e.line}`);
|
|
1055
|
+
if (e.col != null) params.push(`col=${e.col}`);
|
|
1056
|
+
const msg = `[${e.schema}] ${fieldLabel(e.instancePath)} ${e.message}`;
|
|
1057
|
+
lines.push(`::error ${params.join(",")}::${msg}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return lines.join("\n");
|
|
1061
|
+
}
|
|
1062
|
+
function render(format, results, summary, opts = {}) {
|
|
1063
|
+
switch (format) {
|
|
1064
|
+
case "json":
|
|
1065
|
+
return renderJson(results, summary);
|
|
1066
|
+
case "github":
|
|
1067
|
+
return renderGithub(results);
|
|
1068
|
+
case "pretty":
|
|
1069
|
+
default:
|
|
1070
|
+
return renderPretty(results, summary, opts);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export {
|
|
1075
|
+
DocmetaError,
|
|
1076
|
+
parseConfig,
|
|
1077
|
+
loadConfig,
|
|
1078
|
+
DEFAULT_SCHEMA,
|
|
1079
|
+
resolveSchemaSet,
|
|
1080
|
+
listBuiltins,
|
|
1081
|
+
classifyRef,
|
|
1082
|
+
loadSchema,
|
|
1083
|
+
Validator,
|
|
1084
|
+
runValidate,
|
|
1085
|
+
runGet,
|
|
1086
|
+
getSchemasInfo,
|
|
1087
|
+
palette,
|
|
1088
|
+
shouldColor,
|
|
1089
|
+
render
|
|
1090
|
+
};
|
|
1091
|
+
//# sourceMappingURL=chunk-NTXC32C4.js.map
|