spec-up-t 1.3.1 → 1.4.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.
Files changed (130) hide show
  1. package/.github/copilot-instructions.md +13 -0
  2. package/assets/compiled/body.js +17 -11
  3. package/assets/compiled/head.css +6 -4
  4. package/assets/css/collapse-definitions.css +0 -1
  5. package/assets/css/create-pdf.css +4 -2
  6. package/assets/css/create-term-filter.css +4 -4
  7. package/assets/css/definition-buttons-container.css +60 -0
  8. package/assets/css/insert-trefs.css +7 -0
  9. package/assets/css/sidebar-toc.css +2 -1
  10. package/assets/css/terms-and-definitions.css +73 -22
  11. package/assets/js/add-href-to-snapshot-link.js +16 -9
  12. package/assets/js/addAnchorsToTerms.js +1 -1
  13. package/assets/js/charts.js +10 -0
  14. package/assets/js/collapse-definitions.js +13 -2
  15. package/assets/js/collapse-meta-info.js +11 -9
  16. package/assets/js/definition-button-container-utils.js +82 -0
  17. package/assets/js/edit-term-buttons.js +77 -20
  18. package/assets/js/github-issues.js +35 -0
  19. package/assets/js/github-repo-info.js +144 -0
  20. package/assets/js/highlight-heading-plus-sibling-nodes.test.js +18 -0
  21. package/assets/js/insert-trefs.js +62 -13
  22. package/assets/js/mermaid-diagrams.js +11 -0
  23. package/assets/js/terminology-section-utility-container/README.md +107 -0
  24. package/assets/js/terminology-section-utility-container/create-alphabet-index.js +17 -0
  25. package/assets/js/{create-term-filter.js → terminology-section-utility-container/create-term-filter.js} +11 -44
  26. package/assets/js/terminology-section-utility-container/hide-show-utility-container.js +21 -0
  27. package/assets/js/terminology-section-utility-container/search.js +203 -0
  28. package/assets/js/terminology-section-utility-container.js +203 -0
  29. package/assets/js/tooltips.js +283 -0
  30. package/config/asset-map.json +24 -16
  31. package/index.js +57 -390
  32. package/package.json +5 -2
  33. package/src/add-remove-xref-source.js +20 -21
  34. package/src/collect-external-references.js +8 -337
  35. package/src/collect-external-references.test.js +440 -33
  36. package/src/configure.js +8 -109
  37. package/src/create-docx.js +7 -6
  38. package/src/create-pdf.js +15 -14
  39. package/src/freeze-spec-data.js +46 -0
  40. package/src/git-info.test.js +76 -0
  41. package/src/health-check/destination-gitignore-checker.js +5 -3
  42. package/src/health-check/external-specs-checker.js +5 -4
  43. package/src/health-check/specs-configuration-checker.js +2 -1
  44. package/src/health-check/term-references-checker.js +5 -3
  45. package/src/health-check/terms-intro-checker.js +2 -1
  46. package/src/health-check/tref-term-checker.js +8 -7
  47. package/src/health-check.js +8 -7
  48. package/src/init.js +3 -2
  49. package/src/install-from-boilerplate/add-gitignore-entries.js +3 -2
  50. package/src/install-from-boilerplate/add-scripts-keys.js +5 -4
  51. package/src/install-from-boilerplate/boilerplate/README.md +1 -1
  52. package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +1 -1
  53. package/src/install-from-boilerplate/boilerplate/spec/spec-head.md +1 -1
  54. package/src/install-from-boilerplate/boilerplate/specs.json +2 -1
  55. package/src/install-from-boilerplate/config-scripts-keys.js +3 -3
  56. package/src/install-from-boilerplate/copy-boilerplate.js +2 -1
  57. package/src/install-from-boilerplate/copy-system-files.js +4 -3
  58. package/src/install-from-boilerplate/custom-update.js +12 -1
  59. package/src/install-from-boilerplate/help.txt +1 -1
  60. package/src/install-from-boilerplate/menu.sh +6 -6
  61. package/src/json-key-validator.js +17 -11
  62. package/src/markdown-it/README.md +207 -0
  63. package/src/markdown-it/definition-lists.js +397 -0
  64. package/src/markdown-it/index.js +83 -0
  65. package/src/markdown-it/link-enhancement.js +98 -0
  66. package/src/markdown-it/plugins.js +118 -0
  67. package/src/markdown-it/table-enhancement.js +97 -0
  68. package/src/markdown-it/template-tag-syntax.js +152 -0
  69. package/src/parsers/index.js +16 -0
  70. package/src/parsers/spec-parser.js +152 -0
  71. package/src/parsers/spec-parser.test.js +109 -0
  72. package/src/parsers/template-tag-parser.js +277 -0
  73. package/src/parsers/template-tag-parser.test.js +107 -0
  74. package/src/pipeline/configuration/configure-starterpack.js +200 -0
  75. package/src/{create-external-specs-list.js → pipeline/configuration/create-external-specs-list.js} +13 -12
  76. package/src/{create-term-index.js → pipeline/configuration/create-term-index.js} +19 -18
  77. package/src/{create-versions-index.js → pipeline/configuration/create-versions-index.js} +4 -3
  78. package/src/{insert-term-index.js → pipeline/configuration/insert-term-index.js} +2 -2
  79. package/src/pipeline/configuration/prepare-spec-configuration.js +70 -0
  80. package/src/pipeline/parsing/apply-markdown-it-extensions.js +35 -0
  81. package/src/pipeline/parsing/create-markdown-parser.js +94 -0
  82. package/src/pipeline/parsing/create-markdown-parser.test.js +49 -0
  83. package/src/{html-dom-processor.js → pipeline/postprocessing/definition-list-postprocessor.js} +69 -10
  84. package/src/{escape-handler.js → pipeline/preprocessing/escape-processor.js} +3 -1
  85. package/src/{fix-markdown-files.js → pipeline/preprocessing/normalize-terminology-markdown.js} +41 -31
  86. package/src/pipeline/references/collect-external-references.js +307 -0
  87. package/src/pipeline/references/external-references-service.js +231 -0
  88. package/src/pipeline/references/fetch-terms-from-index.js +198 -0
  89. package/src/pipeline/references/match-term.js +34 -0
  90. package/src/{collectExternalReferences/matchTerm.test.js → pipeline/references/match-term.test.js} +8 -2
  91. package/src/pipeline/references/process-xtrefs-data.js +94 -0
  92. package/src/pipeline/references/xtref-utils.js +166 -0
  93. package/src/pipeline/rendering/render-spec-document.js +146 -0
  94. package/src/pipeline/rendering/render-utils.js +154 -0
  95. package/src/utils/LOGGER.md +81 -0
  96. package/src/utils/{doesUrlExist.js → does-url-exist.js} +4 -3
  97. package/src/utils/fetch.js +5 -4
  98. package/src/utils/file-opener.js +3 -2
  99. package/src/utils/git-info.js +77 -0
  100. package/src/utils/logger.js +74 -0
  101. package/src/utils/regex-patterns.js +471 -0
  102. package/src/utils/regex-patterns.test.js +281 -0
  103. package/templates/template.html +56 -21
  104. package/assets/js/create-alphabet-index.js +0 -60
  105. package/assets/js/hide-show-utility-container.js +0 -16
  106. package/assets/js/index.js +0 -87
  107. package/assets/js/search.js +0 -365
  108. package/src/collectExternalReferences/fetchTermsFromIndex.js +0 -284
  109. package/src/collectExternalReferences/matchTerm.js +0 -32
  110. package/src/collectExternalReferences/processXTrefsData.js +0 -108
  111. package/src/freeze.js +0 -90
  112. package/src/markdown-it-extensions.js +0 -395
  113. package/src/references.js +0 -114
  114. /package/assets/css/{bootstrap.min.css → embedded-libraries/bootstrap.min.css} +0 -0
  115. /package/assets/css/{prism.css → embedded-libraries/prism.css} +0 -0
  116. /package/assets/css/{prism.dark.css → embedded-libraries/prism.dark.css} +0 -0
  117. /package/assets/css/{prism.default.css → embedded-libraries/prism.default.css} +0 -0
  118. /package/assets/js/{bootstrap.bundle.min.js → embedded-libraries/bootstrap.bundle.min.js} +0 -0
  119. /package/assets/js/{chart.js → embedded-libraries/chart.js} +0 -0
  120. /package/assets/js/{diff.min.js → embedded-libraries/diff.min.js} +0 -0
  121. /package/assets/js/{font-awesome.js → embedded-libraries/font-awesome.js} +0 -0
  122. /package/assets/js/{mermaid.js → embedded-libraries/mermaid.js} +0 -0
  123. /package/assets/js/{notyf.js → embedded-libraries/notyf.js} +0 -0
  124. /package/assets/js/{popper.js → embedded-libraries/popper.js} +0 -0
  125. /package/assets/js/{prism.dark.js → embedded-libraries/prism.dark.js} +0 -0
  126. /package/assets/js/{prism.default.js → embedded-libraries/prism.default.js} +0 -0
  127. /package/assets/js/{prism.js → embedded-libraries/prism.js} +0 -0
  128. /package/assets/js/{tippy.js → embedded-libraries/tippy.js} +0 -0
  129. /package/src/{escape-mechanism.js → pipeline/preprocessing/escape-placeholder-utils.js} +0 -0
  130. /package/src/utils/{isLineWithDefinition.js → is-line-with-definition.js} +0 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Template Tag Parser - Functional Style
