pict-docuserve 1.3.2 → 1.3.4

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,606 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+
3
+ const libFS = require('fs');
4
+ const libPath = require('path');
5
+ const libChildProcess = require('child_process');
6
+
7
+ /**
8
+ * Built-in package -> CDN URL map.
9
+ *
10
+ * When a staged example vendors one of these packages (declared as a
11
+ * `copyFiles` glob in the example's package.json that points into
12
+ * `node_modules/<pkg>/dist/`), the large library bundle is NOT copied into
13
+ * the docs folder. Instead the staged index.html is rewritten to load the
14
+ * library from jsDelivr. Add entries here to make additional dependencies
15
+ * CDN-externalizable.
16
+ *
17
+ * Vendored files of a package with NO entry here are copied normally, so
18
+ * the staged example still works (just heavier).
19
+ */
20
+ const _PackageCDNMap = (
21
+ {
22
+ 'pict': 'https://cdn.jsdelivr.net/npm/pict@1/dist/pict.min.js',
23
+ 'chart.js': 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js'
24
+ });
25
+
26
+ /**
27
+ * Marker comments delimiting the generated regions this command maintains.
28
+ *
29
+ * Everything BETWEEN a marker pair is regenerated on every run; everything
30
+ * outside the markers is hand-authored prose and is never modified.
31
+ */
32
+ const _Marker = (
33
+ {
34
+ LaunchStart: '<!-- docuserve:example-launch:start -->',
35
+ LaunchEnd: '<!-- docuserve:example-launch:end -->',
36
+ IndexStart: '<!-- docuserve:examples-index:start -->',
37
+ IndexEnd: '<!-- docuserve:examples-index:end -->',
38
+ QuickLinksStart: '<!-- docuserve:examples:start -->',
39
+ QuickLinksEnd: '<!-- docuserve:examples:end -->'
40
+ });
41
+
42
+ /**
43
+ * Recursively create a directory if it does not exist.
44
+ */
45
+ function ensureDir(pPath)
46
+ {
47
+ if (!libFS.existsSync(pPath))
48
+ {
49
+ libFS.mkdirSync(pPath, { recursive: true });
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Escape a string for safe literal use inside a regular expression.
55
+ */
56
+ function escapeRegExp(pText)
57
+ {
58
+ return String(pText).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ }
60
+
61
+ /**
62
+ * Resolve the package name from a `copyFiles` `from` glob that points into
63
+ * a node_modules folder. Returns null when the glob is not a node_modules
64
+ * vendor glob (e.g. `./html/*`).
65
+ */
66
+ function packageNameFromGlob(pFromGlob)
67
+ {
68
+ let tmpNormalized = String(pFromGlob).replace(/\\/g, '/');
69
+ let tmpMatch = tmpNormalized.match(/node_modules\/(@[^/]+\/[^/]+|[^/@][^/]*)\//);
70
+ return tmpMatch ? tmpMatch[1] : null;
71
+ }
72
+
73
+ /**
74
+ * List the actual files on disk that a `copyFiles` `from` glob resolves to.
75
+ *
76
+ * Only the trailing path segment is treated as a glob (the examples use
77
+ * simple `*` patterns like `dist/*` or `dist/chart.umd*`).
78
+ *
79
+ * @param {string} pBaseDir - Directory the glob is relative to (the example dir).
80
+ * @param {string} pFromGlob - The `from` glob value.
81
+ * @returns {string[]} Matching file basenames.
82
+ */
83
+ function listGlobFiles(pBaseDir, pFromGlob)
84
+ {
85
+ let tmpAbsGlob = libPath.resolve(pBaseDir, pFromGlob);
86
+ let tmpDir = libPath.dirname(tmpAbsGlob);
87
+ let tmpBasenamePattern = libPath.basename(tmpAbsGlob);
88
+
89
+ if (!libFS.existsSync(tmpDir))
90
+ {
91
+ return [];
92
+ }
93
+
94
+ let tmpRegEx = new RegExp('^' + escapeRegExp(tmpBasenamePattern).replace(/\\\*/g, '.*') + '$');
95
+ return libFS.readdirSync(tmpDir).filter((pName) =>
96
+ {
97
+ if (!tmpRegEx.test(pName))
98
+ {
99
+ return false;
100
+ }
101
+ try
102
+ {
103
+ return libFS.statSync(libPath.join(tmpDir, pName)).isFile();
104
+ }
105
+ catch (pError)
106
+ {
107
+ return false;
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Replace (or insert) a marker-delimited generated region within a body of
114
+ * text. When both markers are present the content between them is
115
+ * replaced; otherwise fInsert decides placement.
116
+ *
117
+ * @param {string} pText - The current file contents.
118
+ * @param {string} pStartMarker - The opening marker comment.
119
+ * @param {string} pEndMarker - The closing marker comment.
120
+ * @param {string} pInnerBlock - The generated content for between the markers.
121
+ * @param {Function} fInsert - (pText, pRegion) => newText, used when markers are absent.
122
+ * @returns {string} The updated text.
123
+ */
124
+ function replaceRegion(pText, pStartMarker, pEndMarker, pInnerBlock, fInsert)
125
+ {
126
+ let tmpRegion = pStartMarker + '\n' + pInnerBlock + '\n' + pEndMarker;
127
+
128
+ let tmpStartIndex = pText.indexOf(pStartMarker);
129
+ let tmpEndIndex = pText.indexOf(pEndMarker);
130
+
131
+ if ((tmpStartIndex >= 0) && (tmpEndIndex > tmpStartIndex))
132
+ {
133
+ return pText.substring(0, tmpStartIndex) + tmpRegion + pText.substring(tmpEndIndex + pEndMarker.length);
134
+ }
135
+
136
+ return fInsert(pText, tmpRegion);
137
+ }
138
+
139
+ /**
140
+ * Escape a value for safe use inside a markdown table cell.
141
+ */
142
+ function escapeTableCell(pText)
143
+ {
144
+ return String(pText || '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
145
+ }
146
+
147
+ /**
148
+ * Percent-encode underscores in a generated link URL.
149
+ *
150
+ * docuserve's markdown renderer applies emphasis (`_text_`) even inside
151
+ * link URLs, so an href containing underscores — e.g. an example folder
152
+ * named `simple_form` — is corrupted into `<em>` markup. Encoding the
153
+ * underscore as %5F sidesteps the renderer; the HTTP layer decodes it back
154
+ * on fetch, so the staged files still resolve on both the dev server and
155
+ * GitHub Pages.
156
+ */
157
+ function mdSafeUrl(pURL)
158
+ {
159
+ return String(pURL).replace(/_/g, '%5F');
160
+ }
161
+
162
+ class DocuserveCommandStageExamples extends libCommandLineCommand
163
+ {
164
+ constructor(pFable, pManifest, pServiceHash)
165
+ {
166
+ super(pFable, pManifest, pServiceHash);
167
+
168
+ this.options.CommandKeyword = 'stage-examples';
169
+ this.options.Description = 'Build flagged example applications and stage them into a docs folder for static hosting.';
170
+
171
+ this.options.CommandArguments.push({ Name: '[docs-path]', Description: 'Target documentation folder to stage examples into (defaults to ./docs/).' });
172
+
173
+ this.options.CommandOptions.push({ Name: '-m, --module_root [module_root]', Description: 'Module root containing the example_applications/ folder (defaults to CWD).', Default: '' });
174
+
175
+ this.addCommand();
176
+ }
177
+
178
+ onRun()
179
+ {
180
+ let tmpDocsFolder = libPath.resolve(this.ArgumentString || './docs/');
181
+ let tmpModuleRoot = libPath.resolve(this.CommandOptions.module_root || process.cwd());
182
+ let tmpExamplesRoot = libPath.join(tmpModuleRoot, 'example_applications');
183
+
184
+ // No example_applications/ folder -> clean no-op. This command is
185
+ // wired into `quack prepare-docs` and must be safe to run for any
186
+ // module, whether or not it ships example applications.
187
+ if (!libFS.existsSync(tmpExamplesRoot))
188
+ {
189
+ this.log.info(`No example_applications/ folder at [${tmpExamplesRoot}]; nothing to stage.`);
190
+ return;
191
+ }
192
+
193
+ this.log.info(`Staging example applications from [${tmpExamplesRoot}] into [${tmpDocsFolder}]...`);
194
+
195
+ // Discover flagged examples: example_applications/<name>/package.json
196
+ // carrying retold.ExampleApplication.Stage === true.
197
+ let tmpDiscovered = [];
198
+ let tmpEntries = [];
199
+ try
200
+ {
201
+ tmpEntries = libFS.readdirSync(tmpExamplesRoot);
202
+ }
203
+ catch (pError)
204
+ {
205
+ this.log.warn(`Could not read [${tmpExamplesRoot}]: ${pError.message}`);
206
+ return;
207
+ }
208
+
209
+ for (let i = 0; i < tmpEntries.length; i++)
210
+ {
211
+ let tmpName = tmpEntries[i];
212
+ let tmpExampleDir = libPath.join(tmpExamplesRoot, tmpName);
213
+
214
+ try
215
+ {
216
+ if (!libFS.statSync(tmpExampleDir).isDirectory())
217
+ {
218
+ continue;
219
+ }
220
+ }
221
+ catch (pError)
222
+ {
223
+ continue;
224
+ }
225
+
226
+ let tmpPackagePath = libPath.join(tmpExampleDir, 'package.json');
227
+ if (!libFS.existsSync(tmpPackagePath))
228
+ {
229
+ continue;
230
+ }
231
+
232
+ let tmpPackage;
233
+ try
234
+ {
235
+ tmpPackage = JSON.parse(libFS.readFileSync(tmpPackagePath, 'utf8'));
236
+ }
237
+ catch (pError)
238
+ {
239
+ this.log.warn(`Could not parse [${tmpPackagePath}]: ${pError.message}`);
240
+ continue;
241
+ }
242
+
243
+ let tmpFlag = tmpPackage.retold && tmpPackage.retold.ExampleApplication;
244
+ if (!tmpFlag || (tmpFlag.Stage !== true))
245
+ {
246
+ continue;
247
+ }
248
+
249
+ tmpDiscovered.push({ Name: tmpName, Dir: tmpExampleDir, Package: tmpPackage, Flag: tmpFlag });
250
+ }
251
+
252
+ if (tmpDiscovered.length < 1)
253
+ {
254
+ this.log.info(`No example applications are flagged for staging (retold.ExampleApplication.Stage); nothing to do.`);
255
+ return;
256
+ }
257
+
258
+ ensureDir(libPath.join(tmpDocsFolder, 'examples'));
259
+
260
+ // Build + stage each flagged example. A per-example failure is
261
+ // logged and skipped — it is never fatal to the overall run.
262
+ let tmpStaged = [];
263
+ for (let i = 0; i < tmpDiscovered.length; i++)
264
+ {
265
+ let tmpResult = this._stageExample(tmpDiscovered[i], tmpDocsFolder);
266
+ if (tmpResult)
267
+ {
268
+ tmpStaged.push(tmpResult);
269
+ }
270
+ }
271
+
272
+ if (tmpStaged.length < 1)
273
+ {
274
+ this.log.warn(`No example applications were staged successfully.`);
275
+ return;
276
+ }
277
+
278
+ // Regenerate the marker-delimited examples index + the splash region.
279
+ try
280
+ {
281
+ this._writeExamplesIndex(tmpStaged, tmpDocsFolder);
282
+ }
283
+ catch (pError)
284
+ {
285
+ this.log.warn(`Could not update examples index: ${pError.message}`);
286
+ }
287
+
288
+ try
289
+ {
290
+ this._writeCoverExamples(tmpStaged, tmpDocsFolder);
291
+ }
292
+ catch (pError)
293
+ {
294
+ this.log.warn(`Could not update splash examples region: ${pError.message}`);
295
+ }
296
+
297
+ this.log.info(`Staged ${tmpStaged.length} example application(s): ${tmpStaged.map((pEx) => pEx.Name).join(', ')}`);
298
+ }
299
+
300
+ /**
301
+ * Build a single example application and stage it into the docs folder.
302
+ *
303
+ * @param {object} pExample - { Name, Dir, Package, Flag }
304
+ * @param {string} pDocsFolder - The target documentation folder.
305
+ * @returns {object|null} { Name, Title, Summary, Complexity } on success, null on failure.
306
+ */
307
+ _stageExample(pExample, pDocsFolder)
308
+ {
309
+ let tmpName = pExample.Name;
310
+ this.log.info(` [${tmpName}] building...`);
311
+
312
+ // 1. Build the example with quackage (cwd = the example directory).
313
+ try
314
+ {
315
+ libChildProcess.execSync('npx quack build && npx quack copy',
316
+ { cwd: pExample.Dir, stdio: 'pipe', timeout: 600000 });
317
+ }
318
+ catch (pError)
319
+ {
320
+ let tmpDetail = '';
321
+ if (pError.stderr)
322
+ {
323
+ tmpDetail = pError.stderr.toString().split('\n').slice(-6).join('\n');
324
+ }
325
+ this.log.warn(` [${tmpName}] build failed; skipping. ${pError.message}`);
326
+ if (tmpDetail)
327
+ {
328
+ this.log.warn(` [${tmpName}] build output tail:\n${tmpDetail}`);
329
+ }
330
+ return null;
331
+ }
332
+
333
+ try
334
+ {
335
+ let tmpDistPath = libPath.join(pExample.Dir, 'dist');
336
+ let tmpIndexSource = libPath.join(tmpDistPath, 'index.html');
337
+ if (!libFS.existsSync(tmpIndexSource))
338
+ {
339
+ this.log.warn(` [${tmpName}] no dist/index.html after build; skipping.`);
340
+ return null;
341
+ }
342
+
343
+ // 2. Compute the CDN-externalized vendored file set from the
344
+ // example's own copyFiles globs. Only files belonging to a
345
+ // CDN-mapped package are externalized; everything else is
346
+ // staged normally so the example still runs.
347
+ let tmpVendoredBasenames = new Set();
348
+ let tmpBasenameToCDN = new Map();
349
+ let tmpCopyFiles = Array.isArray(pExample.Package.copyFiles) ? pExample.Package.copyFiles : [];
350
+ for (let i = 0; i < tmpCopyFiles.length; i++)
351
+ {
352
+ let tmpFrom = tmpCopyFiles[i] && tmpCopyFiles[i].from;
353
+ if (!tmpFrom)
354
+ {
355
+ continue;
356
+ }
357
+ let tmpPackageName = packageNameFromGlob(tmpFrom);
358
+ if (!tmpPackageName || !_PackageCDNMap[tmpPackageName])
359
+ {
360
+ continue;
361
+ }
362
+ let tmpFiles = listGlobFiles(pExample.Dir, tmpFrom);
363
+ for (let f = 0; f < tmpFiles.length; f++)
364
+ {
365
+ tmpVendoredBasenames.add(tmpFiles[f]);
366
+ if (tmpFiles[f].endsWith('.js'))
367
+ {
368
+ tmpBasenameToCDN.set(tmpFiles[f], _PackageCDNMap[tmpPackageName]);
369
+ }
370
+ }
371
+ }
372
+
373
+ let tmpStageDir = libPath.join(pDocsFolder, 'examples', tmpName);
374
+ ensureDir(tmpStageDir);
375
+
376
+ // 3. Read the built index.html and rewrite vendored <script src>
377
+ // references to their CDN URLs. The app's own bundle <script>
378
+ // (a sibling file) is left untouched.
379
+ let tmpHTML = libFS.readFileSync(tmpIndexSource, 'utf8');
380
+ tmpBasenameToCDN.forEach((pCDNURL, pBasename) =>
381
+ {
382
+ let tmpPattern = new RegExp('(\\bsrc\\s*=\\s*)(["\'])\\.?/?' + escapeRegExp(pBasename) + '\\2', 'gi');
383
+ tmpHTML = tmpHTML.replace(tmpPattern, '$1$2' + pCDNURL + '$2');
384
+ });
385
+ libFS.writeFileSync(libPath.join(tmpStageDir, 'index.html'), tmpHTML);
386
+
387
+ // 4. Determine which local (non-CDN) JS bundles the rewritten
388
+ // index.html actually references — only those are staged.
389
+ let tmpReferencedFiles = new Set();
390
+ let tmpRefMatch;
391
+ let tmpRefRegEx = /(?:src|href)\s*=\s*(["'])([^"']+)\1/gi;
392
+ while ((tmpRefMatch = tmpRefRegEx.exec(tmpHTML)) !== null)
393
+ {
394
+ let tmpRef = tmpRefMatch[2];
395
+ if (/^(?:[a-z]+:)?\/\//i.test(tmpRef) || /^(?:data:|mailto:|#)/i.test(tmpRef))
396
+ {
397
+ continue;
398
+ }
399
+ tmpReferencedFiles.add(libPath.basename(tmpRef.split('?')[0].split('#')[0]));
400
+ }
401
+
402
+ // 5. Stage dist/* into docs/examples/<name>/, skipping the
403
+ // CDN-externalized libraries and sourcemaps. JS files are
404
+ // staged only when index.html references them (the app
405
+ // bundle); data/asset files (json, css, svg, ...) are always
406
+ // staged.
407
+ let tmpStagedFiles = [];
408
+ let tmpDistEntries = libFS.readdirSync(tmpDistPath);
409
+ for (let i = 0; i < tmpDistEntries.length; i++)
410
+ {
411
+ let tmpFile = tmpDistEntries[i];
412
+ if (tmpFile === 'index.html')
413
+ {
414
+ continue;
415
+ }
416
+ if (tmpFile.endsWith('.map'))
417
+ {
418
+ continue;
419
+ }
420
+ if (tmpVendoredBasenames.has(tmpFile))
421
+ {
422
+ continue;
423
+ }
424
+
425
+ let tmpSourceFile = libPath.join(tmpDistPath, tmpFile);
426
+ try
427
+ {
428
+ if (!libFS.statSync(tmpSourceFile).isFile())
429
+ {
430
+ continue;
431
+ }
432
+ }
433
+ catch (pError)
434
+ {
435
+ continue;
436
+ }
437
+
438
+ if (tmpFile.endsWith('.js') && !tmpReferencedFiles.has(tmpFile))
439
+ {
440
+ continue;
441
+ }
442
+
443
+ let tmpDestFile = libPath.join(tmpStageDir, tmpFile);
444
+ if (tmpFile.endsWith('.js'))
445
+ {
446
+ // Strip the sourceMappingURL pragma — the .map sibling
447
+ // is intentionally not staged, so leaving the pragma
448
+ // would 404 in the browser devtools.
449
+ let tmpJS = libFS.readFileSync(tmpSourceFile, 'utf8');
450
+ tmpJS = tmpJS.replace(/\s*\/\/[#@]\s*sourceMappingURL=\S*\s*$/, '\n');
451
+ libFS.writeFileSync(tmpDestFile, tmpJS);
452
+ }
453
+ else
454
+ {
455
+ libFS.copyFileSync(tmpSourceFile, tmpDestFile);
456
+ }
457
+ tmpStagedFiles.push(tmpFile);
458
+ }
459
+
460
+ // 6. Maintain the launch block in the example's writeup.
461
+ this._writeLaunchBlock(pExample, pDocsFolder);
462
+
463
+ this.log.info(` [${tmpName}] staged -> ${tmpStageDir} (index.html, ${tmpStagedFiles.join(', ') || 'no extra files'})`);
464
+
465
+ return {
466
+ Name: tmpName,
467
+ Title: (pExample.Flag.Title || tmpName),
468
+ Summary: (pExample.Flag.Summary || pExample.Package.description || ''),
469
+ Complexity: (pExample.Flag.Complexity || '')
470
+ };
471
+ }
472
+ catch (pError)
473
+ {
474
+ this.log.warn(` [${tmpName}] staging failed; skipping. ${pError.message}`);
475
+ return null;
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Maintain the generated launch block inside an example's writeup at
481
+ * docs/examples/<name>/README.md. When the writeup does not exist a
482
+ * minimal stub is scaffolded; an existing writeup's hand-authored prose
483
+ * is never overwritten.
484
+ *
485
+ * @param {object} pExample - { Name, Package, Flag }
486
+ * @param {string} pDocsFolder - The target documentation folder.
487
+ */
488
+ _writeLaunchBlock(pExample, pDocsFolder)
489
+ {
490
+ let tmpName = pExample.Name;
491
+ let tmpReadmePath = libPath.join(pDocsFolder, 'examples', tmpName, 'README.md');
492
+ let tmpTitle = pExample.Flag.Title || tmpName;
493
+ let tmpSummary = pExample.Flag.Summary || pExample.Package.description || '';
494
+
495
+ // The writeup lives at docs/examples/<name>/README.md, so index.html
496
+ // is its sibling — a document-relative href docuserve resolves the
497
+ // same way it resolves the writeup's other links.
498
+ let tmpInner = `> **[&#9654; Launch the live app](index.html)** — runs in your browser, opens in a new tab.`;
499
+
500
+ let tmpText;
501
+ if (libFS.existsSync(tmpReadmePath))
502
+ {
503
+ tmpText = libFS.readFileSync(tmpReadmePath, 'utf8');
504
+ }
505
+ else
506
+ {
507
+ // Scaffold a minimal stub — only ever created when no writeup
508
+ // exists. Hand-authored writeups are never invented over.
509
+ tmpText = `# ${tmpTitle}\n\n${tmpSummary}\n`;
510
+ }
511
+
512
+ tmpText = replaceRegion(tmpText, _Marker.LaunchStart, _Marker.LaunchEnd, tmpInner,
513
+ (pBody, pRegion) =>
514
+ {
515
+ // No markers — insert the block right after the first H1.
516
+ let tmpH1 = pBody.match(/^#\s+.*$/m);
517
+ if (tmpH1)
518
+ {
519
+ let tmpIndex = pBody.indexOf(tmpH1[0]) + tmpH1[0].length;
520
+ return pBody.substring(0, tmpIndex) + '\n\n' + pRegion + '\n' + pBody.substring(tmpIndex);
521
+ }
522
+ return pRegion + '\n\n' + pBody;
523
+ });
524
+
525
+ libFS.writeFileSync(tmpReadmePath, tmpText);
526
+ }
527
+
528
+ /**
529
+ * Maintain the generated quick-reference table inside the examples
530
+ * index at docs/examples/README.md.
531
+ *
532
+ * @param {object[]} pStaged - Staged example metadata.
533
+ * @param {string} pDocsFolder - The target documentation folder.
534
+ */
535
+ _writeExamplesIndex(pStaged, pDocsFolder)
536
+ {
537
+ let tmpIndexPath = libPath.join(pDocsFolder, 'examples', 'README.md');
538
+
539
+ let tmpRows = pStaged.map((pEx) =>
540
+ {
541
+ let tmpComplexity = escapeTableCell(pEx.Complexity) || '—';
542
+ // Both links are relative to this index (docs/examples/README.md),
543
+ // so they omit the examples/ segment.
544
+ return `| [${escapeTableCell(pEx.Title)}](${mdSafeUrl(pEx.Name + '/README.md')}) | ${tmpComplexity} | ${escapeTableCell(pEx.Summary)} | [&#9654; Launch](${mdSafeUrl(pEx.Name + '/index.html')}) |`;
545
+ });
546
+ let tmpInner = [
547
+ '| Example | Complexity | Summary | Live |',
548
+ '|---------|------------|---------|------|'
549
+ ].concat(tmpRows).join('\n');
550
+
551
+ let tmpText = libFS.existsSync(tmpIndexPath)
552
+ ? libFS.readFileSync(tmpIndexPath, 'utf8')
553
+ : '# Example Applications\n';
554
+
555
+ tmpText = replaceRegion(tmpText, _Marker.IndexStart, _Marker.IndexEnd, tmpInner,
556
+ (pBody, pRegion) =>
557
+ {
558
+ // No markers — append a new "Live Examples" section.
559
+ return pBody.replace(/\s*$/, '') + '\n\n## Live Examples\n\n' + pRegion + '\n';
560
+ });
561
+
562
+ libFS.writeFileSync(tmpIndexPath, tmpText);
563
+ }
564
+
565
+ /**
566
+ * Maintain the generated examples region in docs/_cover.md. docuserve's
567
+ * Splash view renders this region as the "Interactive Examples" section of
568
+ * the landing page. When _cover.md is absent the region is not created —
569
+ * the splash simply shows no examples section.
570
+ *
571
+ * @param {object[]} pStaged - Staged example metadata.
572
+ * @param {string} pDocsFolder - The target documentation folder.
573
+ */
574
+ _writeCoverExamples(pStaged, pDocsFolder)
575
+ {
576
+ let tmpCoverPath = libPath.join(pDocsFolder, '_cover.md');
577
+ if (!libFS.existsSync(tmpCoverPath))
578
+ {
579
+ this.log.info(`No _cover.md at [${tmpCoverPath}]; skipping splash examples region.`);
580
+ return;
581
+ }
582
+
583
+ let tmpRows = pStaged.map((pEx) =>
584
+ {
585
+ let tmpComplexity = escapeTableCell(pEx.Complexity) || '—';
586
+ return `| [${escapeTableCell(pEx.Title)}](${mdSafeUrl('examples/' + pEx.Name + '/README.md')}) | ${tmpComplexity} | [&#9654; Launch](${mdSafeUrl('examples/' + pEx.Name + '/index.html')}) |`;
587
+ });
588
+ let tmpInner = [
589
+ '| Example | Complexity | Launch |',
590
+ '|---------|------------|--------|'
591
+ ].concat(tmpRows).join('\n');
592
+
593
+ let tmpText = libFS.readFileSync(tmpCoverPath, 'utf8');
594
+
595
+ tmpText = replaceRegion(tmpText, _Marker.QuickLinksStart, _Marker.QuickLinksEnd, tmpInner,
596
+ (pBody, pRegion) =>
597
+ {
598
+ // No markers yet — append the region at the end of the cover.
599
+ return pBody.replace(/\s*$/, '') + '\n\n' + pRegion + '\n';
600
+ });
601
+
602
+ libFS.writeFileSync(tmpCoverPath, tmpText);
603
+ }
604
+ }
605
+
606
+ module.exports = DocuserveCommandStageExamples;