pict-docuserve 1.3.2 → 1.3.3
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/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +1162 -780
- package/dist/pict-docuserve.js +549 -365
- package/dist/pict-docuserve.js.map +1 -1
- package/dist/pict-docuserve.min.js +6 -6
- package/dist/pict-docuserve.min.js.map +1 -1
- package/package.json +6 -6
- package/source/Pict-Application-Docuserve.js +13 -0
- package/source/cli/Docuserve-CLI-Program.js +2 -1
- package/source/cli/commands/Docuserve-Command-StageExamples.js +601 -0
- package/source/providers/Pict-Provider-Docuserve-Documentation.js +106 -11
- package/dist/css/docuserve.css +0 -327
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-docuserve",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Pict Documentation Server - A single-page documentation viewer built on Pict",
|
|
5
5
|
"main": "source/Pict-Application-Docuserve.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"fable-serviceproviderbase": "^3.0.19",
|
|
32
32
|
"lunr": "^2.3.9",
|
|
33
|
-
"pict": "^1.0.
|
|
33
|
+
"pict": "^1.0.372",
|
|
34
34
|
"pict-application": "^1.0.34",
|
|
35
35
|
"pict-provider": "^1.0.13",
|
|
36
36
|
"pict-section-code": "^1.0.11",
|
|
37
|
-
"pict-section-content": "^1.0.
|
|
37
|
+
"pict-section-content": "^1.0.3",
|
|
38
38
|
"pict-section-histogram": "^1.0.1",
|
|
39
39
|
"pict-section-modal": "^1.1.1",
|
|
40
|
-
"pict-section-theme": "^1.0.
|
|
40
|
+
"pict-section-theme": "^1.0.5",
|
|
41
41
|
"pict-service-commandlineutility": "^1.0.19",
|
|
42
42
|
"pict-view": "^1.0.68"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"pict-docuserve": "^1.3.
|
|
46
|
-
"quackage": "^1.2.
|
|
45
|
+
"pict-docuserve": "^1.3.2",
|
|
46
|
+
"quackage": "^1.2.4"
|
|
47
47
|
},
|
|
48
48
|
"copyFilesSettings": {
|
|
49
49
|
"whenFileExists": "overwrite"
|
|
@@ -573,6 +573,19 @@ class DocuserveApplication extends libPictApplication
|
|
|
573
573
|
|
|
574
574
|
tmpDocProvider.fetchLocalDocument(tmpPath, (pError, pHTML) =>
|
|
575
575
|
{
|
|
576
|
+
// On a miss the path may be a directory reference — retry
|
|
577
|
+
// <path>/README.md so directory-style links resolve.
|
|
578
|
+
if (pError && !tmpPath.match(/(^|\/)README\.md$/i))
|
|
579
|
+
{
|
|
580
|
+
let tmpReadmePath = tmpPath.replace(/\.md$/i, '') + '/README.md';
|
|
581
|
+
tmpDocProvider.fetchLocalDocument(tmpReadmePath, (pReadmeError, pReadmeHTML) =>
|
|
582
|
+
{
|
|
583
|
+
// Show the README on success, else the original
|
|
584
|
+
// not-found page (it names the path the user asked for).
|
|
585
|
+
tmpContentView.displayContent(pReadmeError ? pHTML : pReadmeHTML);
|
|
586
|
+
});
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
576
589
|
// fetchDocument always provides displayable HTML in pHTML,
|
|
577
590
|
// even on error, so we can use it directly.
|
|
578
591
|
tmpContentView.displayContent(pHTML);
|
|
@@ -11,7 +11,8 @@ let _PictCLIProgram = new libCLIProgram(
|
|
|
11
11
|
[
|
|
12
12
|
require('./commands/Docuserve-Command-Serve.js'),
|
|
13
13
|
require('./commands/Docuserve-Command-Inject.js'),
|
|
14
|
-
require('./commands/Docuserve-Command-PrepareLocal.js')
|
|
14
|
+
require('./commands/Docuserve-Command-PrepareLocal.js'),
|
|
15
|
+
require('./commands/Docuserve-Command-StageExamples.js')
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
module.exports = _PictCLIProgram;
|
|
@@ -0,0 +1,601 @@
|
|
|
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 index + intro quick-links.
|
|
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._writeIntroQuickLinks(tmpStaged, tmpDocsFolder);
|
|
291
|
+
}
|
|
292
|
+
catch (pError)
|
|
293
|
+
{
|
|
294
|
+
this.log.warn(`Could not update intro quick-links: ${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
|
+
let tmpInner = `> **[▶ Launch the live app](${mdSafeUrl('examples/' + tmpName + '/index.html')})** — runs in your browser, opens in a new tab.`;
|
|
496
|
+
|
|
497
|
+
let tmpText;
|
|
498
|
+
if (libFS.existsSync(tmpReadmePath))
|
|
499
|
+
{
|
|
500
|
+
tmpText = libFS.readFileSync(tmpReadmePath, 'utf8');
|
|
501
|
+
}
|
|
502
|
+
else
|
|
503
|
+
{
|
|
504
|
+
// Scaffold a minimal stub — only ever created when no writeup
|
|
505
|
+
// exists. Hand-authored writeups are never invented over.
|
|
506
|
+
tmpText = `# ${tmpTitle}\n\n${tmpSummary}\n`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
tmpText = replaceRegion(tmpText, _Marker.LaunchStart, _Marker.LaunchEnd, tmpInner,
|
|
510
|
+
(pBody, pRegion) =>
|
|
511
|
+
{
|
|
512
|
+
// No markers — insert the block right after the first H1.
|
|
513
|
+
let tmpH1 = pBody.match(/^#\s+.*$/m);
|
|
514
|
+
if (tmpH1)
|
|
515
|
+
{
|
|
516
|
+
let tmpIndex = pBody.indexOf(tmpH1[0]) + tmpH1[0].length;
|
|
517
|
+
return pBody.substring(0, tmpIndex) + '\n\n' + pRegion + '\n' + pBody.substring(tmpIndex);
|
|
518
|
+
}
|
|
519
|
+
return pRegion + '\n\n' + pBody;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
libFS.writeFileSync(tmpReadmePath, tmpText);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Maintain the generated quick-reference table inside the examples
|
|
527
|
+
* index at docs/examples/README.md.
|
|
528
|
+
*
|
|
529
|
+
* @param {object[]} pStaged - Staged example metadata.
|
|
530
|
+
* @param {string} pDocsFolder - The target documentation folder.
|
|
531
|
+
*/
|
|
532
|
+
_writeExamplesIndex(pStaged, pDocsFolder)
|
|
533
|
+
{
|
|
534
|
+
let tmpIndexPath = libPath.join(pDocsFolder, 'examples', 'README.md');
|
|
535
|
+
|
|
536
|
+
let tmpRows = pStaged.map((pEx) =>
|
|
537
|
+
{
|
|
538
|
+
let tmpComplexity = escapeTableCell(pEx.Complexity) || '—';
|
|
539
|
+
return `| [${escapeTableCell(pEx.Title)}](${mdSafeUrl('examples/' + pEx.Name + '/README.md')}) | ${tmpComplexity} | ${escapeTableCell(pEx.Summary)} | [▶ Launch](${mdSafeUrl('examples/' + pEx.Name + '/index.html')}) |`;
|
|
540
|
+
});
|
|
541
|
+
let tmpInner = [
|
|
542
|
+
'| Example | Complexity | Summary | Live |',
|
|
543
|
+
'|---------|------------|---------|------|'
|
|
544
|
+
].concat(tmpRows).join('\n');
|
|
545
|
+
|
|
546
|
+
let tmpText = libFS.existsSync(tmpIndexPath)
|
|
547
|
+
? libFS.readFileSync(tmpIndexPath, 'utf8')
|
|
548
|
+
: '# Example Applications\n';
|
|
549
|
+
|
|
550
|
+
tmpText = replaceRegion(tmpText, _Marker.IndexStart, _Marker.IndexEnd, tmpInner,
|
|
551
|
+
(pBody, pRegion) =>
|
|
552
|
+
{
|
|
553
|
+
// No markers — append a new "Live Examples" section.
|
|
554
|
+
return pBody.replace(/\s*$/, '') + '\n\n## Live Examples\n\n' + pRegion + '\n';
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
libFS.writeFileSync(tmpIndexPath, tmpText);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Maintain the generated "Example Applications" quick-links block on the
|
|
562
|
+
* documentation intro page at docs/README.md.
|
|
563
|
+
*
|
|
564
|
+
* @param {object[]} pStaged - Staged example metadata.
|
|
565
|
+
* @param {string} pDocsFolder - The target documentation folder.
|
|
566
|
+
*/
|
|
567
|
+
_writeIntroQuickLinks(pStaged, pDocsFolder)
|
|
568
|
+
{
|
|
569
|
+
let tmpReadmePath = libPath.join(pDocsFolder, 'README.md');
|
|
570
|
+
|
|
571
|
+
let tmpBullets = pStaged.map((pEx) =>
|
|
572
|
+
{
|
|
573
|
+
let tmpSummary = String(pEx.Summary || '').replace(/\r?\n/g, ' ').trim();
|
|
574
|
+
return `- **[${pEx.Title}](${mdSafeUrl('examples/' + pEx.Name + '/README.md')})** — ${tmpSummary} · [▶ Launch live app](${mdSafeUrl('examples/' + pEx.Name + '/index.html')})`;
|
|
575
|
+
});
|
|
576
|
+
let tmpInner = '*Live, runnable example applications — each opens in a new browser tab:*\n\n' + tmpBullets.join('\n');
|
|
577
|
+
|
|
578
|
+
let tmpText = libFS.existsSync(tmpReadmePath)
|
|
579
|
+
? libFS.readFileSync(tmpReadmePath, 'utf8')
|
|
580
|
+
: '# Documentation\n';
|
|
581
|
+
|
|
582
|
+
tmpText = replaceRegion(tmpText, _Marker.QuickLinksStart, _Marker.QuickLinksEnd, tmpInner,
|
|
583
|
+
(pBody, pRegion) =>
|
|
584
|
+
{
|
|
585
|
+
// Prefer to place the block directly under an existing
|
|
586
|
+
// "Example Applications" heading.
|
|
587
|
+
let tmpHeading = pBody.match(/^#{2,}\s+Example Applications\s*$/m);
|
|
588
|
+
if (tmpHeading)
|
|
589
|
+
{
|
|
590
|
+
let tmpIndex = pBody.indexOf(tmpHeading[0]) + tmpHeading[0].length;
|
|
591
|
+
return pBody.substring(0, tmpIndex) + '\n\n' + pRegion + '\n' + pBody.substring(tmpIndex);
|
|
592
|
+
}
|
|
593
|
+
// Otherwise append a new section at the end of the page.
|
|
594
|
+
return pBody.replace(/\s*$/, '') + '\n\n## Example Applications\n\n' + pRegion + '\n';
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
libFS.writeFileSync(tmpReadmePath, tmpText);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
module.exports = DocuserveCommandStageExamples;
|