3
+ *
4
+ * This module provides pure functions for processing template-tag markdown extensions:
5
+ * - [[def: term, alias]] - Term definitions with optional aliases
6
+ * - [[ref: term]] - Internal term references
7
+ * - [[xref: spec, term]] - External specification term references
8
+ * - [[tref: spec, term, alias1, alias2, ...]] - External term references with multiple aliases
9
+ *
10
+ * The functional approach reduces cognitive complexity and makes functions easier to test
11
+ * as they are pure functions with clear inputs and outputs.
12
+ */
13
+
14
+ const { findExternalSpecByKey } = require('../pipeline/references/external-references-service.js');
15
+ const { lookupXrefTerm } = require('../pipeline/rendering/render-utils.js');
16
+ const { whitespace, htmlComments, contentCleaning, externalReferences } = require('../utils/regex-patterns');
17
+
18
+ /**
19
+ * Extracts the current file from token content for source tracking
20
+ * @param {Object} token - The markdown-it token
21
+ * @param {Object} globalState - Global state containing fallback currentFile
22
+ * @returns {string} The source file name
23
+ */
24
+ function extractCurrentFile(token, globalState) {
25
+ const content = token.map ? token.map[0] : '';
26
+ const fileMatch = content && content.match && content.match(htmlComments.fileTracker);
27
+ return fileMatch ? fileMatch[1] : globalState.currentFile || 'unknown';
28
+ }
29
+
30
+ /**
31
+ * Main parsing entry point for template-tag constructs
32
+ * @param {Object} config - Configuration object containing specs and settings
33
+ * @param {Object} globalState - Global state object containing definitions, references, etc.
34
+ * @param {Object} token - The markdown-it token being processed
35
+ * @param {string} type - The type of construct (def, ref, xref, tref)
36
+ * @param {string} primary - The primary content/term
37
+ * @returns {string} The rendered HTML for the construct
38
+ */
39
+ function parseTemplateTag(config, globalState, token, type, primary) {
40
+ if (!primary) return;
41
+
42
+ const currentFile = extractCurrentFile(token, globalState);
43
+
44
+ switch (type) {
45
+ case 'def':
46
+ return parseDef(globalState, token, primary, currentFile);
47
+ case 'xref':
48
+ return parseXref(config, token);
49
+ case 'tref':
50
+ return parseTref(token);
51
+ default:
52
+ return parseRef(globalState, primary);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Processes [[def: term, alias1, alias2, ...]] constructs
58
+ * Creates definition entries and generates HTML spans with proper IDs
59
+ * Format: "Primary Alias (alias2, alias3, original-term)" similar to tref
60
+ * @param {Object} globalState - Global state to store definitions
61
+ * @param {Object} token - The markdown-it token
62
+ * @param {string} primary - The primary term content
63
+ * @param {string} currentFile - The source file containing this definition
64
+ * @returns {string} HTML span elements with term IDs
65
+ */
66
+ function parseDef(globalState, token, primary, currentFile) {
67
+ const termName = token.info.args[0];
68
+ const aliases = token.info.args.slice(1).filter(Boolean); // Get all aliases after the term
69
+
70
+ // Store definition in global state for validation and cross-referencing
71
+ globalState.definitions.push({
72
+ term: termName,
73
+ alias: aliases[0] || null, // First alias, or null if no aliases
74
+ source: currentFile
75
+ });
76
+
77
+ // Determine the primary display term (first alias if available, otherwise original term)
78
+ const primaryDisplayTerm = aliases.length > 0 ? aliases[0] : termName;
79
+
80
+ // Build the display text format: "Primary (alias1, alias2, original-term)"
81
+ let displayText = primaryDisplayTerm;
82
+
83
+ if (aliases.length > 0) {
84
+ // Collect all additional terms to show in parentheses
85
+ const parentheticalContent = [];
86
+
87
+ // Add remaining aliases (after the first one used as primary)
88
+ if (aliases.length > 1) {
89
+ parentheticalContent.push(...aliases.slice(1));
90
+ }
91
+
92
+ // Add original term if it's different from the primary display term
93
+ if (termName !== primaryDisplayTerm) {
94
+ parentheticalContent.push(`<span class='term-local-original-term' title='original term'>${termName}</span>`);
95
+ }
96
+
97
+ // Append parenthetical terms if any exist
98
+ if (parentheticalContent.length > 0) {
99
+ displayText += ` <span class='term-local-parenthetical-terms'>(${parentheticalContent.join(', ')})</span>`;
100
+ }
101
+ }
102
+
103
+ // Generate HTML spans for each term/alias combination
104
+ // This creates anchor points that can be referenced by links
105
+ // IDs stay intact - we create an ID for the original term and each alias
106
+ return token.info.args.reduce((acc, syn) => {
107
+ // Generate a unique term ID by normalizing the synonym: replace whitespace with hyphens and convert to lowercase. The ID is used for fragment identifier (hash) in the URL, which in turn can be used for an anchor in a web page.
108
+ const termId = `term:${syn.replace(whitespace.oneOrMore, '-').toLowerCase()}`;
109
+ return `<span id="${termId}">${acc}</span>`;
110
+ }, displayText);
111
+ }
112
+
113
+ /**
114
+ * Processes [[ref: term]] constructs
115
+ * Creates internal links to locally defined terms
116
+ * @param {Object} globalState - Global state to track references
117
+ * @param {string} primary - The term to reference
118
+ * @returns {string} HTML anchor element linking to local term
119
+ */
120
+ function parseRef(globalState, primary) {
121
+ // Track this reference for validation purposes
122
+ globalState.references.push(primary);
123
+
124
+ // Create internal link to the term definition
125
+ const termId = primary.replace(whitespace.oneOrMore, '-').toLowerCase();
126
+ return `<a class="term-reference" href="#term:${termId}">${primary}</a>`;
127
+ }
128
+
129
+ /**
130
+ * Processes [[xref: spec, term, alias, ...]] constructs
131
+ * Creates links to external specification terms with tooltips
132
+ * Uses primaryDisplayTerm concept: shows first alias if available, otherwise shows the term itself
133
+ * @param {Object} config - Configuration containing external specs
134
+ * @param {Object} token - The markdown-it token
135
+ * @returns {string} HTML anchor element linking to external term
136
+ */
137
+ function parseXref(config, token) {
138
+ const externalSpec = findExternalSpecByKey(config, token.info.args[0]);
139
+ const url = externalSpec?.gh_page || '#';
140
+ const termName = token.info.args[1];
141
+ const aliases = token.info.args.slice(2).filter(Boolean); // Get all aliases after the term
142
+ const term = termName.replace(whitespace.oneOrMore, '-').toLowerCase();
143
+ const xrefTerm = lookupXrefTerm(token.info.args[0], term);
144
+
145
+ // Determine the primary display term (first alias if available, otherwise original term)
146
+ const primaryDisplayTerm = aliases.length > 0 ? aliases[0] : termName;
147
+
148
+ // Build link attributes with both local and external href capabilities
149
+ let linkAttributes = `class="x-term-reference term-reference" data-local-href="#term:${token.info.args[0]}:${term}" href="${url}#term:${term}"`;
150
+
151
+ // Add tooltip content if term definition is available
152
+ if (xrefTerm && xrefTerm.content) {
153
+ const cleanContent = xrefTerm.content.replace(contentCleaning.quotes, '&quot;').replace(contentCleaning.newlines, ' ');
154
+ linkAttributes += ` title="External term definition" data-term-content="${cleanContent}"`;
155
+ }
156
+
157
+ return `<a ${linkAttributes}>${primaryDisplayTerm}</a>`;
158
+ }
159
+
160
+ /**
161
+ * Processes [[tref: spec, term, alias, ...]] constructs
162
+ * Creates external term references with multiple aliases displayed in a readable format
163
+ * Format: "Primary Alias (alias1, alias2, original-term)"
164
+ * @param {Object} token - The markdown-it token
165
+ * @returns {string} HTML span element for external term reference
166
+ */
167
+ function parseTref(token) {
168
+ const termName = token.info.args[1];
169
+ const aliases = token.info.args.slice(2).filter(Boolean); // Get all aliases after the term
170
+
171
+ // Determine the primary display term (first alias if available, otherwise original term)
172
+ const primaryDisplayTerm = aliases.length > 0 ? aliases[0] : termName;
173
+
174
+ // Build the display text format: "Primary (alias1, alias2, original-term)"
175
+ let displayText = primaryDisplayTerm;
176
+
177
+ if (aliases.length > 0) {
178
+ // Collect all additional terms to show in parentheses
179
+ const parentheticalContent = [];
180
+
181
+ // Add remaining aliases (after the first one used as primary)
182
+ if (aliases.length > 1) {
183
+ parentheticalContent.push(...aliases.slice(1));
184
+ }
185
+
186
+ // Add original term if it's different from the primary display term
187
+ if (termName !== primaryDisplayTerm) {
188
+ parentheticalContent.push(`<span class='term-external-original-term' title='original term'>${termName}</span>`);
189
+ }
190
+
191
+ // Append parenthetical terms if any exist
192
+ if (parentheticalContent.length > 0) {
193
+ displayText += ` <span class='term-external-parenthetical-terms'>(${parentheticalContent.join(', ')})</span>`;
194
+ }
195
+ }
196
+
197
+ const termId = `term:${termName.replace(whitespace.oneOrMore, '-').toLowerCase()}`;
198
+ const primaryAliasId = aliases.length > 0 ? `term:${aliases[0].replace(whitespace.oneOrMore, '-').toLowerCase()}` : '';
199
+
200
+ // Handle cases where we have aliases
201
+ if (aliases.length > 0 && aliases[0] !== termName) {
202
+ return `<span data-original-term="${termName}" class="term-external" id="${termId}"><span title="Externally defined as ${termName}" id="${primaryAliasId}">${displayText}</span></span>`;
203
+ } else {
204
+ return `<span title="Externally also defined as ${termName}" data-original-term="${termName}" class="term-external" id="${termId}">${displayText}</span>`;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Parses an `[[xref:...]]` or `[[tref:...]]` string into a structured object.
210
+ * This function was moved from xtref-utils.js to consolidate parsing logic
211
+ * and prevent cross-module object mutation.
212
+ *
213
+ * @param {string} xtref - Raw reference markup including brackets and prefix.
214
+ * @returns {{ externalSpec: string, term: string, referenceType: string, firstAlias?: string, aliases: string[] }}
215
+ */
216
+ function processXTrefObject(xtref) {
217
+ const referenceTypeMatch = xtref.match(externalReferences.referenceType);
218
+ const referenceType = referenceTypeMatch ? referenceTypeMatch[1] : 'unknown';
219
+
220
+ const parts = xtref
221
+ .replace(externalReferences.openingTag, '')
222
+ .replace(externalReferences.closingTag, '')
223
+ .trim()
224
+ .split(externalReferences.argsSeparator);
225
+
226
+ const xtrefObject = {
227
+ externalSpec: parts[0].trim(),
228
+ term: parts[1].trim(),
229
+ referenceType
230
+ };
231
+
232
+ // Collect all aliases from parts after the term (index 1), trim and filter empties
233
+ const allAliases = parts.slice(2).map(p => p.trim()).filter(Boolean);
234
+
235
+ // Initialize both tref and xref alias arrays
236
+ xtrefObject.trefAliases = [];
237
+ xtrefObject.xrefAliases = [];
238
+
239
+ // Store aliases in the appropriate array based on reference type
240
+ if (referenceType === 'tref') {
241
+ xtrefObject.trefAliases = allAliases;
242
+ // Store the first tref alias separately as it has special meaning
243
+ if (allAliases.length > 0) {
244
+ xtrefObject.firstTrefAlias = allAliases[0];
245
+ }
246
+ } else if (referenceType === 'xref') {
247
+ xtrefObject.xrefAliases = allAliases;
248
+ // Store the first xref alias separately as it has special meaning
249
+ if (allAliases.length > 0) {
250
+ xtrefObject.firstXrefAlias = allAliases[0];
251
+ }
252
+ }
253
+
254
+ return xtrefObject;
255
+ }
256
+
257
+ /**
258
+ * Creates a template-tag parser function with bound configuration and global state.
259
+ * This provides a clean interface similar to the class-based approach but with functional benefits.
260
+ * @param {Object} config - Configuration object
261
+ * @param {Object} globalState - Global state object
262
+ * @returns {Function} A parser function that can be called with (token, type, primary)
263
+ */
264
+ function createTemplateTagParser(config, globalState) {
265
+ return (token, type, primary) => parseTemplateTag(config, globalState, token, type, primary);
266
+ }
267
+
268
+ module.exports = {
269
+ createTemplateTagParser,
270
+ // Export individual functions for testing purposes
271
+ parseDef,
272
+ parseXref,
273
+ parseTref,
274
+ parseRef,
275
+ parseTemplateTag,
276
+ processXTrefObject
277
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Template Tag Parser Tests - Functional Style
3
+ *
4
+ * These tests verify that the template tag parsing functions work correctly
5
+ * and maintain backward compatibility while providing better testability.
6
+ */
7
+
8
+ const {
9
+ createTemplateTagParser
10
+ } = require('../pipeline/parsing/create-markdown-parser.js');
11
+
12
+ // Import functions directly from their modules since they're not exported through index
13
+ const {
14
+ parseDef,
15
+ parseRef
16
+ } = require('./template-tag-parser');
17
+
18
+ // Tests for verifying the template tag parsing functions work correctly
19
+ describe('Template Tag Parsing Functions', () => {
20
+ let mockConfig, mockGlobal;
21
+
22
+ beforeEach(() => {
23
+ mockConfig = {
24
+ specs: [{
25
+ external_specs: [{
26
+ external_spec: 'test-spec',
27
+ gh_page: 'https://example.com/spec'
28
+ }]
29
+ }]
30
+ };
31
+
32
+ mockGlobal = {
33
+ definitions: [],
34
+ references: [],
35
+ specGroups: {},
36
+ noticeTitles: {},
37
+ currentFile: 'test.md'
38
+ };
39
+
40
+ // Set up global state
41
+ global.definitions = mockGlobal.definitions;
42
+ global.references = mockGlobal.references;
43
+ global.specGroups = mockGlobal.specGroups;
44
+ global.noticeTitles = mockGlobal.noticeTitles;
45
+ });
46
+
47
+ // Test: Can the system parse definition markup into proper HTML?
48
+ test('should parse definition correctly', () => {
49
+ const mockToken = {
50
+ info: { args: ['test-term', 'alias'] }
51
+ };
52
+
53
+ const result = parseDef(mockGlobal, mockToken, 'Test Term', 'test.md');
54
+
55
+ expect(result).toContain('id="term:test-term"');
56
+ expect(result).toContain('id="term:alias"');
57
+ expect(mockGlobal.definitions).toHaveLength(1);
58
+ expect(mockGlobal.definitions[0]).toEqual({
59
+ term: 'test-term',
60
+ alias: 'alias',
61
+ source: 'test.md'
62
+ });
63
+ });
64
+
65
+ // Test: Can the system parse reference markup into proper links?
66
+ test('should parse reference correctly', () => {
67
+ const result = parseRef(mockGlobal, 'test-term');
68
+
69
+ expect(result).toContain('href="#term:test-term"');
70
+ expect(result).toContain('class="term-reference"');
71
+ expect(mockGlobal.references).toContain('test-term');
72
+ });
73
+
74
+ // Test: Does the parser factory create functional parsers?
75
+ test('createTemplateTagParser should return a working function', () => {
76
+ const templateTagParser = createTemplateTagParser(mockConfig, mockGlobal);
77
+
78
+ expect(typeof templateTagParser).toBe('function');
79
+
80
+ const mockToken = {
81
+ info: { args: ['test-term'] }
82
+ };
83
+
84
+ const result = templateTagParser(mockToken, 'def', 'Test Term');
85
+ expect(result).toContain('id="term:test-term"');
86
+ });
87
+
88
+ // Test: Are the functions pure and independently testable?
89
+ test('functions should be pure and testable', () => {
90
+ // Test that pure functions work independently
91
+ const testGlobal = { definitions: [], references: [] };
92
+ const testToken = { info: { args: ['pure-test'] } };
93
+
94
+ const result = parseDef(testGlobal, testToken, 'Pure Test', 'test.md');
95
+
96
+ expect(result).toContain('id="term:pure-test"');
97
+ expect(testGlobal.definitions).toHaveLength(1);
98
+ expect(mockGlobal.definitions).toHaveLength(0); // Original should be unchanged
99
+ });
100
+
101
+ // Test: Can individual functions be imported and used separately?
102
+ test('individual functions should be importable', () => {
103
+ // Test that individual functions can be imported and used
104
+ expect(typeof parseDef).toBe('function');
105
+ expect(typeof parseRef).toBe('function');
106
+ });
107
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Interactive configurator for the Spec-Up-T starter pack.
3
+ *
4
+ * The module collects starter metadata from the user, updates the first
5
+ * spec entry in `specs.json`, and persists the changes. It can be required
6
+ * programmatically or executed directly from the command line.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const readline = require('readline');
12
+
13
+ const Logger = require('../../utils/logger');
14
+
15
+ const SPECS_KEY = 'specs';
16
+ const JSON_FILE_PATH = path.resolve(process.cwd(), 'specs.json');
17
+
18
+ const defaultQuestions = [
19
+ { field: 'title', prompt: 'Enter title', defaultValue: 'Spec-Up-T Starterpack' },
20
+ { field: 'description', prompt: 'Enter description', defaultValue: 'Create technical specifications in markdown. Based on the original Spec-Up, extended with Terminology tooling' },
21
+ { field: 'author', prompt: 'Enter author', defaultValue: 'Trust over IP Foundation' },
22
+ { field: 'account', prompt: 'Enter account', defaultValue: 'trustoverip' },
23
+ { field: 'repo', prompt: 'Enter repo', defaultValue: 'spec-up-t-starter-pack' }
24
+ ];
25
+
26
+ /**
27
+ * Verifies that the specs.json file exists before prompting the user.
28
+ */
29
+ function assertSpecsFileExists() {
30
+ if (fs.existsSync(JSON_FILE_PATH)) {
31
+ return;
32
+ }
33
+
34
+ Logger.error(`Error: ${JSON_FILE_PATH} does not exist.`);
35
+ process.exit(1);
36
+ }
37
+
38
+ /**
39
+ * Reads and parses the specs.json file, providing helpful feedback on failure.
40
+ *
41
+ * @returns {object} Parsed JSON contents.
42
+ */
43
+ function loadSpecsConfig() {
44
+ try {
45
+ return JSON.parse(fs.readFileSync(JSON_FILE_PATH, 'utf8'));
46
+ } catch (error) {
47
+ Logger.error(`Error: Could not parse ${JSON_FILE_PATH}.`, error.message);
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Ensures the first spec entry exists and returns it for updates. Exits when the
54
+ * structure is malformed because continuing would corrupt the file.
55
+ *
56
+ * @param {object} config - Parsed configuration object from specs.json.
57
+ * @returns {{ config: object, primarySpec: object }} The config and its first spec entry.
58
+ */
59
+ function resolvePrimarySpec(config) {
60
+ const specs = config[SPECS_KEY];
61
+
62
+ if (!Array.isArray(specs) || specs.length === 0 || !specs[0]) {
63
+ Logger.error(`Error: Invalid JSON structure. "${SPECS_KEY}[0]" is missing.`);
64
+ process.exit(1);
65
+ }
66
+
67
+ return { config, primarySpec: specs[0] };
68
+ }
69
+
70
+ /**
71
+ * Builds the prompt text with the default value appended for clarity.
72
+ *
73
+ * @param {string} label - Question label displayed to the user.
74
+ * @param {string} defaultValue - Value shown as default.
75
+ * @returns {string} Prompt string formatted for readline.
76
+ */
77
+ function formatPrompt(label, defaultValue) {
78
+ return `${label} (${defaultValue}): `;
79
+ }
80
+
81
+ /**
82
+ * Collects answers for all configuration questions from stdin/stdout.
83
+ *
84
+ * @param {Array<{ field: string, prompt: string, defaultValue: string }>} questions - Questions to ask.
85
+ * @param {NodeJS.ReadableStream} input - Input stream for readline.
86
+ * @param {NodeJS.WritableStream} output - Output stream for readline.
87
+ * @returns {Promise<object>} Map of field names to user answers.
88
+ */
89
+ function promptForAnswers(questions, input, output) {
90
+ return new Promise((resolve, reject) => {
91
+ const rl = readline.createInterface({ input, output });
92
+ const answers = {};
93
+ let index = 0;
94
+
95
+ const askNext = () => {
96
+ if (index >= questions.length) {
97
+ rl.close();
98
+ resolve(answers);
99
+ return;
100
+ }
101
+
102
+ const { field, prompt, defaultValue } = questions[index];
103
+ rl.question(formatPrompt(prompt, defaultValue), answer => {
104
+ answers[field] = answer ? answer.trim() : defaultValue;
105
+ index += 1;
106
+ askNext();
107
+ });
108
+ };
109
+
110
+ rl.on('error', error => {
111
+ rl.close();
112
+ reject(error);
113
+ });
114
+
115
+ askNext();
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Applies the collected answers to the spec configuration, keeping nested
121
+ * `source` fields isolated from top-level metadata.
122
+ *
123
+ * @param {object} primarySpec - The spec entry to mutate.
124
+ * @param {object} answers - User responses keyed by field.
125
+ */
126
+ function applyAnswersToSpec(primarySpec, answers) {
127
+ if (!primarySpec.source) {
128
+ primarySpec.source = {};
129
+ }
130
+
131
+ Object.entries(answers).forEach(([field, value]) => {
132
+ if (['account', 'repo'].includes(field)) {
133
+ primarySpec.source[field] = value;
134
+ return;
135
+ }
136
+
137
+ primarySpec[field] = value;
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Persists the updated configuration back to specs.json.
143
+ *
144
+ * @param {object} config - Configuration object with modifications applied.
145
+ */
146
+ function persistUpdatedConfig(config) {
147
+ try {
148
+ fs.writeFileSync(JSON_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
149
+ Logger.success(`Successfully updated ${JSON_FILE_PATH}.`);
150
+ } catch (error) {
151
+ Logger.error(`Error: Could not update ${JSON_FILE_PATH}.`, error.message);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Runs the interactive configuration workflow.
158
+ *
159
+ * @param {object} [options]
160
+ * @param {Array} [options.questions] - Override the default question set.
161
+ * @param {NodeJS.ReadableStream} [options.input] - Custom input stream for testing.
162
+ * @param {NodeJS.WritableStream} [options.output] - Custom output stream for testing.
163
+ * @returns {Promise<void>}
164
+ */
165
+ async function runStarterpackConfigurator({
166
+ questions = defaultQuestions,
167
+ input = process.stdin,
168
+ output = process.stdout
169
+ } = {}) {
170
+ assertSpecsFileExists();
171
+
172
+ Logger.info(`\nWelcome to the Spec-Up-T Starterpack configuration tool!\n` +
173
+ '\nYou will be asked a series of questions to customize your project.\n' +
174
+ "Here’s what each field means:\n" +
175
+ '- "Title": The name of your project.\n' +
176
+ '- "Description": A brief explanation of your project.\n' +
177
+ '- "Author": The name of the person or organization creating the project.\n' +
178
+ '- "Account": The GitHub account or organization where the repository will be hosted.\n' +
179
+ '- "Repo": The name of the GitHub repository.\n\n' +
180
+ 'Press Enter to accept the default value shown in parentheses.\n'
181
+ );
182
+
183
+ try {
184
+ const answers = await promptForAnswers(questions, input, output);
185
+ const { config, primarySpec } = resolvePrimarySpec(loadSpecsConfig());
186
+ applyAnswersToSpec(primarySpec, answers);
187
+ persistUpdatedConfig(config);
188
+ } catch (error) {
189
+ Logger.error('Configuration aborted due to an unexpected error.', error.message);
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ if (process.env.SPEC_UP_T_CONFIGURATOR_AUTORUN !== 'false') {
195
+ runStarterpackConfigurator();
196
+ }
197
+
198
+ module.exports = {
199
+ runStarterpackConfigurator
200
+ };
@@ -12,9 +12,11 @@
12
12
  * Returns a message indicating no specifications were found if the configuration is invalid or empty.
13
13
  */
14
14
 
15
+ const Logger = require('../../utils/logger.js');
16
+
15
17
  module.exports = function createExternalSpecsList(config) {
16
18
  if (!config?.specs?.length || !Array.isArray(config.specs)) {
17
- console.warn('Invalid config format. Expected an object with a specs array.');
19
+ Logger.warn('Invalid config format. Expected an object with a specs array.');
18
20
  return '<p>No external specifications found.</p>';
19
21
  }
20
22
 
@@ -29,24 +31,23 @@ module.exports = function createExternalSpecsList(config) {
29
31
  return '<p>No external specifications found.</p>';
30
32
  }
31
33
 
32
- let html = '<ul class="list-group">';
34
+ let html = '<div class="external-specs-list">';
35
+ html += '<ul class="list-unstyled">';
33
36
 
34
37
  externalSpecs.forEach(spec => {
35
38
  html += `
36
- <li class="list-group-item border-0 p-0 ps-3">
37
- <p>
38
- ${spec.external_spec}:
39
- <a href="${spec.url}" target="_blank" class="">
40
- <i class="fa fa-link"></i> URL
41
- </a>
42
- <a href="${spec.gh_page}" target="_blank" class="">
43
- <i class="fa fa-github"></i> GitHub Page
39
+ <li class="mb-2">
40
+ <div class="d-flex align-items-center">
41
+ <i class="bi bi-diagram-3 me-2"></i>
42
+ <a href="${spec.url}" target="_blank" class="text-decoration-none">${spec.external_spec}</a>
43
+ <a href="${spec.gh_page}" target="_blank" class="ms-2 btn btn-sm btn-light">
44
+ <i class="bi bi-github"></i>
44
45
  </a>
45
- </p>
46
+ </div>
46
47
  </li>
47
48
  `;
48
49
  });
49
50
 
50
- html += '</ul>';
51
+ html += '</ul></div>';
51
52
  return html;
52
53
  };