spec-up-t 1.6.1 → 1.6.3-beta.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.
- package/README.md +1 -0
- package/assets/compiled/body.js +2 -1
- package/assets/compiled/head.css +1 -0
- package/assets/css/validate-external-refs.css +369 -0
- package/assets/js/validate-external-refs.js +764 -0
- package/assets/js/validate-external-refs.test.js +577 -0
- package/config/asset-map.json +2 -0
- package/package.json +1 -1
- package/src/install-from-boilerplate/boilerplate/spec/terms-and-definitions-intro.md +4 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/composability.md +4 -2
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/dormancy.md +5 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/greenhouse.md +5 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/soil.md +4 -2
- package/src/install-from-boilerplate/boilerplate/specs.json +6 -18
- package/src/pipeline/references/external-references-service.js +12 -4
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Unit tests for validate-external-refs.js
|
|
6
|
+
* Tests the utility functions used for validating external references (xrefs and trefs).
|
|
7
|
+
*
|
|
8
|
+
* Note: The main validation flow involves network fetches and is tested via integration tests.
|
|
9
|
+
* These unit tests cover the synchronous utility functions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Mock the global allXTrefs before requiring the module
|
|
13
|
+
global.allXTrefs = {
|
|
14
|
+
xtrefs: [
|
|
15
|
+
{
|
|
16
|
+
externalSpec: 'TestSpec',
|
|
17
|
+
term: 'test-term',
|
|
18
|
+
ghPageUrl: 'https://example.github.io/test-spec/',
|
|
19
|
+
content: '<dd><p>This is a test definition.</p></dd>',
|
|
20
|
+
sourceFiles: [{ file: 'test.md', type: 'tref' }]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
externalSpec: 'TestSpec',
|
|
24
|
+
term: 'another-term',
|
|
25
|
+
ghPageUrl: 'https://example.github.io/test-spec/',
|
|
26
|
+
content: '<dd><p>Another definition here.</p></dd>',
|
|
27
|
+
sourceFiles: [{ file: 'test.md', type: 'xref' }]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Since the file auto-initializes, we need to mock DOM ready state
|
|
33
|
+
Object.defineProperty(document, 'readyState', {
|
|
34
|
+
get: () => 'complete'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Read and evaluate the source file to get the functions
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
// We'll test the functions by extracting their logic
|
|
42
|
+
// Since the module doesn't export, we test via DOM manipulation
|
|
43
|
+
|
|
44
|
+
describe('validate-external-refs', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
document.body.innerHTML = '';
|
|
47
|
+
// Clear any event listeners by resetting the body
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
document.body.innerHTML = '';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('normalizeContent helper logic', () => {
|
|
56
|
+
/**
|
|
57
|
+
* Tests normalization of HTML content for comparison
|
|
58
|
+
* This replicates the normalizeContent function logic
|
|
59
|
+
* Note: In production, markdown-it processing is included
|
|
60
|
+
*/
|
|
61
|
+
function normalizeContent(html) {
|
|
62
|
+
if (!html) return '';
|
|
63
|
+
// In tests, we skip markdown-it since it may not be available
|
|
64
|
+
const tempDiv = document.createElement('div');
|
|
65
|
+
tempDiv.innerHTML = html;
|
|
66
|
+
let text = tempDiv.textContent
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/\s+/g, ' ')
|
|
69
|
+
.trim();
|
|
70
|
+
text = text.replace(/[\u200B-\u200D\uFEFF]/g, '');
|
|
71
|
+
text = text.replace(/\s*([.,;:!?])\s*/g, '$1 ');
|
|
72
|
+
text = text.trim();
|
|
73
|
+
return text;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test: Does normalization handle empty input?
|
|
77
|
+
it('should return empty string for empty input', () => {
|
|
78
|
+
expect(normalizeContent('')).toBe('');
|
|
79
|
+
expect(normalizeContent(null)).toBe('');
|
|
80
|
+
expect(normalizeContent(undefined)).toBe('');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Test: Does normalization extract text from HTML?
|
|
84
|
+
it('should extract text content from HTML', () => {
|
|
85
|
+
const html = '<dd><p>This is a <strong>test</strong> definition.</p></dd>';
|
|
86
|
+
expect(normalizeContent(html)).toBe('this is a test definition.');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Test: Does normalization collapse whitespace?
|
|
90
|
+
it('should normalize whitespace', () => {
|
|
91
|
+
const html = '<p>Multiple spaces\nand\nnewlines</p>';
|
|
92
|
+
expect(normalizeContent(html)).toBe('multiple spaces and newlines');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Test: Does normalization convert to lowercase?
|
|
96
|
+
it('should convert to lowercase', () => {
|
|
97
|
+
const html = '<p>UPPERCASE and MixedCase</p>';
|
|
98
|
+
expect(normalizeContent(html)).toBe('uppercase and mixedcase');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Test: Does normalization handle punctuation spacing?
|
|
102
|
+
it('should normalize punctuation spacing', () => {
|
|
103
|
+
const html = '<p>Word,word . another</p>';
|
|
104
|
+
expect(normalizeContent(html)).toBe('word, word. another');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Test: Does normalization handle identical HTML with different formatting?
|
|
108
|
+
it('should normalize identically formatted content', () => {
|
|
109
|
+
const html1 = '<p>This is a test.</p>';
|
|
110
|
+
const html2 = '<p>This is a test.</p>';
|
|
111
|
+
expect(normalizeContent(html1)).toBe(normalizeContent(html2));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('extractTermsFromHtml helper logic', () => {
|
|
116
|
+
/**
|
|
117
|
+
* Tests term extraction from HTML content
|
|
118
|
+
* This replicates the extractTermsFromHtml function logic
|
|
119
|
+
*/
|
|
120
|
+
function extractTermsFromHtml(html) {
|
|
121
|
+
const terms = new Map();
|
|
122
|
+
const parser = new DOMParser();
|
|
123
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
124
|
+
const termElements = doc.querySelectorAll('dl.terms-and-definitions-list dt');
|
|
125
|
+
|
|
126
|
+
termElements.forEach(dt => {
|
|
127
|
+
const termSpan = dt.querySelector('[id^="term:"]');
|
|
128
|
+
if (!termSpan) return;
|
|
129
|
+
|
|
130
|
+
const termId = termSpan.id;
|
|
131
|
+
const termName = termId.split(':').pop();
|
|
132
|
+
|
|
133
|
+
// Collect ALL consecutive dd elements (not just the first one)
|
|
134
|
+
const ddElements = [];
|
|
135
|
+
let definitionContent = '';
|
|
136
|
+
let rawContent = '';
|
|
137
|
+
let sibling = dt.nextElementSibling;
|
|
138
|
+
|
|
139
|
+
// Collect all consecutive dd elements, skipping meta-info wrappers
|
|
140
|
+
while (sibling && sibling.tagName === 'DD') {
|
|
141
|
+
if (!sibling.classList.contains('meta-info-content-wrapper')) {
|
|
142
|
+
ddElements.push(sibling);
|
|
143
|
+
}
|
|
144
|
+
sibling = sibling.nextElementSibling;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (ddElements.length > 0) {
|
|
148
|
+
// Combine all dd elements' raw HTML
|
|
149
|
+
rawContent = ddElements.map(dd => dd.outerHTML).join('\n');
|
|
150
|
+
// Combine all dd elements' text content
|
|
151
|
+
definitionContent = ddElements.map(dd => dd.textContent).join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
terms.set(termName.toLowerCase(), {
|
|
155
|
+
content: definitionContent.trim(),
|
|
156
|
+
rawContent: rawContent,
|
|
157
|
+
termId: termId
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return terms;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Test: Does extraction handle empty HTML?
|
|
165
|
+
it('should return empty map for empty HTML', () => {
|
|
166
|
+
const terms = extractTermsFromHtml('');
|
|
167
|
+
expect(terms.size).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Test: Does extraction handle HTML without terms?
|
|
171
|
+
it('should return empty map for HTML without term definitions', () => {
|
|
172
|
+
const html = '<div><p>Just some content</p></div>';
|
|
173
|
+
const terms = extractTermsFromHtml(html);
|
|
174
|
+
expect(terms.size).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Test: Does extraction correctly parse term definitions?
|
|
178
|
+
it('should extract terms from valid term list HTML', () => {
|
|
179
|
+
const html = `
|
|
180
|
+
<dl class="terms-and-definitions-list">
|
|
181
|
+
<dt><span id="term:test-term">test-term</span></dt>
|
|
182
|
+
<dd><p>Definition of test term.</p></dd>
|
|
183
|
+
<dt><span id="term:another-term">another-term</span></dt>
|
|
184
|
+
<dd><p>Another definition.</p></dd>
|
|
185
|
+
</dl>
|
|
186
|
+
`;
|
|
187
|
+
const terms = extractTermsFromHtml(html);
|
|
188
|
+
expect(terms.size).toBe(2);
|
|
189
|
+
expect(terms.has('test-term')).toBe(true);
|
|
190
|
+
expect(terms.has('another-term')).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Test: Does extraction capture definition content?
|
|
194
|
+
it('should capture definition content correctly', () => {
|
|
195
|
+
const html = `
|
|
196
|
+
<dl class="terms-and-definitions-list">
|
|
197
|
+
<dt><span id="term:my-term">my-term</span></dt>
|
|
198
|
+
<dd><p>This is the definition.</p></dd>
|
|
199
|
+
</dl>
|
|
200
|
+
`;
|
|
201
|
+
const terms = extractTermsFromHtml(html);
|
|
202
|
+
const term = terms.get('my-term');
|
|
203
|
+
expect(term.content).toContain('This is the definition.');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Test: Does extraction skip meta-info wrappers?
|
|
207
|
+
it('should skip meta-info-content-wrapper elements', () => {
|
|
208
|
+
const html = `
|
|
209
|
+
<dl class="terms-and-definitions-list">
|
|
210
|
+
<dt><span id="term:meta-term">meta-term</span></dt>
|
|
211
|
+
<dd class="meta-info-content-wrapper">Meta info</dd>
|
|
212
|
+
<dd><p>Real definition.</p></dd>
|
|
213
|
+
</dl>
|
|
214
|
+
`;
|
|
215
|
+
const terms = extractTermsFromHtml(html);
|
|
216
|
+
const term = terms.get('meta-term');
|
|
217
|
+
expect(term.content).not.toContain('Meta info');
|
|
218
|
+
expect(term.content).toContain('Real definition.');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Test: Does extraction include all consecutive dd elements?
|
|
222
|
+
it('should extract all consecutive dd elements', () => {
|
|
223
|
+
const html = `
|
|
224
|
+
<dl class="terms-and-definitions-list">
|
|
225
|
+
<dt><span id="term:multi-dd">multi-dd</span></dt>
|
|
226
|
+
<dd><p>First part.</p></dd>
|
|
227
|
+
<dd><p>Second part.</p></dd>
|
|
228
|
+
</dl>
|
|
229
|
+
`;
|
|
230
|
+
const terms = extractTermsFromHtml(html);
|
|
231
|
+
const term = terms.get('multi-dd');
|
|
232
|
+
expect(term.content).toContain('First part.');
|
|
233
|
+
expect(term.content).toContain('Second part.'); // Should include all dd elements
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('createIndicator helper logic', () => {
|
|
238
|
+
/**
|
|
239
|
+
* Tests indicator creation logic
|
|
240
|
+
*/
|
|
241
|
+
function createIndicator(type, details = {}) {
|
|
242
|
+
const VALIDATOR_CONFIG = {
|
|
243
|
+
classes: {
|
|
244
|
+
indicator: 'external-ref-validation-indicator',
|
|
245
|
+
missing: 'external-ref-missing',
|
|
246
|
+
changed: 'external-ref-changed',
|
|
247
|
+
valid: 'external-ref-valid',
|
|
248
|
+
error: 'external-ref-error'
|
|
249
|
+
},
|
|
250
|
+
labels: {
|
|
251
|
+
missing: '⚠️ Term not found',
|
|
252
|
+
changed: '🔄 Definition changed',
|
|
253
|
+
error: '❌ Could not verify',
|
|
254
|
+
valid: '✓ Verified'
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const indicator = document.createElement('span');
|
|
259
|
+
indicator.classList.add(
|
|
260
|
+
VALIDATOR_CONFIG.classes.indicator,
|
|
261
|
+
VALIDATOR_CONFIG.classes[type]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const labelText = details.message || VALIDATOR_CONFIG.labels[type];
|
|
265
|
+
indicator.setAttribute('title', labelText);
|
|
266
|
+
|
|
267
|
+
const iconSpan = document.createElement('span');
|
|
268
|
+
iconSpan.classList.add('indicator-icon');
|
|
269
|
+
iconSpan.textContent = VALIDATOR_CONFIG.labels[type].split(' ')[0];
|
|
270
|
+
indicator.appendChild(iconSpan);
|
|
271
|
+
|
|
272
|
+
return indicator;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Test: Does indicator have correct classes for missing type?
|
|
276
|
+
it('should create indicator with correct classes for missing type', () => {
|
|
277
|
+
const indicator = createIndicator('missing');
|
|
278
|
+
expect(indicator.classList.contains('external-ref-validation-indicator')).toBe(true);
|
|
279
|
+
expect(indicator.classList.contains('external-ref-missing')).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Test: Does indicator have correct classes for changed type?
|
|
283
|
+
it('should create indicator with correct classes for changed type', () => {
|
|
284
|
+
const indicator = createIndicator('changed');
|
|
285
|
+
expect(indicator.classList.contains('external-ref-validation-indicator')).toBe(true);
|
|
286
|
+
expect(indicator.classList.contains('external-ref-changed')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Test: Does indicator have correct classes for error type?
|
|
290
|
+
it('should create indicator with correct classes for error type', () => {
|
|
291
|
+
const indicator = createIndicator('error');
|
|
292
|
+
expect(indicator.classList.contains('external-ref-validation-indicator')).toBe(true);
|
|
293
|
+
expect(indicator.classList.contains('external-ref-error')).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Test: Does indicator have correct classes for valid type?
|
|
297
|
+
it('should create indicator with correct classes for valid type', () => {
|
|
298
|
+
const indicator = createIndicator('valid');
|
|
299
|
+
expect(indicator.classList.contains('external-ref-validation-indicator')).toBe(true);
|
|
300
|
+
expect(indicator.classList.contains('external-ref-valid')).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Test: Does indicator have correct title attribute?
|
|
304
|
+
it('should set appropriate title for tooltip', () => {
|
|
305
|
+
const indicator = createIndicator('missing');
|
|
306
|
+
expect(indicator.getAttribute('title')).toBe('⚠️ Term not found');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Test: Does indicator contain icon element?
|
|
310
|
+
it('should contain an icon element with emoji', () => {
|
|
311
|
+
const indicator = createIndicator('changed');
|
|
312
|
+
const iconSpan = indicator.querySelector('.indicator-icon');
|
|
313
|
+
expect(iconSpan).not.toBeNull();
|
|
314
|
+
expect(iconSpan.textContent).toBe('🔄');
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('truncateText helper logic', () => {
|
|
319
|
+
function truncateText(text, maxLength) {
|
|
320
|
+
if (!text || text.length <= maxLength) return text || '';
|
|
321
|
+
return text.substring(0, maxLength) + '...';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Test: Does truncation handle empty input?
|
|
325
|
+
it('should return empty string for null or undefined', () => {
|
|
326
|
+
expect(truncateText(null, 10)).toBe('');
|
|
327
|
+
expect(truncateText(undefined, 10)).toBe('');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Test: Does truncation preserve short text?
|
|
331
|
+
it('should not truncate text shorter than max length', () => {
|
|
332
|
+
expect(truncateText('short', 10)).toBe('short');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Test: Does truncation work on long text?
|
|
336
|
+
it('should truncate text longer than max length', () => {
|
|
337
|
+
expect(truncateText('this is a long text', 10)).toBe('this is a ...');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Test: Does truncation handle exact length?
|
|
341
|
+
it('should not truncate text exactly at max length', () => {
|
|
342
|
+
expect(truncateText('exactly10!', 10)).toBe('exactly10!');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('similarity calculation logic', () => {
|
|
347
|
+
/**
|
|
348
|
+
* Tests similarity calculation between two strings
|
|
349
|
+
*/
|
|
350
|
+
function levenshteinDistance(str1, str2) {
|
|
351
|
+
const matrix = [];
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
354
|
+
matrix[i] = [i];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
358
|
+
matrix[0][j] = j;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
362
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
363
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
364
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
365
|
+
} else {
|
|
366
|
+
matrix[i][j] = Math.min(
|
|
367
|
+
matrix[i - 1][j - 1] + 1,
|
|
368
|
+
matrix[i][j - 1] + 1,
|
|
369
|
+
matrix[i - 1][j] + 1
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return matrix[str2.length][str1.length];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function calculateSimilarity(str1, str2) {
|
|
379
|
+
if (str1 === str2) return 1;
|
|
380
|
+
if (!str1 || !str2) return 0;
|
|
381
|
+
|
|
382
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
383
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
384
|
+
|
|
385
|
+
if (longer.length === 0) return 1;
|
|
386
|
+
|
|
387
|
+
const editDistance = levenshteinDistance(shorter, longer);
|
|
388
|
+
return (longer.length - editDistance) / longer.length;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Test: Do identical strings have 100% similarity?
|
|
392
|
+
it('should return 1 for identical strings', () => {
|
|
393
|
+
expect(calculateSimilarity('test', 'test')).toBe(1);
|
|
394
|
+
expect(calculateSimilarity('hello world', 'hello world')).toBe(1);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Test: Do completely different strings have low similarity?
|
|
398
|
+
it('should return low similarity for very different strings', () => {
|
|
399
|
+
const similarity = calculateSimilarity('abc', 'xyz');
|
|
400
|
+
expect(similarity).toBeLessThan(0.5);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Test: Does similarity work for slight variations?
|
|
404
|
+
it('should return high similarity for slight variations', () => {
|
|
405
|
+
const similarity = calculateSimilarity('hello world', 'hello world!');
|
|
406
|
+
expect(similarity).toBeGreaterThan(0.9);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Test: Does similarity handle empty strings?
|
|
410
|
+
it('should return 0 for empty strings', () => {
|
|
411
|
+
expect(calculateSimilarity('', '')).toBe(1); // Both empty is 100% similar
|
|
412
|
+
expect(calculateSimilarity('test', '')).toBe(0);
|
|
413
|
+
expect(calculateSimilarity('', 'test')).toBe(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Test: Does similarity threshold of 95% catch meaningful changes?
|
|
417
|
+
it('should detect significant changes below 95% threshold', () => {
|
|
418
|
+
const original = 'This is a long definition with many words about something important.';
|
|
419
|
+
const changed = 'This is completely different content about other things.';
|
|
420
|
+
const similarity = calculateSimilarity(original, changed);
|
|
421
|
+
expect(similarity).toBeLessThan(0.95); // Should trigger change indicator
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Test: Does similarity threshold of 95% ignore minor formatting?
|
|
425
|
+
it('should ignore minor formatting differences above 95% threshold', () => {
|
|
426
|
+
const original = 'This is a definition.';
|
|
427
|
+
const formatted = 'This is a definition. '; // Extra space
|
|
428
|
+
const similarity = calculateSimilarity(original.toLowerCase().trim(), formatted.toLowerCase().trim());
|
|
429
|
+
expect(similarity).toBeGreaterThanOrEqual(0.95); // Should NOT trigger change indicator
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe('truncateText helper logic', () => {
|
|
434
|
+
function truncateText(text, maxLength) {
|
|
435
|
+
if (!text || text.length <= maxLength) return text || '';
|
|
436
|
+
return text.substring(0, maxLength) + '...';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Test: Does truncation handle empty input?
|
|
440
|
+
it('should return empty string for null or undefined', () => {
|
|
441
|
+
expect(truncateText(null, 10)).toBe('');
|
|
442
|
+
expect(truncateText(undefined, 10)).toBe('');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Test: Does truncation preserve short text?
|
|
446
|
+
it('should not truncate text shorter than max length', () => {
|
|
447
|
+
expect(truncateText('short', 10)).toBe('short');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Test: Does truncation work on long text?
|
|
451
|
+
it('should truncate text longer than max length', () => {
|
|
452
|
+
expect(truncateText('this is a long text', 10)).toBe('this is a ...');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Test: Does truncation handle exact length?
|
|
456
|
+
it('should not truncate text exactly at max length', () => {
|
|
457
|
+
expect(truncateText('exactly10!', 10)).toBe('exactly10!');
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe('escapeHtml helper logic', () => {
|
|
462
|
+
function escapeHtml(text) {
|
|
463
|
+
const div = document.createElement('div');
|
|
464
|
+
div.textContent = text;
|
|
465
|
+
return div.innerHTML;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Test: Does escaping handle normal text?
|
|
469
|
+
it('should return text unchanged for normal input', () => {
|
|
470
|
+
expect(escapeHtml('normal text')).toBe('normal text');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Test: Does escaping convert HTML entities?
|
|
474
|
+
it('should escape HTML special characters', () => {
|
|
475
|
+
expect(escapeHtml('<script>alert("xss")</script>')).toBe(
|
|
476
|
+
'<script>alert("xss")</script>'
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Test: Does escaping handle ampersands?
|
|
481
|
+
it('should escape ampersands', () => {
|
|
482
|
+
expect(escapeHtml('one & two')).toBe('one & two');
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('DOM element finding logic', () => {
|
|
487
|
+
beforeEach(() => {
|
|
488
|
+
document.body.innerHTML = `
|
|
489
|
+
<a class="x-term-reference"
|
|
490
|
+
data-local-href="#term:TestSpec:test-term"
|
|
491
|
+
href="https://example.github.io/test-spec/#term:test-term">
|
|
492
|
+
test-term
|
|
493
|
+
</a>
|
|
494
|
+
<dl class="terms-and-definitions-list">
|
|
495
|
+
<dt class="term-external">
|
|
496
|
+
<span class="term-external" data-original-term="tref-term">
|
|
497
|
+
tref-term
|
|
498
|
+
</span>
|
|
499
|
+
</dt>
|
|
500
|
+
</dl>
|
|
501
|
+
`;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Test: Can xref elements be found in the DOM?
|
|
505
|
+
it('should find xref elements by class', () => {
|
|
506
|
+
const xrefs = document.querySelectorAll('a.x-term-reference');
|
|
507
|
+
expect(xrefs.length).toBe(1);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Test: Can tref elements be found in the DOM?
|
|
511
|
+
it('should find tref elements by class', () => {
|
|
512
|
+
const trefs = document.querySelectorAll('dt.term-external');
|
|
513
|
+
expect(trefs.length).toBe(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Test: Can data-local-href be parsed for spec and term?
|
|
517
|
+
it('should parse data-local-href for spec and term names', () => {
|
|
518
|
+
const xref = document.querySelector('a.x-term-reference');
|
|
519
|
+
const localHref = xref.getAttribute('data-local-href');
|
|
520
|
+
const match = localHref.match(/#term:([^:]+):(.+)/);
|
|
521
|
+
expect(match).not.toBeNull();
|
|
522
|
+
expect(match[1]).toBe('TestSpec');
|
|
523
|
+
expect(match[2]).toBe('test-term');
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Test: Can original term be retrieved from tref data attribute?
|
|
527
|
+
it('should retrieve original term from data-original-term', () => {
|
|
528
|
+
const termSpan = document.querySelector('[data-original-term]');
|
|
529
|
+
expect(termSpan.dataset.originalTerm).toBe('tref-term');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('indicator insertion logic', () => {
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
document.body.innerHTML = `
|
|
536
|
+
<a class="x-term-reference" id="test-xref">test</a>
|
|
537
|
+
<dt class="term-external" id="test-tref">
|
|
538
|
+
<span class="term-external">term</span>
|
|
539
|
+
</dt>
|
|
540
|
+
`;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Test: Can indicators be inserted after xref elements?
|
|
544
|
+
it('should insert indicator after xref element', () => {
|
|
545
|
+
const xref = document.getElementById('test-xref');
|
|
546
|
+
const indicator = document.createElement('span');
|
|
547
|
+
indicator.classList.add('external-ref-validation-indicator');
|
|
548
|
+
xref.insertAdjacentElement('afterend', indicator);
|
|
549
|
+
|
|
550
|
+
expect(xref.nextElementSibling).toBe(indicator);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Test: Can indicators be inserted into tref elements?
|
|
554
|
+
it('should insert indicator into tref span element', () => {
|
|
555
|
+
const termSpan = document.querySelector('dt.term-external span.term-external');
|
|
556
|
+
const indicator = document.createElement('span');
|
|
557
|
+
indicator.classList.add('external-ref-validation-indicator');
|
|
558
|
+
termSpan.appendChild(indicator);
|
|
559
|
+
|
|
560
|
+
expect(termSpan.querySelector('.external-ref-validation-indicator')).toBe(indicator);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Test: Should prevent duplicate indicators
|
|
564
|
+
it('should not add duplicate indicators', () => {
|
|
565
|
+
const xref = document.getElementById('test-xref');
|
|
566
|
+
|
|
567
|
+
// Add first indicator
|
|
568
|
+
const indicator1 = document.createElement('span');
|
|
569
|
+
indicator1.classList.add('external-ref-validation-indicator');
|
|
570
|
+
xref.insertAdjacentElement('afterend', indicator1);
|
|
571
|
+
|
|
572
|
+
// Check for existing before adding second
|
|
573
|
+
const existing = xref.nextElementSibling?.classList.contains('external-ref-validation-indicator');
|
|
574
|
+
expect(existing).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
});
|
package/config/asset-map.json
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"assets/css/horizontal-scroll-hint.css",
|
|
30
30
|
"assets/css/highlight-heading-plus-sibling-nodes.css",
|
|
31
31
|
"assets/css/counter.css",
|
|
32
|
+
"assets/css/validate-external-refs.css",
|
|
32
33
|
|
|
33
34
|
"assets/css/index.css"
|
|
34
35
|
],
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
"assets/js/add-bootstrap-classes-to-images.js",
|
|
85
86
|
"assets/js/image-full-size.js",
|
|
86
87
|
"assets/js/highlight-heading-plus-sibling-nodes.js",
|
|
88
|
+
"assets/js/validate-external-refs.js",
|
|
87
89
|
"assets/js/embedded-libraries/bootstrap.bundle.min.js"
|
|
88
90
|
]
|
|
89
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-up-t",
|
|
3
|
-
"version": "1.6.1",
|
|
3
|
+
"version": "1.6.3-beta.1",
|
|
4
4
|
"description": "Technical specification drafting tool that generates rich specification documents from markdown. Forked from https://github.com/decentralized-identity/spec-up by Daniel Buchner (https://github.com/csuwildcat)",
|
|
5
5
|
"main": "./index",
|
|
6
6
|
"repository": {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
[[tref:
|
|
1
|
+
[[tref: ExtRef1, greenhouse]]
|
|
2
2
|
|
|
3
|
-
~ Note:
|
|
3
|
+
~ Note: This is a tref example. The term "greenhouse" is imported from the ExtRef1 external glossary (focused on greenhouse and irrigation concepts).
|
|
4
|
+
|
|
5
|
+
~ See also: [[xref: ExtRef2, propagation]] for plant propagation from ExtRef2.
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
~ Refs examples: [[ref: Compost]], [[ref: Mulch]], [[ref: Fertilizer]].
|
|
6
6
|
|
|
7
|
-
~ Xref
|
|
7
|
+
~ Xref examples from external gardening glossaries:
|
|
8
|
+
~ - From ExtRef1 (greenhouse focus): [[xref: ExtRef1, greenhouse]], [[xref: ExtRef1, irrigation]]
|
|
9
|
+
~ - From ExtRef2 (lifecycle focus): [[xref: ExtRef2, dormancy]], [[xref: ExtRef2, propagation]]
|
|
8
10
|
|
|
9
|
-
~
|
|
11
|
+
~ Note: The term "soil" exists in ExtRef1 and ExtRef2 with different definitions focused on their respective themes.
|
|
10
12
|
|
|
11
13
|
~ Soil quality affects water retention, nutrient availability, and plant health. Amending soil with compost or mulch can improve its structure and fertility. Fertilizer may be added to supplement nutrients as needed.
|