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.
- package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +1183 -992
- package/dist/pict-docuserve.js +610 -370
- 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 +14 -1
- package/source/cli/Docuserve-CLI-Program.js +3 -1
- package/source/cli/commands/Docuserve-Command-CheckLinks.js +428 -0
- package/source/cli/commands/Docuserve-Command-StageExamples.js +606 -0
- package/source/providers/Pict-Provider-Docuserve-Documentation.js +245 -15
- package/source/views/PictView-Docuserve-Splash.js +126 -0
- 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.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": {
|
|
@@ -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.4",
|
|
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.3",
|
|
46
|
+
"quackage": "^1.2.4"
|
|
47
47
|
},
|
|
48
48
|
"copyFilesSettings": {
|
|
49
49
|
"whenFileExists": "overwrite"
|
|
@@ -573,10 +573,23 @@ 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
|
+
}, '', '', tmpReadmePath);
|
|
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);
|
|
579
|
-
});
|
|
592
|
+
}, '', '', tmpPath);
|
|
580
593
|
}
|
|
581
594
|
|
|
582
595
|
/**
|
|
@@ -11,7 +11,9 @@ 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'),
|
|
16
|
+
require('./commands/Docuserve-Command-CheckLinks.js')
|
|
15
17
|
]);
|
|
16
18
|
|
|
17
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;
|