glossarist 0.3.8 → 0.4.1

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.
@@ -1,4 +1,6 @@
1
1
  import { ValidationRule } from './validation-rule.js';
2
+ import { parseMention } from '../reference-mention.js';
3
+ import { GraphicalSymbol } from '../models/designation.js';
2
4
 
3
5
  const _eachLocalization = (concept, fn) => {
4
6
  for (const lang of concept.languages) {
@@ -160,36 +162,14 @@ const CITE_MENTION_RE = /\{\{\s*cite:([^,}\s]+)[^}]*?\}\}/g;
160
162
 
161
163
  function _findCiteMentions(concept) {
162
164
  const mentions = [];
163
- const walkText = (text, source) => {
164
- if (typeof text !== 'string' || text.length === 0) return;
165
+ for (const { text, source } of concept.walkTexts()) {
166
+ if (typeof text !== 'string' || text.length === 0) continue;
165
167
  CITE_MENTION_RE.lastIndex = 0;
166
168
  let m;
167
169
  while ((m = CITE_MENTION_RE.exec(text)) !== null) {
168
170
  mentions.push({ key: m[1].trim(), source });
169
171
  }
170
- };
171
-
172
- for (const lang of concept.languages) {
173
- const lc = concept.localization(lang);
174
- if (!lc) continue;
175
-
176
- for (let i = 0; lc.definitions[i]; i++) {
177
- walkText(lc.definitions[i]?.content, `localizations.${lang}.definitions[${i}].content`);
178
- }
179
- for (let i = 0; lc.notes[i]; i++) {
180
- const content = typeof lc.notes[i] === 'object'
181
- ? (lc.notes[i]?.content ?? '')
182
- : String(lc.notes[i] ?? '');
183
- walkText(content, `localizations.${lang}.notes[${i}].content`);
184
- }
185
- for (let i = 0; lc.examples[i]; i++) {
186
- walkText(lc.examples[i]?.content, `localizations.${lang}.examples[${i}].content`);
187
- }
188
- for (let i = 0; lc.annotations[i]; i++) {
189
- walkText(lc.annotations[i]?.content, `localizations.${lang}.annotations[${i}].content`);
190
- }
191
172
  }
192
-
193
173
  return mentions;
194
174
  }
195
175
 
@@ -267,3 +247,131 @@ export class CiteRefIntegrityRule extends ValidationRule {
267
247
  }
268
248
  }
269
249
  }
250
+
251
+
252
+ // ── NonVerbalRefIntegrityRule ────────────────────────────────────────
253
+ // Uses parseMention for classification (no regex duplication).
254
+
255
+
256
+ const NVR_ARRAYS = Object.freeze([
257
+ { name: 'figures', entityType: 'figure' },
258
+ { name: 'tables', entityType: 'table' },
259
+ { name: 'formulas', entityType: 'formula' },
260
+ ]);
261
+
262
+ function _findNvrMentions(concept) {
263
+ const mentions = [];
264
+ const walkText = (text, source) => {
265
+ if (typeof text !== 'string' || text.length === 0) return;
266
+ const re = /\{\{([^{}]*?)\}\}/g;
267
+ let m;
268
+ while ((m = re.exec(text)) !== null) {
269
+ const parsed = parseMention(m[1]);
270
+ if (parsed.kind === 'fig-ref' ||
271
+ parsed.kind === 'table-ref' ||
272
+ parsed.kind === 'formula-ref') {
273
+ mentions.push({ key: parsed.key, source });
274
+ }
275
+ }
276
+ };
277
+
278
+ for (const { text, source } of concept.walkTexts()) {
279
+ walkText(text, source);
280
+ }
281
+ return mentions;
282
+ }
283
+
284
+ export class NonVerbalRefIntegrityRule extends ValidationRule {
285
+ constructor() {
286
+ super('nvr-integrity', 'warning');
287
+ }
288
+
289
+ validate(concept, path, result) {
290
+ for (const { name } of NVR_ARRAYS) {
291
+ const counts = new Map();
292
+ for (const ref of concept[name]) {
293
+ if (ref.entityId == null) continue;
294
+ counts.set(ref.entityId, (counts.get(ref.entityId) ?? 0) + 1);
295
+ }
296
+ for (const [id, count] of counts) {
297
+ if (count > 1) {
298
+ this.addIssue(result, `${path}${name}`,
299
+ `duplicate ${name} reference id "${id}" appears ${count} times`);
300
+ }
301
+ }
302
+ }
303
+
304
+ const mentions = _findNvrMentions(concept);
305
+ if (mentions.length === 0) return;
306
+
307
+ const knownIds = new Set();
308
+ for (const { name } of NVR_ARRAYS) {
309
+ for (const ref of concept[name]) {
310
+ if (ref.entityId != null) knownIds.add(ref.entityId);
311
+ }
312
+ }
313
+
314
+ for (const { key, source } of mentions) {
315
+ if (!knownIds.has(key)) {
316
+ this.addIssue(result, source,
317
+ `inline NVR mention "${key}" does not resolve to any figures/tables/formulas entry`);
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ // ── OrphanedImagesRule ───────────────────────────────────────────────
324
+ // Collection-scope rule: needs AssetIndex + all concepts. Called
325
+ // directly by GcrValidator (not in concept validator chain).
326
+
327
+ export class OrphanedImagesRule {
328
+ constructor() {
329
+ this.name = 'orphaned-images';
330
+ this.severity = 'warning';
331
+ }
332
+
333
+ check(context) {
334
+ const { assetIndex, concepts, resolver } = context;
335
+ if (!assetIndex || assetIndex.size === 0) return [];
336
+
337
+ const referenced = new Set();
338
+
339
+ for (const concept of concepts) {
340
+ if (resolver) {
341
+ for (const ref of resolver.extractReferences(concept)) {
342
+ if (ref.target && ref.target.includes('images/')) {
343
+ referenced.add(ref.target.replace(/^\//, ''));
344
+ }
345
+ }
346
+ }
347
+
348
+ for (const lang of concept.languages) {
349
+ const lc = concept.localization(lang);
350
+ if (!lc) continue;
351
+
352
+ for (const nvr of lc.nonVerbalRep) {
353
+ for (const img of nvr.images) {
354
+ if (img.src) referenced.add(img.src.replace(/^\//, ''));
355
+ }
356
+ }
357
+ for (const term of lc.terms) {
358
+ if (term instanceof GraphicalSymbol && term.image) {
359
+ referenced.add(term.image.replace(/^\//, ''));
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ const issues = [];
366
+ for (const imgPath of assetIndex.paths) {
367
+ if (!referenced.has(imgPath)) {
368
+ issues.push({
369
+ path: imgPath,
370
+ severity: 'warning',
371
+ message: `orphaned image: ${imgPath} (not referenced by any concept)`,
372
+ });
373
+ }
374
+ }
375
+ return issues;
376
+ }
377
+ }