@mandolin/jsdoc-plugin-hia-sys 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.
@@ -0,0 +1,416 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { normalizeOpenMode } = require("./source.cjs");
6
+ const { VERSION } = require("../version.cjs");
7
+
8
+ const CONTRACT_VERSION = "0.1.0";
9
+ const STANDALONE_CONTRACT = "jsdoc-plugin-hia-sys/standalone";
10
+ const INTEGRATION_CONTRACT = "hia-jsdoc-integration";
11
+
12
+ function getMetadata(doclet, state) {
13
+ const key = state.config.metadataKey || "hia";
14
+ return doclet && doclet[key] && typeof doclet[key] === "object"
15
+ ? doclet[key]
16
+ : {};
17
+ }
18
+
19
+ function getDocletId(doclet) {
20
+ return doclet.longname || doclet.name || "";
21
+ }
22
+
23
+ function getNodeId(doclet) {
24
+ const rawId = getDocletId(doclet) || "anonymous";
25
+ const kind = doclet.kind || "unknown";
26
+ return `jsdoc:${kind}:${rawId}`;
27
+ }
28
+
29
+ function toArray(value) {
30
+ return Array.isArray(value) ? value : [];
31
+ }
32
+
33
+ function countI18nFieldContent(fields) {
34
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
35
+ return 0;
36
+ }
37
+
38
+ let count = 0;
39
+
40
+ for (const field of Object.values(fields)) {
41
+ if (!field || typeof field !== "object" || Array.isArray(field)) {
42
+ continue;
43
+ }
44
+
45
+ const localizedText = field.localizedText && typeof field.localizedText === "object"
46
+ ? field.localizedText
47
+ : {};
48
+
49
+ if (
50
+ field.defaultText ||
51
+ Object.values(localizedText).some(Boolean) ||
52
+ toArray(field.blocks).length > 0 ||
53
+ toArray(field.segments).length > 0
54
+ ) {
55
+ count += 1;
56
+ }
57
+ }
58
+
59
+ return count;
60
+ }
61
+
62
+ function scoreIntegrationNode(node) {
63
+ let score = 0;
64
+
65
+ if (node.summary) {
66
+ score += 100;
67
+ }
68
+
69
+ if (node.i18n && node.i18n.key) {
70
+ score += 80;
71
+ }
72
+
73
+ score += countI18nFieldContent(node.i18n && node.i18n.fields) * 20;
74
+ score += toArray(node.jsdoc && node.jsdoc.params).length * 10;
75
+ score += toArray(node.jsdoc && node.jsdoc.returns).length * 10;
76
+ score += toArray(node.jsdoc && node.jsdoc.properties).length * 8;
77
+ score += toArray(node.jsdoc && node.jsdoc.examples).length * 8;
78
+ score += toArray(node.source && node.source.references).length * 12;
79
+ score += toArray(node.diagnostics).length;
80
+
81
+ return score;
82
+ }
83
+
84
+ function hasIntegrationNodeContent(node) {
85
+ if (!node || !node.id || !node.name) {
86
+ return false;
87
+ }
88
+
89
+ if (node.kind === "package" && /undefined/.test(String(node.longname || node.name || ""))) {
90
+ return false;
91
+ }
92
+
93
+ return scoreIntegrationNode(node) > 0;
94
+ }
95
+
96
+ function buildIntegrationEntries(doclets, state) {
97
+ const result = [];
98
+ const indexesById = new Map();
99
+
100
+ for (const doclet of doclets) {
101
+ const node = toIntegrationNode(doclet, state);
102
+
103
+ if (!hasIntegrationNodeContent(node)) {
104
+ continue;
105
+ }
106
+
107
+ const existingIndex = indexesById.get(node.id);
108
+
109
+ if (existingIndex === undefined) {
110
+ indexesById.set(node.id, result.length);
111
+ result.push({
112
+ doclet,
113
+ node
114
+ });
115
+ continue;
116
+ }
117
+
118
+ if (scoreIntegrationNode(node) > scoreIntegrationNode(result[existingIndex].node)) {
119
+ result[existingIndex] = {
120
+ doclet,
121
+ node
122
+ };
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ function isUnsafePathLike(value) {
130
+ const text = String(value || "");
131
+
132
+ if (/^[A-Za-z]:[\\/]/.test(text) || /^\\\\/.test(text)) {
133
+ return true;
134
+ }
135
+
136
+ if (/^\/(?!\/)/.test(text) && !/^\/\//.test(text)) {
137
+ return true;
138
+ }
139
+
140
+ return text.split(/[\\/]/).includes("..");
141
+ }
142
+
143
+ function sanitizeIntegrationValue(value, key = "") {
144
+ if (Array.isArray(value)) {
145
+ return value
146
+ .map((item) => sanitizeIntegrationValue(item))
147
+ .filter((item) => item !== undefined);
148
+ }
149
+
150
+ if (value && typeof value === "object") {
151
+ const record = {};
152
+
153
+ for (const [itemKey, itemValue] of Object.entries(value)) {
154
+ if (/filePath$/i.test(itemKey)) {
155
+ continue;
156
+ }
157
+
158
+ const sanitized = sanitizeIntegrationValue(itemValue, itemKey);
159
+
160
+ if (sanitized !== undefined) {
161
+ record[itemKey] = sanitized;
162
+ }
163
+ }
164
+
165
+ return record;
166
+ }
167
+
168
+ if (key === "openMode") {
169
+ return normalizeOpenMode(value);
170
+ }
171
+
172
+ if (
173
+ typeof value === "string" &&
174
+ /(?:path|file|root|output)$/i.test(key) &&
175
+ isUnsafePathLike(value)
176
+ ) {
177
+ return undefined;
178
+ }
179
+
180
+ return value;
181
+ }
182
+
183
+ function normalizeDiagnostic(diagnostic, targetPath) {
184
+ const sanitized = sanitizeIntegrationValue(diagnostic || {});
185
+
186
+ return {
187
+ code: sanitized.code || "HIA0000",
188
+ severity: sanitized.severity === "error" || sanitized.severity === "warning" || sanitized.severity === "info"
189
+ ? sanitized.severity
190
+ : "warning",
191
+ message: sanitized.message || "Unknown HIA diagnostic.",
192
+ targetPath: sanitized.targetPath || sanitized.path || targetPath,
193
+ path: sanitized.path || sanitized.targetPath || targetPath,
194
+ plugin: sanitized.plugin || "core",
195
+ range: sanitized.range || null,
196
+ data: sanitized.data && typeof sanitized.data === "object" && !Array.isArray(sanitized.data)
197
+ ? sanitized.data
198
+ : null
199
+ };
200
+ }
201
+
202
+ function normalizeDiagnostics(diagnostics, targetPath) {
203
+ return toArray(diagnostics).map((diagnostic, index) => (
204
+ normalizeDiagnostic(diagnostic, `${targetPath}.${index}`)
205
+ ));
206
+ }
207
+
208
+ function summarizeDoclet(doclet, state) {
209
+ const metadata = getMetadata(doclet, state);
210
+ const source = metadata.source || {};
211
+ const i18n = metadata.i18n || {};
212
+
213
+ return {
214
+ id: getDocletId(doclet),
215
+ name: doclet.name || "",
216
+ longname: doclet.longname || doclet.name || "",
217
+ kind: doclet.kind || "",
218
+ memberof: doclet.memberof || "",
219
+ scope: doclet.scope || "",
220
+ hiaKey: i18n.key || "",
221
+ hiaPath: i18n.path || "",
222
+ sourceReferenceCount: Array.isArray(source.references) ? source.references.length : 0,
223
+ locales: Array.isArray(i18n.locales) ? i18n.locales : [],
224
+ hasHia: Boolean(metadata.version)
225
+ };
226
+ }
227
+
228
+ function toIntegrationNode(doclet, state) {
229
+ const metadata = getMetadata(doclet, state);
230
+
231
+ const node = {
232
+ id: getNodeId(doclet),
233
+ kind: doclet.kind || "",
234
+ name: doclet.name || "",
235
+ longname: doclet.longname || doclet.name || "",
236
+ title: doclet.longname || doclet.name || "",
237
+ summary: doclet.description || doclet.classdesc || "",
238
+ jsdoc: {
239
+ docletId: getDocletId(doclet),
240
+ kind: doclet.kind || "",
241
+ name: doclet.name || "",
242
+ longname: doclet.longname || doclet.name || "",
243
+ memberof: doclet.memberof || "",
244
+ scope: doclet.scope || "",
245
+ params: doclet.params || [],
246
+ returns: doclet.returns || [],
247
+ properties: doclet.properties || [],
248
+ examples: doclet.examples || []
249
+ },
250
+ hia: {
251
+ metadataKey: state.config.metadataKey || "hia",
252
+ microPlugins: metadata.microPlugins || []
253
+ },
254
+ source: metadata.source || {
255
+ fragments: [],
256
+ references: []
257
+ },
258
+ i18n: metadata.i18n || {},
259
+ diagnostics: normalizeDiagnostics(metadata.diagnostics, `ir.nodes.${getNodeId(doclet)}.diagnostics`)
260
+ };
261
+
262
+ return sanitizeIntegrationValue(node);
263
+ }
264
+
265
+ function buildDocletNodeMap(doclets) {
266
+ return doclets.map((doclet) => ({
267
+ docletId: getDocletId(doclet),
268
+ nodeId: getNodeId(doclet),
269
+ kind: doclet.kind || "",
270
+ name: doclet.name || "",
271
+ longname: doclet.longname || doclet.name || ""
272
+ }));
273
+ }
274
+
275
+ function getDiagnostics(state) {
276
+ if (Array.isArray(state.output.diagnostics)) {
277
+ return state.output.diagnostics;
278
+ }
279
+
280
+ return state.diagnostics.list();
281
+ }
282
+
283
+ function buildThemeContract(state, doclets) {
284
+ return {
285
+ name: "jsdoc-theme-hia",
286
+ contract: STANDALONE_CONTRACT,
287
+ version: CONTRACT_VERSION,
288
+ metadataKey: state.config.metadataKey || "hia",
289
+ consumes: {
290
+ docletMetadata: "doclet.hia",
291
+ sourceReferences: "doclet.hia.source.references",
292
+ sourceFragments: "doclet.hia.source.fragments",
293
+ i18n: "doclet.hia.i18n",
294
+ diagnostics: "doclet.hia.diagnostics"
295
+ },
296
+ i18n: {
297
+ defaultLocale: state.config.i18n.defaultLocale,
298
+ fallbackLocale: state.config.i18n.fallbackLocale,
299
+ locales: state.config.i18n.locales || [],
300
+ mode: state.config.i18n.mode
301
+ },
302
+ doclets: doclets.map((doclet) => summarizeDoclet(doclet, state))
303
+ };
304
+ }
305
+
306
+ function buildStandaloneOutput(state, doclets) {
307
+ return {
308
+ contract: STANDALONE_CONTRACT,
309
+ contractVersion: CONTRACT_VERSION,
310
+ pluginVersion: VERSION,
311
+ mode: state.config.mode,
312
+ metadataKey: state.config.metadataKey || "hia",
313
+ theme: buildThemeContract(state, doclets),
314
+ doclets: doclets.map((doclet) => ({
315
+ summary: summarizeDoclet(doclet, state),
316
+ hia: getMetadata(doclet, state)
317
+ })),
318
+ sourceFragments: state.output.sourceFragments || [],
319
+ localizationResources: state.registries.localizationResources || [],
320
+ diagnostics: getDiagnostics(state),
321
+ diagnosticCounts: state.output.diagnosticCounts || state.diagnostics.countBySeverity()
322
+ };
323
+ }
324
+
325
+ function buildIntegrationOutput(state, doclets) {
326
+ const integrationEntries = buildIntegrationEntries(doclets, state);
327
+ const nodes = integrationEntries.map((entry) => entry.node);
328
+ const integrationDoclets = integrationEntries.map((entry) => entry.doclet);
329
+
330
+ return sanitizeIntegrationValue({
331
+ contract: INTEGRATION_CONTRACT,
332
+ contractVersion: CONTRACT_VERSION,
333
+ pluginVersion: VERSION,
334
+ mode: state.config.mode === "hiaIntegration" ? "hiaIntegration" : "standalone",
335
+ artifactKind: "hia-integration",
336
+ parserBoundary: {
337
+ adapter: "parser-jsdoc",
338
+ owns: [
339
+ "JSDoc doclet normalization",
340
+ "JSDoc tag to HIA metadata mapping",
341
+ "source fragment reference extraction",
342
+ "doc-i18n metadata extraction"
343
+ ],
344
+ handsOff: [
345
+ "language-neutral HIA IR evolution",
346
+ "non-JavaScript parser implementations",
347
+ "IDE protocol and LSP transport"
348
+ ]
349
+ },
350
+ ir: {
351
+ version: VERSION,
352
+ source: "jsdoc",
353
+ nodes
354
+ },
355
+ sourceFragments: state.output.sourceFragments || [],
356
+ localizationResources: state.registries.localizationResources || [],
357
+ diagnostics: normalizeDiagnostics(getDiagnostics(state), "diagnostics"),
358
+ diagnosticCounts: state.output.diagnosticCounts || state.diagnostics.countBySeverity(),
359
+ docletNodeMap: buildDocletNodeMap(integrationDoclets)
360
+ });
361
+ }
362
+
363
+ function buildOutputContract(state, doclets) {
364
+ const sourceDoclets = Array.isArray(doclets) ? doclets : [];
365
+ const standalone = buildStandaloneOutput(state, sourceDoclets);
366
+ const integration = buildIntegrationOutput(state, sourceDoclets);
367
+
368
+ return {
369
+ standalone,
370
+ integration
371
+ };
372
+ }
373
+
374
+ function resolveIntegrationOutputFile(state) {
375
+ const outputFile = state.config.integration && state.config.integration.outputFile;
376
+
377
+ if (!outputFile) {
378
+ return "";
379
+ }
380
+
381
+ if (path.isAbsolute(outputFile)) {
382
+ return outputFile;
383
+ }
384
+
385
+ return path.resolve(process.cwd(), outputFile);
386
+ }
387
+
388
+ function writeIntegrationOutput(state, integrationOutput) {
389
+ if (!state.config.integration || !state.config.integration.enabled) {
390
+ return "";
391
+ }
392
+
393
+ const outputFile = resolveIntegrationOutputFile(state);
394
+
395
+ if (!outputFile) {
396
+ return "";
397
+ }
398
+
399
+ fs.mkdirSync(path.dirname(outputFile), {
400
+ recursive: true
401
+ });
402
+ fs.writeFileSync(outputFile, `${JSON.stringify(integrationOutput, null, 2)}\n`, "utf8");
403
+
404
+ return outputFile;
405
+ }
406
+
407
+ module.exports = {
408
+ CONTRACT_VERSION,
409
+ INTEGRATION_CONTRACT,
410
+ STANDALONE_CONTRACT,
411
+ buildIntegrationOutput,
412
+ buildOutputContract,
413
+ buildStandaloneOutput,
414
+ buildThemeContract,
415
+ writeIntegrationOutput
416
+ };