pict-docuserve 1.3.3 → 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.
- package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +267 -458
- package/dist/pict-docuserve.js +73 -17
- package/dist/pict-docuserve.js.map +1 -1
- package/dist/pict-docuserve.min.js +2 -2
- package/dist/pict-docuserve.min.js.map +1 -1
- package/package.json +3 -3
- package/source/Pict-Application-Docuserve.js +2 -2
- package/source/cli/Docuserve-CLI-Program.js +2 -1
- package/source/cli/commands/Docuserve-Command-CheckLinks.js +428 -0
- package/source/cli/commands/Docuserve-Command-StageExamples.js +32 -27
- package/source/providers/Pict-Provider-Docuserve-Documentation.js +150 -15
- package/source/views/PictView-Docuserve-Splash.js +126 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-docuserve",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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": {
|
|
@@ -34,7 +34,7 @@
|
|
|
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.4",
|
|
38
38
|
"pict-section-histogram": "^1.0.1",
|
|
39
39
|
"pict-section-modal": "^1.1.1",
|
|
40
40
|
"pict-section-theme": "^1.0.5",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"pict-view": "^1.0.68"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"pict-docuserve": "^1.3.
|
|
45
|
+
"pict-docuserve": "^1.3.3",
|
|
46
46
|
"quackage": "^1.2.4"
|
|
47
47
|
},
|
|
48
48
|
"copyFilesSettings": {
|
|
@@ -583,13 +583,13 @@ class DocuserveApplication extends libPictApplication
|
|
|
583
583
|
// Show the README on success, else the original
|
|
584
584
|
// not-found page (it names the path the user asked for).
|
|
585
585
|
tmpContentView.displayContent(pReadmeError ? pHTML : pReadmeHTML);
|
|
586
|
-
});
|
|
586
|
+
}, '', '', tmpReadmePath);
|
|
587
587
|
return;
|
|
588
588
|
}
|
|
589
589
|
// fetchDocument always provides displayable HTML in pHTML,
|
|
590
590
|
// even on error, so we can use it directly.
|
|
591
591
|
tmpContentView.displayContent(pHTML);
|
|
592
|
-
});
|
|
592
|
+
}, '', '', tmpPath);
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
/**
|
|
@@ -12,7 +12,8 @@ let _PictCLIProgram = new libCLIProgram(
|
|
|
12
12
|
require('./commands/Docuserve-Command-Serve.js'),
|
|
13
13
|
require('./commands/Docuserve-Command-Inject.js'),
|
|
14
14
|
require('./commands/Docuserve-Command-PrepareLocal.js'),
|
|
15
|
-
require('./commands/Docuserve-Command-StageExamples.js')
|
|
15
|
+
require('./commands/Docuserve-Command-StageExamples.js'),
|
|
16
|
+
require('./commands/Docuserve-Command-CheckLinks.js')
|
|
16
17
|
]);
|
|
17
18
|
|
|
18
19
|
module.exports = _PictCLIProgram;
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
|
|
2
|
+
|
|
3
|
+
const libFS = require('fs');
|
|
4
|
+
const libPath = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recursively collect every .md file beneath a folder.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} pRoot - The folder to walk.
|
|
10
|
+
* @returns {string[]} Absolute paths of every markdown file found, sorted.
|
|
11
|
+
*/
|
|
12
|
+
function collectMarkdownFiles(pRoot)
|
|
13
|
+
{
|
|
14
|
+
let tmpResults = [];
|
|
15
|
+
let tmpStack = [pRoot];
|
|
16
|
+
|
|
17
|
+
while (tmpStack.length > 0)
|
|
18
|
+
{
|
|
19
|
+
let tmpDir = tmpStack.pop();
|
|
20
|
+
let tmpEntries;
|
|
21
|
+
try
|
|
22
|
+
{
|
|
23
|
+
tmpEntries = libFS.readdirSync(tmpDir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch (pError)
|
|
26
|
+
{
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < tmpEntries.length; i++)
|
|
31
|
+
{
|
|
32
|
+
let tmpEntry = tmpEntries[i];
|
|
33
|
+
let tmpFullPath = libPath.join(tmpDir, tmpEntry.name);
|
|
34
|
+
if (tmpEntry.isDirectory())
|
|
35
|
+
{
|
|
36
|
+
tmpStack.push(tmpFullPath);
|
|
37
|
+
}
|
|
38
|
+
else if (tmpEntry.isFile() && /\.md$/i.test(tmpEntry.name))
|
|
39
|
+
{
|
|
40
|
+
tmpResults.push(tmpFullPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tmpResults.sort();
|
|
46
|
+
return tmpResults;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when a file exists on disk and is a regular file.
|
|
51
|
+
*/
|
|
52
|
+
function fileExists(pPath)
|
|
53
|
+
{
|
|
54
|
+
try
|
|
55
|
+
{
|
|
56
|
+
return libFS.statSync(pPath).isFile();
|
|
57
|
+
}
|
|
58
|
+
catch (pError)
|
|
59
|
+
{
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* True when a path exists on disk and is a directory.
|
|
66
|
+
*/
|
|
67
|
+
function directoryExists(pPath)
|
|
68
|
+
{
|
|
69
|
+
try
|
|
70
|
+
{
|
|
71
|
+
return libFS.statSync(pPath).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch (pError)
|
|
74
|
+
{
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a relative href against a base directory, collapsing "." and ".."
|
|
81
|
+
* segments. ".." is clamped at the docs root. Mirrors docuserve module-mode
|
|
82
|
+
* resolution (Pict-Provider-Docuserve-Documentation._resolveRelativeDocPath)
|
|
83
|
+
* so the checker resolves .md links exactly the way the viewer routes them.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} pBaseDir - The directory the href is relative to (POSIX, docs-root-relative).
|
|
86
|
+
* @param {string} pHref - The href to resolve.
|
|
87
|
+
* @returns {string} The resolved docs-root-relative path (no leading slash).
|
|
88
|
+
*/
|
|
89
|
+
function resolveRelativeDocPath(pBaseDir, pHref)
|
|
90
|
+
{
|
|
91
|
+
let tmpSegments = [];
|
|
92
|
+
let tmpCombined = (pBaseDir ? pBaseDir + '/' : '') + String(pHref || '');
|
|
93
|
+
let tmpParts = tmpCombined.split('/');
|
|
94
|
+
for (let i = 0; i < tmpParts.length; i++)
|
|
95
|
+
{
|
|
96
|
+
let tmpPart = tmpParts[i];
|
|
97
|
+
if ((tmpPart === '') || (tmpPart === '.'))
|
|
98
|
+
{
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (tmpPart === '..')
|
|
102
|
+
{
|
|
103
|
+
if (tmpSegments.length > 0)
|
|
104
|
+
{
|
|
105
|
+
tmpSegments.pop();
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
tmpSegments.push(tmpPart);
|
|
110
|
+
}
|
|
111
|
+
return tmpSegments.join('/');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* True when a link target points outside the docs site and must never be
|
|
116
|
+
* resolved against the filesystem: an absolute or protocol-relative URL, a
|
|
117
|
+
* scheme such as mailto:/data:/tel:, or a pure #anchor.
|
|
118
|
+
*/
|
|
119
|
+
function isExternalOrAnchor(pTarget)
|
|
120
|
+
{
|
|
121
|
+
if (!pTarget)
|
|
122
|
+
{
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (pTarget.charAt(0) === '#')
|
|
126
|
+
{
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (pTarget.indexOf('//') === 0)
|
|
130
|
+
{
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(pTarget))
|
|
134
|
+
{
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Normalize a raw link target extracted from markdown: drop a surrounding
|
|
142
|
+
* <...>, drop a link title (` "Title"`), drop a #fragment / ?query, and
|
|
143
|
+
* decode the %5F underscore escape that docuserve's mdSafeUrl introduces.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} pRawTarget - The target captured between the link parens.
|
|
146
|
+
* @returns {string} The cleaned target, ready for filesystem resolution.
|
|
147
|
+
*/
|
|
148
|
+
function cleanTarget(pRawTarget)
|
|
149
|
+
{
|
|
150
|
+
let tmpTarget = String(pRawTarget || '').trim();
|
|
151
|
+
|
|
152
|
+
// Drop an angle-bracket wrapper: [text](<url>) -> url
|
|
153
|
+
tmpTarget = tmpTarget.replace(/^<+/, '').replace(/>+$/, '');
|
|
154
|
+
|
|
155
|
+
// Drop a link title: (url "Title") -> url
|
|
156
|
+
let tmpSpaceIndex = tmpTarget.search(/\s/);
|
|
157
|
+
if (tmpSpaceIndex >= 0)
|
|
158
|
+
{
|
|
159
|
+
tmpTarget = tmpTarget.substring(0, tmpSpaceIndex);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Drop a #fragment and/or ?query
|
|
163
|
+
tmpTarget = tmpTarget.split('#')[0].split('?')[0];
|
|
164
|
+
|
|
165
|
+
// docuserve's mdSafeUrl percent-encodes underscores; the HTTP layer
|
|
166
|
+
// decodes them back on fetch, so resolve against the decoded path.
|
|
167
|
+
tmpTarget = tmpTarget.replace(/%5[fF]/g, '_');
|
|
168
|
+
|
|
169
|
+
return tmpTarget.trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class DocuserveCommandCheckLinks extends libCommandLineCommand
|
|
173
|
+
{
|
|
174
|
+
constructor(pFable, pManifest, pServiceHash)
|
|
175
|
+
{
|
|
176
|
+
super(pFable, pManifest, pServiceHash);
|
|
177
|
+
|
|
178
|
+
this.options.CommandKeyword = 'check-links';
|
|
179
|
+
this.options.Description = 'Scan a docs folder for unresolvable local links and image/media references.';
|
|
180
|
+
|
|
181
|
+
this.options.CommandArguments.push({ Name: '[docs-path]', Description: 'Documentation folder to check (defaults to ./docs/).' });
|
|
182
|
+
|
|
183
|
+
this.addCommand();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onRun()
|
|
187
|
+
{
|
|
188
|
+
let tmpDocsRoot = libPath.resolve(this.ArgumentString || './docs/');
|
|
189
|
+
|
|
190
|
+
if (!directoryExists(tmpDocsRoot))
|
|
191
|
+
{
|
|
192
|
+
this.log.error(`Docs folder not found at [${tmpDocsRoot}].`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.log.info(`Checking local links + media references under [${tmpDocsRoot}]...`);
|
|
198
|
+
|
|
199
|
+
let tmpFiles = collectMarkdownFiles(tmpDocsRoot);
|
|
200
|
+
if (tmpFiles.length < 1)
|
|
201
|
+
{
|
|
202
|
+
this.log.info(`No markdown files found; nothing to check.`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let tmpLinkCount = 0;
|
|
207
|
+
let tmpImageCount = 0;
|
|
208
|
+
let tmpBroken = [];
|
|
209
|
+
|
|
210
|
+
for (let f = 0; f < tmpFiles.length; f++)
|
|
211
|
+
{
|
|
212
|
+
let tmpFilePath = tmpFiles[f];
|
|
213
|
+
let tmpContent;
|
|
214
|
+
try
|
|
215
|
+
{
|
|
216
|
+
tmpContent = libFS.readFileSync(tmpFilePath, 'utf8');
|
|
217
|
+
}
|
|
218
|
+
catch (pError)
|
|
219
|
+
{
|
|
220
|
+
this.log.warn(`Could not read [${tmpFilePath}]: ${pError.message}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let tmpReferences = this._extractReferences(tmpContent);
|
|
225
|
+
for (let r = 0; r < tmpReferences.length; r++)
|
|
226
|
+
{
|
|
227
|
+
let tmpReference = tmpReferences[r];
|
|
228
|
+
|
|
229
|
+
if (tmpReference.IsImage)
|
|
230
|
+
{
|
|
231
|
+
tmpImageCount++;
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{
|
|
235
|
+
tmpLinkCount++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let tmpTarget = cleanTarget(tmpReference.Target);
|
|
239
|
+
if (isExternalOrAnchor(tmpTarget) || (tmpTarget === ''))
|
|
240
|
+
{
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let tmpResolution = tmpReference.IsImage
|
|
245
|
+
? this._resolveImageReference(tmpTarget, tmpFilePath, tmpDocsRoot)
|
|
246
|
+
: this._resolveDocumentLink(tmpTarget, tmpFilePath, tmpDocsRoot);
|
|
247
|
+
|
|
248
|
+
if (!tmpResolution.OK)
|
|
249
|
+
{
|
|
250
|
+
tmpBroken.push(
|
|
251
|
+
{
|
|
252
|
+
File: libPath.relative(tmpDocsRoot, tmpFilePath),
|
|
253
|
+
Line: tmpReference.Line,
|
|
254
|
+
Snippet: tmpReference.Snippet,
|
|
255
|
+
Reason: tmpResolution.Reason
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.log.info(`Checked ${tmpLinkCount} link(s) + ${tmpImageCount} image reference(s) across ${tmpFiles.length} file(s).`);
|
|
262
|
+
|
|
263
|
+
if (tmpBroken.length > 0)
|
|
264
|
+
{
|
|
265
|
+
this.log.error(`Found ${tmpBroken.length} broken reference(s):`);
|
|
266
|
+
for (let i = 0; i < tmpBroken.length; i++)
|
|
267
|
+
{
|
|
268
|
+
let tmpEntry = tmpBroken[i];
|
|
269
|
+
this.log.error(` ${tmpEntry.File}:${tmpEntry.Line} ${tmpEntry.Snippet} -> ${tmpEntry.Reason}`);
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.log.info(`All local links + media references resolve.`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract every inline link and image reference from a markdown document.
|
|
280
|
+
*
|
|
281
|
+
* Fenced code blocks and inline code spans are skipped so that example
|
|
282
|
+
* code containing `[text](target)` is never treated as a real link.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} pContent - The raw markdown text.
|
|
285
|
+
* @returns {object[]} { Target, IsImage, Line, Snippet } per reference.
|
|
286
|
+
*/
|
|
287
|
+
_extractReferences(pContent)
|
|
288
|
+
{
|
|
289
|
+
let tmpReferences = [];
|
|
290
|
+
let tmpLines = String(pContent).split(/\r?\n/);
|
|
291
|
+
let tmpInFence = false;
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
294
|
+
{
|
|
295
|
+
let tmpLine = tmpLines[i];
|
|
296
|
+
|
|
297
|
+
// Toggle on fenced code block boundaries (backtick fences, to
|
|
298
|
+
// match the content provider's markdown parser).
|
|
299
|
+
if (/^\s*`{3,}/.test(tmpLine))
|
|
300
|
+
{
|
|
301
|
+
tmpInFence = !tmpInFence;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (tmpInFence)
|
|
305
|
+
{
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Drop inline code spans so `[x](y)` in backticks is not a link.
|
|
310
|
+
let tmpScrubbed = tmpLine.replace(/`[^`]*`/g, ' ');
|
|
311
|
+
|
|
312
|
+
// Images: 
|
|
313
|
+
let tmpImageRegEx = /!\[[^\]]*\]\(([^)]+)\)/g;
|
|
314
|
+
let tmpImageMatch;
|
|
315
|
+
while ((tmpImageMatch = tmpImageRegEx.exec(tmpScrubbed)) !== null)
|
|
316
|
+
{
|
|
317
|
+
tmpReferences.push(
|
|
318
|
+
{
|
|
319
|
+
Target: tmpImageMatch[1],
|
|
320
|
+
IsImage: true,
|
|
321
|
+
Line: i + 1,
|
|
322
|
+
Snippet: tmpImageMatch[0]
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Links: [text](target) — the [^!] guard excludes image links.
|
|
327
|
+
let tmpLinkRegEx = /(^|[^!])(\[[^\]]*\]\(([^)]+)\))/g;
|
|
328
|
+
let tmpLinkMatch;
|
|
329
|
+
while ((tmpLinkMatch = tmpLinkRegEx.exec(tmpScrubbed)) !== null)
|
|
330
|
+
{
|
|
331
|
+
tmpReferences.push(
|
|
332
|
+
{
|
|
333
|
+
Target: tmpLinkMatch[3],
|
|
334
|
+
IsImage: false,
|
|
335
|
+
Line: i + 1,
|
|
336
|
+
Snippet: tmpLinkMatch[2]
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return tmpReferences;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Resolve a documentation link the way docuserve module-mode renders it,
|
|
346
|
+
* and report whether the target exists on disk.
|
|
347
|
+
*
|
|
348
|
+
* Every relative link — a .md page, a .html app, a directory, a media
|
|
349
|
+
* file — is resolved against the directory of the document that contains
|
|
350
|
+
* it, with ../ clamped at the docs root. A /-rooted link is resolved
|
|
351
|
+
* against the docs root.
|
|
352
|
+
*
|
|
353
|
+
* @param {string} pTarget - The cleaned link target.
|
|
354
|
+
* @param {string} pCurrentFile - Absolute path of the file the link is in.
|
|
355
|
+
* @param {string} pDocsRoot - The absolute docs root folder.
|
|
356
|
+
* @returns {object} { OK, Reason }
|
|
357
|
+
*/
|
|
358
|
+
_resolveDocumentLink(pTarget, pCurrentFile, pDocsRoot)
|
|
359
|
+
{
|
|
360
|
+
let tmpBaseDir = '';
|
|
361
|
+
let tmpHref = pTarget;
|
|
362
|
+
if (pTarget.charAt(0) === '/')
|
|
363
|
+
{
|
|
364
|
+
tmpHref = pTarget.replace(/^\/+/, '');
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
{
|
|
368
|
+
tmpBaseDir = libPath.relative(pDocsRoot, libPath.dirname(pCurrentFile)).split(libPath.sep).join('/');
|
|
369
|
+
}
|
|
370
|
+
let tmpResolved = libPath.join(pDocsRoot, resolveRelativeDocPath(tmpBaseDir, tmpHref));
|
|
371
|
+
|
|
372
|
+
if (fileExists(tmpResolved) || directoryExists(tmpResolved))
|
|
373
|
+
{
|
|
374
|
+
return { OK: true };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// An extensionless target may be a directory or an extensionless page
|
|
378
|
+
// route — docuserve's page route also tries <target>.md and
|
|
379
|
+
// <target>/README.md, so accept those too.
|
|
380
|
+
let tmpBareTarget = pTarget.replace(/[#?].*$/, '').replace(/\/+$/, '');
|
|
381
|
+
if (/\.md$/i.test(pTarget) || !/\.[a-z0-9]+$/i.test(tmpBareTarget))
|
|
382
|
+
{
|
|
383
|
+
if (fileExists(tmpResolved + '.md') || fileExists(libPath.join(tmpResolved, 'README.md')))
|
|
384
|
+
{
|
|
385
|
+
return { OK: true };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { OK: false, Reason: `no file at ${libPath.relative(pDocsRoot, tmpResolved) || '.'}` };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Resolve an image/media reference the way docuserve's image resolver does
|
|
394
|
+
* — relative to the containing document's directory, or docs-root-relative
|
|
395
|
+
* when the target is /-rooted — and report whether it exists on disk.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} pTarget - The cleaned image target.
|
|
398
|
+
* @param {string} pCurrentFile - Absolute path of the file the ref is in.
|
|
399
|
+
* @param {string} pDocsRoot - The absolute docs root folder.
|
|
400
|
+
* @returns {object} { OK, Reason }
|
|
401
|
+
*/
|
|
402
|
+
_resolveImageReference(pTarget, pCurrentFile, pDocsRoot)
|
|
403
|
+
{
|
|
404
|
+
let tmpResolved;
|
|
405
|
+
if (pTarget.charAt(0) === '/')
|
|
406
|
+
{
|
|
407
|
+
tmpResolved = libPath.normalize(libPath.join(pDocsRoot, pTarget.replace(/^\/+/, '')));
|
|
408
|
+
}
|
|
409
|
+
else
|
|
410
|
+
{
|
|
411
|
+
tmpResolved = libPath.normalize(libPath.join(libPath.dirname(pCurrentFile), pTarget));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if ((tmpResolved !== pDocsRoot) && (tmpResolved.indexOf(pDocsRoot + libPath.sep) !== 0))
|
|
415
|
+
{
|
|
416
|
+
return { OK: false, Reason: 'resolves outside the docs root' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (fileExists(tmpResolved))
|
|
420
|
+
{
|
|
421
|
+
return { OK: true };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { OK: false, Reason: `no file at ${libPath.relative(pDocsRoot, tmpResolved) || '.'}` };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = DocuserveCommandCheckLinks;
|
|
@@ -275,7 +275,7 @@ class DocuserveCommandStageExamples extends libCommandLineCommand
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
// Regenerate the marker-delimited index +
|
|
278
|
+
// Regenerate the marker-delimited examples index + the splash region.
|
|
279
279
|
try
|
|
280
280
|
{
|
|
281
281
|
this._writeExamplesIndex(tmpStaged, tmpDocsFolder);
|
|
@@ -287,11 +287,11 @@ class DocuserveCommandStageExamples extends libCommandLineCommand
|
|
|
287
287
|
|
|
288
288
|
try
|
|
289
289
|
{
|
|
290
|
-
this.
|
|
290
|
+
this._writeCoverExamples(tmpStaged, tmpDocsFolder);
|
|
291
291
|
}
|
|
292
292
|
catch (pError)
|
|
293
293
|
{
|
|
294
|
-
this.log.warn(`Could not update
|
|
294
|
+
this.log.warn(`Could not update splash examples region: ${pError.message}`);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
this.log.info(`Staged ${tmpStaged.length} example application(s): ${tmpStaged.map((pEx) => pEx.Name).join(', ')}`);
|
|
@@ -492,7 +492,10 @@ class DocuserveCommandStageExamples extends libCommandLineCommand
|
|
|
492
492
|
let tmpTitle = pExample.Flag.Title || tmpName;
|
|
493
493
|
let tmpSummary = pExample.Flag.Summary || pExample.Package.description || '';
|
|
494
494
|
|
|
495
|
-
|
|
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 = `> **[▶ Launch the live app](index.html)** — runs in your browser, opens in a new tab.`;
|
|
496
499
|
|
|
497
500
|
let tmpText;
|
|
498
501
|
if (libFS.existsSync(tmpReadmePath))
|
|
@@ -536,7 +539,9 @@ class DocuserveCommandStageExamples extends libCommandLineCommand
|
|
|
536
539
|
let tmpRows = pStaged.map((pEx) =>
|
|
537
540
|
{
|
|
538
541
|
let tmpComplexity = escapeTableCell(pEx.Complexity) || '—';
|
|
539
|
-
|
|
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)} | [▶ Launch](${mdSafeUrl(pEx.Name + '/index.html')}) |`;
|
|
540
545
|
});
|
|
541
546
|
let tmpInner = [
|
|
542
547
|
'| Example | Complexity | Summary | Live |',
|
|
@@ -558,43 +563,43 @@ class DocuserveCommandStageExamples extends libCommandLineCommand
|
|
|
558
563
|
}
|
|
559
564
|
|
|
560
565
|
/**
|
|
561
|
-
* Maintain the generated
|
|
562
|
-
*
|
|
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.
|
|
563
570
|
*
|
|
564
571
|
* @param {object[]} pStaged - Staged example metadata.
|
|
565
572
|
* @param {string} pDocsFolder - The target documentation folder.
|
|
566
573
|
*/
|
|
567
|
-
|
|
574
|
+
_writeCoverExamples(pStaged, pDocsFolder)
|
|
568
575
|
{
|
|
569
|
-
let
|
|
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
|
+
}
|
|
570
582
|
|
|
571
|
-
let
|
|
583
|
+
let tmpRows = pStaged.map((pEx) =>
|
|
572
584
|
{
|
|
573
|
-
let
|
|
574
|
-
return
|
|
585
|
+
let tmpComplexity = escapeTableCell(pEx.Complexity) || '—';
|
|
586
|
+
return `| [${escapeTableCell(pEx.Title)}](${mdSafeUrl('examples/' + pEx.Name + '/README.md')}) | ${tmpComplexity} | [▶ Launch](${mdSafeUrl('examples/' + pEx.Name + '/index.html')}) |`;
|
|
575
587
|
});
|
|
576
|
-
let tmpInner =
|
|
588
|
+
let tmpInner = [
|
|
589
|
+
'| Example | Complexity | Launch |',
|
|
590
|
+
'|---------|------------|--------|'
|
|
591
|
+
].concat(tmpRows).join('\n');
|
|
577
592
|
|
|
578
|
-
let tmpText = libFS.
|
|
579
|
-
? libFS.readFileSync(tmpReadmePath, 'utf8')
|
|
580
|
-
: '# Documentation\n';
|
|
593
|
+
let tmpText = libFS.readFileSync(tmpCoverPath, 'utf8');
|
|
581
594
|
|
|
582
595
|
tmpText = replaceRegion(tmpText, _Marker.QuickLinksStart, _Marker.QuickLinksEnd, tmpInner,
|
|
583
596
|
(pBody, pRegion) =>
|
|
584
597
|
{
|
|
585
|
-
//
|
|
586
|
-
|
|
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';
|
|
598
|
+
// No markers yet — append the region at the end of the cover.
|
|
599
|
+
return pBody.replace(/\s*$/, '') + '\n\n' + pRegion + '\n';
|
|
595
600
|
});
|
|
596
601
|
|
|
597
|
-
libFS.writeFileSync(
|
|
602
|
+
libFS.writeFileSync(tmpCoverPath, tmpText);
|
|
598
603
|
}
|
|
599
604
|
}
|
|
600
605
|
|