pict-docuserve 0.0.1
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/LICENSE +21 -0
- package/build-site.js +172 -0
- package/css/docuserve.css +73 -0
- package/dist/css/docuserve.css +73 -0
- package/dist/index.html +32 -0
- package/dist/js/pict.compatible.js +7793 -0
- package/dist/js/pict.compatible.js.map +1 -0
- package/dist/js/pict.compatible.min.js +12 -0
- package/dist/js/pict.compatible.min.js.map +1 -0
- package/dist/js/pict.js +7792 -0
- package/dist/js/pict.js.map +1 -0
- package/dist/js/pict.min.js +12 -0
- package/dist/js/pict.min.js.map +1 -0
- package/dist/pict-docuserve.compatible.js +5148 -0
- package/dist/pict-docuserve.compatible.js.map +1 -0
- package/dist/pict-docuserve.compatible.min.js +2 -0
- package/dist/pict-docuserve.compatible.min.js.map +1 -0
- package/dist/pict-docuserve.js +4670 -0
- package/dist/pict-docuserve.js.map +1 -0
- package/dist/pict-docuserve.min.js +2 -0
- package/dist/pict-docuserve.min.js.map +1 -0
- package/html/index.html +32 -0
- package/package.json +52 -0
- package/source/Pict-Application-Docuserve-Configuration.json +15 -0
- package/source/Pict-Application-Docuserve.js +246 -0
- package/source/cli/Docuserve-CLI-Program.js +16 -0
- package/source/cli/Docuserve-CLI-Run.js +3 -0
- package/source/cli/commands/Docuserve-Command-Inject.js +127 -0
- package/source/cli/commands/Docuserve-Command-Serve.js +135 -0
- package/source/providers/Pict-Provider-Docuserve-Documentation.js +1156 -0
- package/source/providers/PictRouter-Docuserve-Configuration.json +26 -0
- package/source/views/PictView-Docuserve-Content.js +208 -0
- package/source/views/PictView-Docuserve-Layout.js +105 -0
- package/source/views/PictView-Docuserve-Sidebar.js +362 -0
- package/source/views/PictView-Docuserve-Splash.js +264 -0
- package/source/views/PictView-Docuserve-TopBar.js +213 -0
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
const libPictProvider = require('pict-provider');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Documentation Provider for Docuserve
|
|
5
|
+
*
|
|
6
|
+
* Loads the Indoctrinate-generated catalog and keyword index,
|
|
7
|
+
* fetches markdown documents from local paths or raw GitHub URLs,
|
|
8
|
+
* and parses them into HTML for rendering.
|
|
9
|
+
*/
|
|
10
|
+
class DocuserveDocumentationProvider extends libPictProvider
|
|
11
|
+
{
|
|
12
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
13
|
+
{
|
|
14
|
+
super(pFable, pOptions, pServiceHash);
|
|
15
|
+
|
|
16
|
+
this._Catalog = null;
|
|
17
|
+
this._ContentCache = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load all documentation data sources: catalog, cover.md, _sidebar.md.
|
|
22
|
+
*
|
|
23
|
+
* Loads the catalog first (it provides the fallback data), then attempts
|
|
24
|
+
* to load cover.md and _sidebar.md in parallel. If those markdown files
|
|
25
|
+
* exist they drive the splash and sidebar views; otherwise the catalog
|
|
26
|
+
* data is used as a fallback.
|
|
27
|
+
*
|
|
28
|
+
* @param {Function} fCallback - Callback when all loading is complete
|
|
29
|
+
*/
|
|
30
|
+
loadCatalog(fCallback)
|
|
31
|
+
{
|
|
32
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
33
|
+
|
|
34
|
+
let tmpCatalogURL = this.pict.AppData.Docuserve.CatalogURL || 'retold-catalog.json';
|
|
35
|
+
|
|
36
|
+
fetch(tmpCatalogURL)
|
|
37
|
+
.then((pResponse) =>
|
|
38
|
+
{
|
|
39
|
+
if (!pResponse.ok)
|
|
40
|
+
{
|
|
41
|
+
this.log.warn(`Docuserve: Could not load catalog from [${tmpCatalogURL}].`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return pResponse.json();
|
|
45
|
+
})
|
|
46
|
+
.then((pCatalog) =>
|
|
47
|
+
{
|
|
48
|
+
if (pCatalog)
|
|
49
|
+
{
|
|
50
|
+
this._Catalog = pCatalog;
|
|
51
|
+
this.pict.AppData.Docuserve.Catalog = pCatalog;
|
|
52
|
+
this.pict.AppData.Docuserve.CatalogLoaded = true;
|
|
53
|
+
|
|
54
|
+
// Build sidebar navigation data from the catalog as default
|
|
55
|
+
this.buildSidebarData(pCatalog);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Now try to load cover.md, _sidebar.md, _topbar.md, and errorpage.md in parallel
|
|
59
|
+
let tmpPending = 4;
|
|
60
|
+
let tmpFinish = () =>
|
|
61
|
+
{
|
|
62
|
+
tmpPending--;
|
|
63
|
+
if (tmpPending <= 0)
|
|
64
|
+
{
|
|
65
|
+
return tmpCallback();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.loadCover(tmpFinish);
|
|
70
|
+
this.loadSidebar(tmpFinish);
|
|
71
|
+
this.loadTopbar(tmpFinish);
|
|
72
|
+
this.loadErrorPage(tmpFinish);
|
|
73
|
+
})
|
|
74
|
+
.catch((pError) =>
|
|
75
|
+
{
|
|
76
|
+
this.log.warn(`Docuserve: Error loading catalog: ${pError}`);
|
|
77
|
+
return tmpCallback();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch and parse cover.md into structured data for the splash view.
|
|
83
|
+
*
|
|
84
|
+
* The expected cover.md format follows the docsify convention:
|
|
85
|
+
* # Title
|
|
86
|
+
* > Tagline
|
|
87
|
+
* Description paragraph text.
|
|
88
|
+
* - **Group** — description
|
|
89
|
+
* [Link Text](url)
|
|
90
|
+
*
|
|
91
|
+
* Parsed result stored in this.pict.AppData.Docuserve.Cover:
|
|
92
|
+
* { Title, Tagline, Description, Highlights: [{Label, Text}], Actions: [{Text, Href}] }
|
|
93
|
+
*
|
|
94
|
+
* @param {Function} fCallback - Callback when done
|
|
95
|
+
*/
|
|
96
|
+
loadCover(fCallback)
|
|
97
|
+
{
|
|
98
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
99
|
+
let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
100
|
+
|
|
101
|
+
fetch(tmpDocsBase + 'cover.md')
|
|
102
|
+
.then((pResponse) =>
|
|
103
|
+
{
|
|
104
|
+
if (!pResponse.ok)
|
|
105
|
+
{
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return pResponse.text();
|
|
109
|
+
})
|
|
110
|
+
.then((pMarkdown) =>
|
|
111
|
+
{
|
|
112
|
+
if (!pMarkdown)
|
|
113
|
+
{
|
|
114
|
+
this.log.info('Docuserve: No cover.md found; splash will use catalog data.');
|
|
115
|
+
return tmpCallback();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.pict.AppData.Docuserve.Cover = this.parseCover(pMarkdown);
|
|
119
|
+
this.pict.AppData.Docuserve.CoverLoaded = true;
|
|
120
|
+
return tmpCallback();
|
|
121
|
+
})
|
|
122
|
+
.catch((pError) =>
|
|
123
|
+
{
|
|
124
|
+
this.log.warn(`Docuserve: Error loading cover.md: ${pError}`);
|
|
125
|
+
return tmpCallback();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse cover.md markdown text into a structured object.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} pMarkdown - Raw cover.md content
|
|
133
|
+
* @returns {Object} Parsed cover data
|
|
134
|
+
*/
|
|
135
|
+
parseCover(pMarkdown)
|
|
136
|
+
{
|
|
137
|
+
let tmpCover = {
|
|
138
|
+
Title: '',
|
|
139
|
+
Tagline: '',
|
|
140
|
+
Description: '',
|
|
141
|
+
Highlights: [],
|
|
142
|
+
Actions: []
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let tmpLines = pMarkdown.split('\n');
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
148
|
+
{
|
|
149
|
+
let tmpLine = tmpLines[i].trim();
|
|
150
|
+
|
|
151
|
+
if (!tmpLine)
|
|
152
|
+
{
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Heading — the title
|
|
157
|
+
let tmpHeadingMatch = tmpLine.match(/^#+\s+(.+)/);
|
|
158
|
+
if (tmpHeadingMatch)
|
|
159
|
+
{
|
|
160
|
+
tmpCover.Title = tmpHeadingMatch[1].trim();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Blockquote — the tagline
|
|
165
|
+
let tmpBlockquoteMatch = tmpLine.match(/^>\s*(.*)/);
|
|
166
|
+
if (tmpBlockquoteMatch)
|
|
167
|
+
{
|
|
168
|
+
tmpCover.Tagline = tmpBlockquoteMatch[1].trim();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Bullet list — highlights (e.g. "- **Fable** — Core ecosystem, DI, config")
|
|
173
|
+
let tmpBulletMatch = tmpLine.match(/^[-*+]\s+(.*)/);
|
|
174
|
+
if (tmpBulletMatch)
|
|
175
|
+
{
|
|
176
|
+
let tmpBulletContent = tmpBulletMatch[1];
|
|
177
|
+
// Try to split on bold label: **Label** — rest
|
|
178
|
+
let tmpLabelMatch = tmpBulletContent.match(/^\*\*([^*]+)\*\*\s*[-—:]\s*(.*)/);
|
|
179
|
+
if (tmpLabelMatch)
|
|
180
|
+
{
|
|
181
|
+
tmpCover.Highlights.push({ Label: tmpLabelMatch[1].trim(), Text: tmpLabelMatch[2].trim() });
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
{
|
|
185
|
+
tmpCover.Highlights.push({ Label: '', Text: tmpBulletContent.trim() });
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Bare link — action button (e.g. "[Get Started](getting-started.md)")
|
|
191
|
+
let tmpLinkMatch = tmpLine.match(/^\[([^\]]+)\]\(([^)]+)\)\s*$/);
|
|
192
|
+
if (tmpLinkMatch)
|
|
193
|
+
{
|
|
194
|
+
tmpCover.Actions.push({ Text: tmpLinkMatch[1].trim(), Href: tmpLinkMatch[2].trim() });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Otherwise it's description text
|
|
199
|
+
if (!tmpCover.Description)
|
|
200
|
+
{
|
|
201
|
+
tmpCover.Description = tmpLine;
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
{
|
|
205
|
+
tmpCover.Description += ' ' + tmpLine;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return tmpCover;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fetch and parse _sidebar.md into structured navigation data.
|
|
214
|
+
*
|
|
215
|
+
* The expected _sidebar.md format follows the docsify convention:
|
|
216
|
+
* - [Home](/)
|
|
217
|
+
* - Group Title
|
|
218
|
+
* - [module-name](/group/module/)
|
|
219
|
+
* - [Group Title](group.md)
|
|
220
|
+
* - [module-name](/group/module/)
|
|
221
|
+
*
|
|
222
|
+
* If _sidebar.md is successfully loaded and parsed, its data replaces
|
|
223
|
+
* the catalog-inferred SidebarGroups in AppData.
|
|
224
|
+
*
|
|
225
|
+
* @param {Function} fCallback - Callback when done
|
|
226
|
+
*/
|
|
227
|
+
loadSidebar(fCallback)
|
|
228
|
+
{
|
|
229
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
230
|
+
let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
231
|
+
|
|
232
|
+
fetch(tmpDocsBase + '_sidebar.md')
|
|
233
|
+
.then((pResponse) =>
|
|
234
|
+
{
|
|
235
|
+
if (!pResponse.ok)
|
|
236
|
+
{
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return pResponse.text();
|
|
240
|
+
})
|
|
241
|
+
.then((pMarkdown) =>
|
|
242
|
+
{
|
|
243
|
+
if (!pMarkdown)
|
|
244
|
+
{
|
|
245
|
+
this.log.info('Docuserve: No _sidebar.md found; sidebar will use catalog data.');
|
|
246
|
+
return tmpCallback();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let tmpSidebarData = this.parseSidebarMarkdown(pMarkdown);
|
|
250
|
+
if (tmpSidebarData && tmpSidebarData.length > 0)
|
|
251
|
+
{
|
|
252
|
+
this.pict.AppData.Docuserve.SidebarGroups = tmpSidebarData;
|
|
253
|
+
this.pict.AppData.Docuserve.SidebarLoaded = true;
|
|
254
|
+
}
|
|
255
|
+
return tmpCallback();
|
|
256
|
+
})
|
|
257
|
+
.catch((pError) =>
|
|
258
|
+
{
|
|
259
|
+
this.log.warn(`Docuserve: Error loading _sidebar.md: ${pError}`);
|
|
260
|
+
return tmpCallback();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Fetch and parse _topbar.md into structured data for the top bar view.
|
|
266
|
+
*
|
|
267
|
+
* The expected _topbar.md format:
|
|
268
|
+
* # Brand Name
|
|
269
|
+
* - [Link Text](url)
|
|
270
|
+
* - [Link Text](url)
|
|
271
|
+
*
|
|
272
|
+
* The heading becomes the brand/title shown on the left. List items become
|
|
273
|
+
* navigation links. External links (starting with http) render on the
|
|
274
|
+
* right side; internal links render in the centre nav area.
|
|
275
|
+
*
|
|
276
|
+
* Parsed result stored in this.pict.AppData.Docuserve.TopBar:
|
|
277
|
+
* { Brand, NavLinks: [{Text, Href, External}], ExternalLinks: [{Text, Href}] }
|
|
278
|
+
*
|
|
279
|
+
* @param {Function} fCallback - Callback when done
|
|
280
|
+
*/
|
|
281
|
+
loadTopbar(fCallback)
|
|
282
|
+
{
|
|
283
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
284
|
+
let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
285
|
+
|
|
286
|
+
fetch(tmpDocsBase + '_topbar.md')
|
|
287
|
+
.then((pResponse) =>
|
|
288
|
+
{
|
|
289
|
+
if (!pResponse.ok)
|
|
290
|
+
{
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return pResponse.text();
|
|
294
|
+
})
|
|
295
|
+
.then((pMarkdown) =>
|
|
296
|
+
{
|
|
297
|
+
if (!pMarkdown)
|
|
298
|
+
{
|
|
299
|
+
this.log.info('Docuserve: No _topbar.md found; top bar will use defaults.');
|
|
300
|
+
return tmpCallback();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.pict.AppData.Docuserve.TopBar = this.parseTopbar(pMarkdown);
|
|
304
|
+
this.pict.AppData.Docuserve.TopBarLoaded = true;
|
|
305
|
+
return tmpCallback();
|
|
306
|
+
})
|
|
307
|
+
.catch((pError) =>
|
|
308
|
+
{
|
|
309
|
+
this.log.warn(`Docuserve: Error loading _topbar.md: ${pError}`);
|
|
310
|
+
return tmpCallback();
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Parse _topbar.md markdown text into a structured object.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} pMarkdown - Raw _topbar.md content
|
|
318
|
+
* @returns {Object} Parsed top bar data { Brand, NavLinks, ExternalLinks }
|
|
319
|
+
*/
|
|
320
|
+
parseTopbar(pMarkdown)
|
|
321
|
+
{
|
|
322
|
+
let tmpTopBar = {
|
|
323
|
+
Brand: '',
|
|
324
|
+
NavLinks: [],
|
|
325
|
+
ExternalLinks: []
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
let tmpLines = pMarkdown.split('\n');
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
331
|
+
{
|
|
332
|
+
let tmpLine = tmpLines[i].trim();
|
|
333
|
+
|
|
334
|
+
if (!tmpLine)
|
|
335
|
+
{
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Heading — the brand name
|
|
340
|
+
let tmpHeadingMatch = tmpLine.match(/^#+\s+(.+)/);
|
|
341
|
+
if (tmpHeadingMatch)
|
|
342
|
+
{
|
|
343
|
+
tmpTopBar.Brand = tmpHeadingMatch[1].trim();
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Bullet list item with link
|
|
348
|
+
let tmpBulletMatch = tmpLine.match(/^[-*+]\s+(.*)/);
|
|
349
|
+
if (tmpBulletMatch)
|
|
350
|
+
{
|
|
351
|
+
let tmpContent = tmpBulletMatch[1].trim();
|
|
352
|
+
let tmpLinkMatch = tmpContent.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
353
|
+
|
|
354
|
+
if (tmpLinkMatch)
|
|
355
|
+
{
|
|
356
|
+
let tmpText = tmpLinkMatch[1].trim();
|
|
357
|
+
let tmpHref = tmpLinkMatch[2].trim();
|
|
358
|
+
|
|
359
|
+
// External links (http/https) go to the right side
|
|
360
|
+
if (tmpHref.match(/^https?:\/\//))
|
|
361
|
+
{
|
|
362
|
+
tmpTopBar.ExternalLinks.push({ Text: tmpText, Href: tmpHref });
|
|
363
|
+
}
|
|
364
|
+
else
|
|
365
|
+
{
|
|
366
|
+
// Internal link — convert to hash route
|
|
367
|
+
let tmpRoute = this.convertSidebarLink(tmpHref);
|
|
368
|
+
tmpTopBar.NavLinks.push({ Text: tmpText, Href: tmpRoute });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return tmpTopBar;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Fetch and parse errorpage.md into HTML for use as a custom error page.
|
|
380
|
+
*
|
|
381
|
+
* The errorpage.md is a standard markdown file. If it contains the
|
|
382
|
+
* placeholder `{{path}}` anywhere in its source, that token will be
|
|
383
|
+
* replaced with the actual requested path at display time (via
|
|
384
|
+
* getErrorPageHTML).
|
|
385
|
+
*
|
|
386
|
+
* @param {Function} fCallback - Callback when done
|
|
387
|
+
*/
|
|
388
|
+
loadErrorPage(fCallback)
|
|
389
|
+
{
|
|
390
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
391
|
+
let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
392
|
+
|
|
393
|
+
fetch(tmpDocsBase + 'errorpage.md')
|
|
394
|
+
.then((pResponse) =>
|
|
395
|
+
{
|
|
396
|
+
if (!pResponse.ok)
|
|
397
|
+
{
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return pResponse.text();
|
|
401
|
+
})
|
|
402
|
+
.then((pMarkdown) =>
|
|
403
|
+
{
|
|
404
|
+
if (!pMarkdown)
|
|
405
|
+
{
|
|
406
|
+
this.log.info('Docuserve: No errorpage.md found; errors will use default page.');
|
|
407
|
+
return tmpCallback();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.pict.AppData.Docuserve.ErrorPageHTML = this.parseMarkdown(pMarkdown);
|
|
411
|
+
this.pict.AppData.Docuserve.ErrorPageLoaded = true;
|
|
412
|
+
return tmpCallback();
|
|
413
|
+
})
|
|
414
|
+
.catch((pError) =>
|
|
415
|
+
{
|
|
416
|
+
this.log.warn(`Docuserve: Error loading errorpage.md: ${pError}`);
|
|
417
|
+
return tmpCallback();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get the error page HTML for a given requested path.
|
|
423
|
+
*
|
|
424
|
+
* If a custom errorpage.md was loaded, its parsed HTML is returned with
|
|
425
|
+
* the `{{path}}` placeholder replaced by the actual requested path.
|
|
426
|
+
* Otherwise a default not-found HTML block is returned.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} pRequestedPath - The path that was not found
|
|
429
|
+
* @returns {string} HTML to display
|
|
430
|
+
*/
|
|
431
|
+
getErrorPageHTML(pRequestedPath)
|
|
432
|
+
{
|
|
433
|
+
let tmpPath = pRequestedPath || 'unknown';
|
|
434
|
+
|
|
435
|
+
if (this.pict.AppData.Docuserve.ErrorPageLoaded && this.pict.AppData.Docuserve.ErrorPageHTML)
|
|
436
|
+
{
|
|
437
|
+
// Replace the {{path}} placeholder with the actual requested path
|
|
438
|
+
return this.pict.AppData.Docuserve.ErrorPageHTML.replace(/\{\{path\}\}/g, this.escapeHTML(tmpPath));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Default fallback
|
|
442
|
+
return '<div class="docuserve-not-found">'
|
|
443
|
+
+ '<h2>Page Not Found</h2>'
|
|
444
|
+
+ '<p>The document <code>' + this.escapeHTML(tmpPath) + '</code> could not be loaded.</p>'
|
|
445
|
+
+ '<p><a href="#/Home">Return to the home page</a></p>'
|
|
446
|
+
+ '</div>';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Parse _sidebar.md into the SidebarGroups format the sidebar view consumes.
|
|
451
|
+
*
|
|
452
|
+
* Returns an array of group objects:
|
|
453
|
+
* [{ Name, Key, Route, Modules: [{ Name, HasDocs, Group, Route }] }]
|
|
454
|
+
*
|
|
455
|
+
* Top-level items (no indent) become groups. Indented child items become
|
|
456
|
+
* modules within the preceding group. The special "Home" entry is stored
|
|
457
|
+
* as a group with no modules.
|
|
458
|
+
*
|
|
459
|
+
* @param {string} pMarkdown - Raw _sidebar.md content
|
|
460
|
+
* @returns {Array} Parsed sidebar groups
|
|
461
|
+
*/
|
|
462
|
+
parseSidebarMarkdown(pMarkdown)
|
|
463
|
+
{
|
|
464
|
+
let tmpGroups = [];
|
|
465
|
+
let tmpCurrentGroup = null;
|
|
466
|
+
let tmpLines = pMarkdown.split('\n');
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
469
|
+
{
|
|
470
|
+
let tmpLine = tmpLines[i];
|
|
471
|
+
|
|
472
|
+
if (!tmpLine.trim())
|
|
473
|
+
{
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Detect indent level: child items have 2+ leading spaces
|
|
478
|
+
let tmpIndentMatch = tmpLine.match(/^(\s*)/);
|
|
479
|
+
let tmpIndent = tmpIndentMatch ? tmpIndentMatch[1].length : 0;
|
|
480
|
+
let tmpContent = tmpLine.trim();
|
|
481
|
+
|
|
482
|
+
// Must start with a list marker
|
|
483
|
+
let tmpListMatch = tmpContent.match(/^[-*+]\s+(.*)/);
|
|
484
|
+
if (!tmpListMatch)
|
|
485
|
+
{
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let tmpItemContent = tmpListMatch[1].trim();
|
|
490
|
+
|
|
491
|
+
// Parse link if present: [Text](href)
|
|
492
|
+
let tmpLinkMatch = tmpItemContent.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
493
|
+
|
|
494
|
+
if (tmpIndent < 2)
|
|
495
|
+
{
|
|
496
|
+
// Top-level item — this is a group header or standalone link
|
|
497
|
+
if (tmpLinkMatch)
|
|
498
|
+
{
|
|
499
|
+
let tmpName = tmpLinkMatch[1].trim();
|
|
500
|
+
let tmpHref = tmpLinkMatch[2].trim();
|
|
501
|
+
|
|
502
|
+
// Derive a group key from the href or name
|
|
503
|
+
let tmpKey = this.deriveGroupKey(tmpName, tmpHref);
|
|
504
|
+
let tmpRoute = this.convertSidebarLink(tmpHref);
|
|
505
|
+
|
|
506
|
+
tmpCurrentGroup = {
|
|
507
|
+
Name: tmpName,
|
|
508
|
+
Key: tmpKey,
|
|
509
|
+
Route: tmpRoute,
|
|
510
|
+
Modules: []
|
|
511
|
+
};
|
|
512
|
+
tmpGroups.push(tmpCurrentGroup);
|
|
513
|
+
}
|
|
514
|
+
else
|
|
515
|
+
{
|
|
516
|
+
// Plain text group header (no link)
|
|
517
|
+
let tmpName = tmpItemContent;
|
|
518
|
+
let tmpKey = tmpName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
519
|
+
|
|
520
|
+
tmpCurrentGroup = {
|
|
521
|
+
Name: tmpName,
|
|
522
|
+
Key: tmpKey,
|
|
523
|
+
Route: '',
|
|
524
|
+
Modules: []
|
|
525
|
+
};
|
|
526
|
+
tmpGroups.push(tmpCurrentGroup);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else if (tmpCurrentGroup)
|
|
530
|
+
{
|
|
531
|
+
// Indented item — this is a module within the current group
|
|
532
|
+
if (tmpLinkMatch)
|
|
533
|
+
{
|
|
534
|
+
let tmpModuleName = tmpLinkMatch[1].trim();
|
|
535
|
+
let tmpModuleHref = tmpLinkMatch[2].trim();
|
|
536
|
+
let tmpModuleRoute = this.convertSidebarLink(tmpModuleHref);
|
|
537
|
+
|
|
538
|
+
tmpCurrentGroup.Modules.push({
|
|
539
|
+
Name: tmpModuleName,
|
|
540
|
+
HasDocs: true,
|
|
541
|
+
Group: tmpCurrentGroup.Key,
|
|
542
|
+
Route: tmpModuleRoute
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
else
|
|
546
|
+
{
|
|
547
|
+
// Plain text child entry (no docs link)
|
|
548
|
+
tmpCurrentGroup.Modules.push({
|
|
549
|
+
Name: tmpItemContent,
|
|
550
|
+
HasDocs: false,
|
|
551
|
+
Group: tmpCurrentGroup.Key,
|
|
552
|
+
Route: ''
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return tmpGroups;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Convert a docsify-style sidebar link href into a docuserve hash route.
|
|
563
|
+
*
|
|
564
|
+
* Handles these forms:
|
|
565
|
+
* / -> #/Home
|
|
566
|
+
* /group/module/ -> #/doc/group/module
|
|
567
|
+
* /group/module/path.md -> #/doc/group/module/path.md
|
|
568
|
+
* something.md -> #/page/something
|
|
569
|
+
*
|
|
570
|
+
* @param {string} pHref - The original sidebar link href
|
|
571
|
+
* @returns {string} The converted hash route
|
|
572
|
+
*/
|
|
573
|
+
convertSidebarLink(pHref)
|
|
574
|
+
{
|
|
575
|
+
if (!pHref)
|
|
576
|
+
{
|
|
577
|
+
return '';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Root home link
|
|
581
|
+
if (pHref === '/')
|
|
582
|
+
{
|
|
583
|
+
return '#/Home';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Strip leading/trailing slashes for parsing
|
|
587
|
+
let tmpPath = pHref.replace(/^\//, '').replace(/\/$/, '');
|
|
588
|
+
|
|
589
|
+
if (!tmpPath)
|
|
590
|
+
{
|
|
591
|
+
return '#/Home';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let tmpParts = tmpPath.split('/');
|
|
595
|
+
|
|
596
|
+
// Check if it's a module path (group/module)
|
|
597
|
+
if (tmpParts.length >= 2)
|
|
598
|
+
{
|
|
599
|
+
let tmpGroupKeys = ['fable', 'meadow', 'orator', 'pict', 'utility'];
|
|
600
|
+
if (tmpGroupKeys.indexOf(tmpParts[0]) >= 0)
|
|
601
|
+
{
|
|
602
|
+
return '#/doc/' + tmpPath;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Local page reference
|
|
607
|
+
if (tmpPath.match(/\.md$/))
|
|
608
|
+
{
|
|
609
|
+
return '#/page/' + tmpPath.replace(/\.md$/, '');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return '#/page/' + tmpPath;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Derive a short group key from a sidebar group name or href.
|
|
617
|
+
*
|
|
618
|
+
* @param {string} pName - The display name (e.g. "Fable — Core Ecosystem")
|
|
619
|
+
* @param {string} pHref - The link href (e.g. "fable.md")
|
|
620
|
+
* @returns {string} A short key (e.g. "fable")
|
|
621
|
+
*/
|
|
622
|
+
deriveGroupKey(pName, pHref)
|
|
623
|
+
{
|
|
624
|
+
// Try href first — "fable.md" -> "fable"
|
|
625
|
+
if (pHref && pHref !== '/')
|
|
626
|
+
{
|
|
627
|
+
let tmpFromHref = pHref.replace(/^\//, '').replace(/\.md$/, '').replace(/\/$/, '');
|
|
628
|
+
if (tmpFromHref && !tmpFromHref.includes('/'))
|
|
629
|
+
{
|
|
630
|
+
return tmpFromHref.toLowerCase();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Fall back to first word of name lowercased
|
|
635
|
+
let tmpFirstWord = pName.split(/[\s—\-:]+/)[0];
|
|
636
|
+
return tmpFirstWord.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Build structured sidebar data from the catalog for the sidebar view.
|
|
641
|
+
*
|
|
642
|
+
* @param {Object} pCatalog - The parsed retold-catalog.json
|
|
643
|
+
*/
|
|
644
|
+
buildSidebarData(pCatalog)
|
|
645
|
+
{
|
|
646
|
+
let tmpSidebarGroups = [];
|
|
647
|
+
|
|
648
|
+
for (let i = 0; i < pCatalog.Groups.length; i++)
|
|
649
|
+
{
|
|
650
|
+
let tmpGroup = pCatalog.Groups[i];
|
|
651
|
+
let tmpGroupEntry = {
|
|
652
|
+
Name: tmpGroup.Name,
|
|
653
|
+
Key: tmpGroup.Key,
|
|
654
|
+
Description: tmpGroup.Description,
|
|
655
|
+
Modules: []
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
for (let j = 0; j < tmpGroup.Modules.length; j++)
|
|
659
|
+
{
|
|
660
|
+
let tmpModule = tmpGroup.Modules[j];
|
|
661
|
+
tmpGroupEntry.Modules.push({
|
|
662
|
+
Name: tmpModule.Name,
|
|
663
|
+
HasDocs: tmpModule.HasDocs,
|
|
664
|
+
Group: tmpGroup.Key,
|
|
665
|
+
Route: '#/doc/' + tmpGroup.Key + '/' + tmpModule.Name
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
tmpSidebarGroups.push(tmpGroupEntry);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
this.pict.AppData.Docuserve.SidebarGroups = tmpSidebarGroups;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Resolve a document URL from group/module/path to a fetchable URL.
|
|
677
|
+
*
|
|
678
|
+
* @param {string} pGroup - The group key (e.g. 'fable')
|
|
679
|
+
* @param {string} pModule - The module name (e.g. 'fable')
|
|
680
|
+
* @param {string} pPath - The document path within the module docs (e.g. 'README.md')
|
|
681
|
+
* @returns {string} The resolved URL
|
|
682
|
+
*/
|
|
683
|
+
resolveDocumentURL(pGroup, pModule, pPath)
|
|
684
|
+
{
|
|
685
|
+
if (!this._Catalog)
|
|
686
|
+
{
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let tmpOrg = this._Catalog.GitHubOrg || 'stevenvelozo';
|
|
691
|
+
let tmpDefaultBranch = this._Catalog.DefaultBranch || 'master';
|
|
692
|
+
|
|
693
|
+
// Find the module in the catalog
|
|
694
|
+
for (let i = 0; i < this._Catalog.Groups.length; i++)
|
|
695
|
+
{
|
|
696
|
+
let tmpGroup = this._Catalog.Groups[i];
|
|
697
|
+
if (tmpGroup.Key !== pGroup)
|
|
698
|
+
{
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (let j = 0; j < tmpGroup.Modules.length; j++)
|
|
703
|
+
{
|
|
704
|
+
let tmpModule = tmpGroup.Modules[j];
|
|
705
|
+
if (tmpModule.Name !== pModule)
|
|
706
|
+
{
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let tmpBranch = tmpModule.Branch || tmpDefaultBranch;
|
|
711
|
+
let tmpDocPath = pPath || 'README.md';
|
|
712
|
+
return 'https://raw.githubusercontent.com/' + tmpOrg + '/' + tmpModule.Repo + '/' + tmpBranch + '/docs/' + tmpDocPath;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Get the module-specific sidebar entries for a given group/module.
|
|
721
|
+
*
|
|
722
|
+
* @param {string} pGroup - The group key
|
|
723
|
+
* @param {string} pModule - The module name
|
|
724
|
+
* @returns {Array|null} The sidebar entries or null
|
|
725
|
+
*/
|
|
726
|
+
getModuleSidebar(pGroup, pModule)
|
|
727
|
+
{
|
|
728
|
+
if (!this._Catalog)
|
|
729
|
+
{
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (let i = 0; i < this._Catalog.Groups.length; i++)
|
|
734
|
+
{
|
|
735
|
+
let tmpGroup = this._Catalog.Groups[i];
|
|
736
|
+
if (tmpGroup.Key !== pGroup)
|
|
737
|
+
{
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
for (let j = 0; j < tmpGroup.Modules.length; j++)
|
|
742
|
+
{
|
|
743
|
+
let tmpModule = tmpGroup.Modules[j];
|
|
744
|
+
if (tmpModule.Name !== pModule)
|
|
745
|
+
{
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return tmpModule.Sidebar || null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Fetch a markdown document and convert it to HTML.
|
|
758
|
+
*
|
|
759
|
+
* @param {string} pURL - The URL to fetch
|
|
760
|
+
* @param {Function} fCallback - Callback receiving (error, htmlContent)
|
|
761
|
+
*/
|
|
762
|
+
fetchDocument(pURL, fCallback)
|
|
763
|
+
{
|
|
764
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
765
|
+
|
|
766
|
+
if (!pURL)
|
|
767
|
+
{
|
|
768
|
+
return tmpCallback('No URL provided', '');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Check cache
|
|
772
|
+
if (this._ContentCache[pURL])
|
|
773
|
+
{
|
|
774
|
+
return tmpCallback(null, this._ContentCache[pURL]);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
fetch(pURL)
|
|
778
|
+
.then((pResponse) =>
|
|
779
|
+
{
|
|
780
|
+
if (!pResponse.ok)
|
|
781
|
+
{
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
return pResponse.text();
|
|
785
|
+
})
|
|
786
|
+
.then((pMarkdown) =>
|
|
787
|
+
{
|
|
788
|
+
if (!pMarkdown)
|
|
789
|
+
{
|
|
790
|
+
return tmpCallback('Document not found', this.getErrorPageHTML(pURL));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
let tmpHTML = this.parseMarkdown(pMarkdown);
|
|
794
|
+
this._ContentCache[pURL] = tmpHTML;
|
|
795
|
+
return tmpCallback(null, tmpHTML);
|
|
796
|
+
})
|
|
797
|
+
.catch((pError) =>
|
|
798
|
+
{
|
|
799
|
+
this.log.warn(`Docuserve: Error fetching document [${pURL}]: ${pError}`);
|
|
800
|
+
return tmpCallback(pError, this.getErrorPageHTML(pURL));
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Fetch a local document relative to the docs folder.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} pPath - The relative path (e.g. 'architecture.md')
|
|
808
|
+
* @param {Function} fCallback - Callback receiving (error, htmlContent)
|
|
809
|
+
*/
|
|
810
|
+
fetchLocalDocument(pPath, fCallback)
|
|
811
|
+
{
|
|
812
|
+
let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
813
|
+
let tmpURL = tmpDocsBase + pPath;
|
|
814
|
+
this.fetchDocument(tmpURL, fCallback);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Parse a markdown string into HTML.
|
|
819
|
+
*
|
|
820
|
+
* This is a basic markdown parser that handles the most common constructs:
|
|
821
|
+
* headings, paragraphs, code blocks, inline code, links, bold, italic,
|
|
822
|
+
* lists, blockquotes, and horizontal rules.
|
|
823
|
+
*
|
|
824
|
+
* @param {string} pMarkdown - The raw markdown text
|
|
825
|
+
* @returns {string} The parsed HTML
|
|
826
|
+
*/
|
|
827
|
+
parseMarkdown(pMarkdown)
|
|
828
|
+
{
|
|
829
|
+
if (!pMarkdown)
|
|
830
|
+
{
|
|
831
|
+
return '';
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
let tmpLines = pMarkdown.split('\n');
|
|
835
|
+
let tmpHTML = [];
|
|
836
|
+
let tmpInCodeBlock = false;
|
|
837
|
+
let tmpCodeLang = '';
|
|
838
|
+
let tmpCodeLines = [];
|
|
839
|
+
let tmpInList = false;
|
|
840
|
+
let tmpListType = '';
|
|
841
|
+
let tmpInBlockquote = false;
|
|
842
|
+
let tmpBlockquoteLines = [];
|
|
843
|
+
|
|
844
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
845
|
+
{
|
|
846
|
+
let tmpLine = tmpLines[i];
|
|
847
|
+
|
|
848
|
+
// Code blocks (fenced)
|
|
849
|
+
if (tmpLine.match(/^```/))
|
|
850
|
+
{
|
|
851
|
+
if (tmpInCodeBlock)
|
|
852
|
+
{
|
|
853
|
+
// End code block
|
|
854
|
+
tmpHTML.push('<pre><code class="language-' + this.escapeHTML(tmpCodeLang) + '">' + this.escapeHTML(tmpCodeLines.join('\n')) + '</code></pre>');
|
|
855
|
+
tmpInCodeBlock = false;
|
|
856
|
+
tmpCodeLang = '';
|
|
857
|
+
tmpCodeLines = [];
|
|
858
|
+
}
|
|
859
|
+
else
|
|
860
|
+
{
|
|
861
|
+
// Close any open list or blockquote
|
|
862
|
+
if (tmpInList)
|
|
863
|
+
{
|
|
864
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
865
|
+
tmpInList = false;
|
|
866
|
+
}
|
|
867
|
+
if (tmpInBlockquote)
|
|
868
|
+
{
|
|
869
|
+
tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n')) + '</blockquote>');
|
|
870
|
+
tmpInBlockquote = false;
|
|
871
|
+
tmpBlockquoteLines = [];
|
|
872
|
+
}
|
|
873
|
+
// Start code block
|
|
874
|
+
tmpCodeLang = tmpLine.replace(/^```/, '').trim();
|
|
875
|
+
tmpInCodeBlock = true;
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (tmpInCodeBlock)
|
|
881
|
+
{
|
|
882
|
+
tmpCodeLines.push(tmpLine);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Blockquotes
|
|
887
|
+
if (tmpLine.match(/^>\s?/))
|
|
888
|
+
{
|
|
889
|
+
if (!tmpInBlockquote)
|
|
890
|
+
{
|
|
891
|
+
// Close any open list
|
|
892
|
+
if (tmpInList)
|
|
893
|
+
{
|
|
894
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
895
|
+
tmpInList = false;
|
|
896
|
+
}
|
|
897
|
+
tmpInBlockquote = true;
|
|
898
|
+
tmpBlockquoteLines = [];
|
|
899
|
+
}
|
|
900
|
+
tmpBlockquoteLines.push(tmpLine.replace(/^>\s?/, ''));
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
else if (tmpInBlockquote)
|
|
904
|
+
{
|
|
905
|
+
tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n')) + '</blockquote>');
|
|
906
|
+
tmpInBlockquote = false;
|
|
907
|
+
tmpBlockquoteLines = [];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Horizontal rule
|
|
911
|
+
if (tmpLine.match(/^(-{3,}|\*{3,}|_{3,})\s*$/))
|
|
912
|
+
{
|
|
913
|
+
if (tmpInList)
|
|
914
|
+
{
|
|
915
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
916
|
+
tmpInList = false;
|
|
917
|
+
}
|
|
918
|
+
tmpHTML.push('<hr>');
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Headings
|
|
923
|
+
let tmpHeadingMatch = tmpLine.match(/^(#{1,6})\s+(.+)/);
|
|
924
|
+
if (tmpHeadingMatch)
|
|
925
|
+
{
|
|
926
|
+
if (tmpInList)
|
|
927
|
+
{
|
|
928
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
929
|
+
tmpInList = false;
|
|
930
|
+
}
|
|
931
|
+
let tmpLevel = tmpHeadingMatch[1].length;
|
|
932
|
+
let tmpText = this.parseInline(tmpHeadingMatch[2]);
|
|
933
|
+
let tmpID = tmpHeadingMatch[2].toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
|
|
934
|
+
tmpHTML.push('<h' + tmpLevel + ' id="' + tmpID + '">' + tmpText + '</h' + tmpLevel + '>');
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Unordered list items
|
|
939
|
+
let tmpULMatch = tmpLine.match(/^(\s*)[-*+]\s+(.*)/);
|
|
940
|
+
if (tmpULMatch)
|
|
941
|
+
{
|
|
942
|
+
if (!tmpInList || tmpListType !== 'ul')
|
|
943
|
+
{
|
|
944
|
+
if (tmpInList)
|
|
945
|
+
{
|
|
946
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
947
|
+
}
|
|
948
|
+
tmpHTML.push('<ul>');
|
|
949
|
+
tmpInList = true;
|
|
950
|
+
tmpListType = 'ul';
|
|
951
|
+
}
|
|
952
|
+
tmpHTML.push('<li>' + this.parseInline(tmpULMatch[2]) + '</li>');
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Ordered list items
|
|
957
|
+
let tmpOLMatch = tmpLine.match(/^(\s*)\d+\.\s+(.*)/);
|
|
958
|
+
if (tmpOLMatch)
|
|
959
|
+
{
|
|
960
|
+
if (!tmpInList || tmpListType !== 'ol')
|
|
961
|
+
{
|
|
962
|
+
if (tmpInList)
|
|
963
|
+
{
|
|
964
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
965
|
+
}
|
|
966
|
+
tmpHTML.push('<ol>');
|
|
967
|
+
tmpInList = true;
|
|
968
|
+
tmpListType = 'ol';
|
|
969
|
+
}
|
|
970
|
+
tmpHTML.push('<li>' + this.parseInline(tmpOLMatch[2]) + '</li>');
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Close list if we've left list items
|
|
975
|
+
if (tmpInList && tmpLine.trim() !== '')
|
|
976
|
+
{
|
|
977
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
978
|
+
tmpInList = false;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Empty line
|
|
982
|
+
if (tmpLine.trim() === '')
|
|
983
|
+
{
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Table detection
|
|
988
|
+
if (tmpLine.match(/^\|/) && i + 1 < tmpLines.length && tmpLines[i + 1].match(/^\|[\s-:|]+\|/))
|
|
989
|
+
{
|
|
990
|
+
// Close any open list
|
|
991
|
+
if (tmpInList)
|
|
992
|
+
{
|
|
993
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
994
|
+
tmpInList = false;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
let tmpTableHTML = '<table>';
|
|
998
|
+
|
|
999
|
+
// Header row
|
|
1000
|
+
let tmpHeaders = tmpLine.split('|').filter((pCell) => { return pCell.trim() !== ''; });
|
|
1001
|
+
tmpTableHTML += '<thead><tr>';
|
|
1002
|
+
for (let h = 0; h < tmpHeaders.length; h++)
|
|
1003
|
+
{
|
|
1004
|
+
tmpTableHTML += '<th>' + this.parseInline(tmpHeaders[h].trim()) + '</th>';
|
|
1005
|
+
}
|
|
1006
|
+
tmpTableHTML += '</tr></thead>';
|
|
1007
|
+
|
|
1008
|
+
// Skip separator row
|
|
1009
|
+
i++;
|
|
1010
|
+
|
|
1011
|
+
// Body rows
|
|
1012
|
+
tmpTableHTML += '<tbody>';
|
|
1013
|
+
while (i + 1 < tmpLines.length && tmpLines[i + 1].match(/^\|/))
|
|
1014
|
+
{
|
|
1015
|
+
i++;
|
|
1016
|
+
let tmpCells = tmpLines[i].split('|').filter((pCell) => { return pCell.trim() !== ''; });
|
|
1017
|
+
tmpTableHTML += '<tr>';
|
|
1018
|
+
for (let c = 0; c < tmpCells.length; c++)
|
|
1019
|
+
{
|
|
1020
|
+
tmpTableHTML += '<td>' + this.parseInline(tmpCells[c].trim()) + '</td>';
|
|
1021
|
+
}
|
|
1022
|
+
tmpTableHTML += '</tr>';
|
|
1023
|
+
}
|
|
1024
|
+
tmpTableHTML += '</tbody></table>';
|
|
1025
|
+
tmpHTML.push(tmpTableHTML);
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Regular paragraph
|
|
1030
|
+
tmpHTML.push('<p>' + this.parseInline(tmpLine) + '</p>');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Close any trailing open elements
|
|
1034
|
+
if (tmpInList)
|
|
1035
|
+
{
|
|
1036
|
+
tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
|
|
1037
|
+
}
|
|
1038
|
+
if (tmpInBlockquote)
|
|
1039
|
+
{
|
|
1040
|
+
tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n')) + '</blockquote>');
|
|
1041
|
+
}
|
|
1042
|
+
if (tmpInCodeBlock)
|
|
1043
|
+
{
|
|
1044
|
+
tmpHTML.push('<pre><code>' + this.escapeHTML(tmpCodeLines.join('\n')) + '</code></pre>');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return tmpHTML.join('\n');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Parse inline markdown elements (bold, italic, code, links, images).
|
|
1052
|
+
*
|
|
1053
|
+
* @param {string} pText - The text to parse
|
|
1054
|
+
* @returns {string} HTML with inline elements
|
|
1055
|
+
*/
|
|
1056
|
+
parseInline(pText)
|
|
1057
|
+
{
|
|
1058
|
+
if (!pText)
|
|
1059
|
+
{
|
|
1060
|
+
return '';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
let tmpResult = pText;
|
|
1064
|
+
|
|
1065
|
+
// Inline code (backticks) - handle first to avoid interfering with other patterns
|
|
1066
|
+
tmpResult = tmpResult.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1067
|
+
|
|
1068
|
+
// Images
|
|
1069
|
+
tmpResult = tmpResult.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
|
1070
|
+
|
|
1071
|
+
// Links
|
|
1072
|
+
tmpResult = tmpResult.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (pMatch, pLinkText, pHref) =>
|
|
1073
|
+
{
|
|
1074
|
+
// Convert internal doc links to hash routes
|
|
1075
|
+
if (pHref.match(/^\//) || pHref.match(/^[^:]+\.md/))
|
|
1076
|
+
{
|
|
1077
|
+
let tmpRoute = this.convertDocLink(pHref);
|
|
1078
|
+
return '<a href="' + tmpRoute + '">' + pLinkText + '</a>';
|
|
1079
|
+
}
|
|
1080
|
+
return '<a href="' + pHref + '" target="_blank" rel="noopener">' + pLinkText + '</a>';
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Bold
|
|
1084
|
+
tmpResult = tmpResult.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
1085
|
+
tmpResult = tmpResult.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
1086
|
+
|
|
1087
|
+
// Italic
|
|
1088
|
+
tmpResult = tmpResult.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
1089
|
+
tmpResult = tmpResult.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
1090
|
+
|
|
1091
|
+
return tmpResult;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Convert a docsify-style internal link to a hash route for docuserve.
|
|
1096
|
+
*
|
|
1097
|
+
* @param {string} pHref - The original link href
|
|
1098
|
+
* @returns {string} The converted hash route
|
|
1099
|
+
*/
|
|
1100
|
+
convertDocLink(pHref)
|
|
1101
|
+
{
|
|
1102
|
+
// Remove leading slash
|
|
1103
|
+
let tmpPath = pHref.replace(/^\//, '');
|
|
1104
|
+
|
|
1105
|
+
// If it looks like a module path (group/module/), convert to our route format
|
|
1106
|
+
let tmpParts = tmpPath.split('/');
|
|
1107
|
+
if (tmpParts.length >= 2)
|
|
1108
|
+
{
|
|
1109
|
+
// Check if first segment matches a known group key
|
|
1110
|
+
let tmpGroupKeys = ['fable', 'meadow', 'orator', 'pict', 'utility'];
|
|
1111
|
+
if (tmpGroupKeys.indexOf(tmpParts[0]) >= 0)
|
|
1112
|
+
{
|
|
1113
|
+
return '#/doc/' + tmpPath;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Local doc page
|
|
1118
|
+
if (tmpPath.match(/\.md$/))
|
|
1119
|
+
{
|
|
1120
|
+
let tmpPageKey = tmpPath.replace(/\.md$/, '');
|
|
1121
|
+
return '#/page/' + tmpPageKey;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return '#/page/' + tmpPath;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Escape HTML special characters.
|
|
1129
|
+
*
|
|
1130
|
+
* @param {string} pText - The text to escape
|
|
1131
|
+
* @returns {string} The escaped text
|
|
1132
|
+
*/
|
|
1133
|
+
escapeHTML(pText)
|
|
1134
|
+
{
|
|
1135
|
+
if (!pText)
|
|
1136
|
+
{
|
|
1137
|
+
return '';
|
|
1138
|
+
}
|
|
1139
|
+
return pText
|
|
1140
|
+
.replace(/&/g, '&')
|
|
1141
|
+
.replace(/</g, '<')
|
|
1142
|
+
.replace(/>/g, '>')
|
|
1143
|
+
.replace(/"/g, '"')
|
|
1144
|
+
.replace(/'/g, ''');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
module.exports = DocuserveDocumentationProvider;
|
|
1149
|
+
|
|
1150
|
+
module.exports.default_configuration =
|
|
1151
|
+
{
|
|
1152
|
+
ProviderIdentifier: "Docuserve-Documentation",
|
|
1153
|
+
|
|
1154
|
+
AutoInitialize: true,
|
|
1155
|
+
AutoInitializeOrdinal: 0
|
|
1156
|
+
};
|