pict-docuserve 0.0.8 → 0.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-docuserve",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
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": {
@@ -18,7 +18,7 @@
18
18
  "build-docs": "npx quack build && npx quack copy && node source/cli/Docuserve-CLI-Run.js inject ./docs && node example_applications/build-examples.js stage-docs",
19
19
  "serve-docs": "node source/cli/Docuserve-CLI-Run.js serve ./docs",
20
20
  "serve-examples": "node example_applications/build-examples.js",
21
- "test": "echo \"Error: no test specified\" && exit 0",
21
+ "test": "npx mocha -u tdd --exit -R spec",
22
22
  "tests": "npx mocha -u tdd --exit -R spec --grep",
23
23
  "coverage": "npx nyc --reporter=lcov --reporter=text-lcov npx mocha -- -u tdd -R spec"
24
24
  },
@@ -29,10 +29,13 @@
29
29
  "pict": "^1.0.343",
30
30
  "pict-application": "^1.0.30",
31
31
  "pict-provider": "^1.0.7",
32
+ "pict-section-content": "file:../pict-section-content",
32
33
  "pict-service-commandlineutility": "^1.0.17",
33
34
  "pict-view": "^1.0.64"
34
35
  },
35
36
  "devDependencies": {
37
+ "chai": "^6.2.2",
38
+ "mocha": "^11.7.5",
36
39
  "quackage": "^1.0.47"
37
40
  },
38
41
  "copyFilesSettings": {
@@ -1,5 +1,7 @@
1
1
  const libPictProvider = require('pict-provider');
2
2
  const libLunr = require('lunr');
3
+ const libPictSectionContent = require('pict-section-content');
4
+ const libPictContentProvider = libPictSectionContent.PictContentProvider;
3
5
 
4
6
  /**
5
7
  * Documentation Provider for Docuserve
@@ -16,6 +18,42 @@ class DocuserveDocumentationProvider extends libPictProvider
16
18
 
17
19
  this._Catalog = null;
18
20
  this._ContentCache = {};
21
+
22
+ // Create an instance of the content provider for markdown parsing
23
+ this._ContentProvider = this.pict.addProvider('Pict-Content', libPictContentProvider.default_configuration, libPictContentProvider);
24
+ }
25
+
26
+ /**
27
+ * Create a link resolver closure for the content provider.
28
+ *
29
+ * Wraps docuserve-specific link resolution (catalog-aware routing,
30
+ * GitHub URL matching) into a callback compatible with the
31
+ * pict-section-content link resolver pattern.
32
+ *
33
+ * @param {string} [pCurrentGroup] - The current group key
34
+ * @param {string} [pCurrentModule] - The current module name
35
+ * @param {string} [pCurrentDocPath] - The current document path
36
+ * @returns {Function} A link resolver callback
37
+ */
38
+ _createLinkResolver(pCurrentGroup, pCurrentModule, pCurrentDocPath)
39
+ {
40
+ return (pHref, pLinkText) =>
41
+ {
42
+ // Convert internal doc links to hash routes
43
+ if (pHref.match(/^\//) || pHref.match(/^[^:]+\.md/))
44
+ {
45
+ let tmpRoute = this.convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
46
+ return { href: tmpRoute };
47
+ }
48
+ // Check if this is a GitHub URL that matches a catalog module
49
+ let tmpCatalogRoute = this.resolveGitHubURLToRoute(pHref);
50
+ if (tmpCatalogRoute)
51
+ {
52
+ return { href: tmpCatalogRoute };
53
+ }
54
+ // Use default behavior for other links
55
+ return null;
56
+ };
19
57
  }
20
58
 
21
59
  /**
@@ -495,7 +533,7 @@ class DocuserveDocumentationProvider extends libPictProvider
495
533
  return tmpCallback();
496
534
  }
497
535
 
498
- this.pict.AppData.Docuserve.ErrorPageHTML = this.parseMarkdown(pMarkdown);
536
+ this.pict.AppData.Docuserve.ErrorPageHTML = this._ContentProvider.parseMarkdown(pMarkdown);
499
537
  this.pict.AppData.Docuserve.ErrorPageLoaded = true;
500
538
  return tmpCallback();
501
539
  })
@@ -714,13 +752,13 @@ class DocuserveDocumentationProvider extends libPictProvider
714
752
  if (this.pict.AppData.Docuserve.ErrorPageLoaded && this.pict.AppData.Docuserve.ErrorPageHTML)
715
753
  {
716
754
  // Replace the {{path}} placeholder with the actual requested path
717
- return this.pict.AppData.Docuserve.ErrorPageHTML.replace(/\{\{path\}\}/g, this.escapeHTML(tmpPath));
755
+ return this.pict.AppData.Docuserve.ErrorPageHTML.replace(/\{\{path\}\}/g, this._ContentProvider.escapeHTML(tmpPath));
718
756
  }
719
757
 
720
758
  // Default fallback
721
759
  return '<div class="docuserve-not-found">'
722
760
  + '<h2>Page Not Found</h2>'
723
- + '<p>The document <code>' + this.escapeHTML(tmpPath) + '</code> could not be loaded.</p>'
761
+ + '<p>The document <code>' + this._ContentProvider.escapeHTML(tmpPath) + '</code> could not be loaded.</p>'
724
762
  + '<p><a href="#/Home">Return to the home page</a></p>'
725
763
  + '</div>';
726
764
  }
@@ -994,6 +1032,57 @@ class DocuserveDocumentationProvider extends libPictProvider
994
1032
  return null;
995
1033
  }
996
1034
 
1035
+ /**
1036
+ * Resolve a GitHub repository URL to an internal hash route.
1037
+ *
1038
+ * If the URL matches a module in the loaded catalog, returns the
1039
+ * corresponding #/doc/ route so the link navigates within docuserve
1040
+ * instead of leaving to GitHub.
1041
+ *
1042
+ * @param {string} pURL - A GitHub URL (e.g. "https://github.com/stevenvelozo/fable")
1043
+ * @returns {string|null} The hash route (e.g. "#/doc/fable/fable") or null if not a catalog module
1044
+ */
1045
+ resolveGitHubURLToRoute(pURL)
1046
+ {
1047
+ if (!this._Catalog || !this._Catalog.Groups || !pURL)
1048
+ {
1049
+ return null;
1050
+ }
1051
+
1052
+ // Match https://github.com/{org}/{repo} with optional trailing path/slash
1053
+ let tmpMatch = pURL.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)/);
1054
+ if (!tmpMatch)
1055
+ {
1056
+ return null;
1057
+ }
1058
+
1059
+ let tmpOrg = tmpMatch[1];
1060
+ let tmpRepo = tmpMatch[2];
1061
+
1062
+ // Only resolve URLs that match the catalog's GitHub org
1063
+ if (tmpOrg !== this._Catalog.GitHubOrg)
1064
+ {
1065
+ return null;
1066
+ }
1067
+
1068
+ // Search catalog for a module with a matching Repo
1069
+ for (let i = 0; i < this._Catalog.Groups.length; i++)
1070
+ {
1071
+ let tmpGroup = this._Catalog.Groups[i];
1072
+
1073
+ for (let j = 0; j < tmpGroup.Modules.length; j++)
1074
+ {
1075
+ let tmpModule = tmpGroup.Modules[j];
1076
+ if (tmpModule.Repo === tmpRepo)
1077
+ {
1078
+ return '#/doc/' + tmpGroup.Key + '/' + tmpModule.Name;
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ return null;
1084
+ }
1085
+
997
1086
  /**
998
1087
  * Get the module-specific sidebar entries for a given group/module.
999
1088
  *
@@ -1071,7 +1160,7 @@ class DocuserveDocumentationProvider extends libPictProvider
1071
1160
  return tmpCallback('Document not found', this.getErrorPageHTML(pURL));
1072
1161
  }
1073
1162
 
1074
- let tmpHTML = this.parseMarkdown(pMarkdown, pCurrentGroup, pCurrentModule, pCurrentDocPath);
1163
+ let tmpHTML = this._ContentProvider.parseMarkdown(pMarkdown, this._createLinkResolver(pCurrentGroup, pCurrentModule, pCurrentDocPath));
1075
1164
  this._ContentCache[pURL] = tmpHTML;
1076
1165
  return tmpCallback(null, tmpHTML);
1077
1166
  })
@@ -1098,357 +1187,6 @@ class DocuserveDocumentationProvider extends libPictProvider
1098
1187
  this.fetchDocument(tmpURL, fCallback, pCurrentGroup, pCurrentModule, pCurrentDocPath);
1099
1188
  }
1100
1189
 
1101
- /**
1102
- * Parse a markdown string into HTML.
1103
- *
1104
- * This is a basic markdown parser that handles the most common constructs:
1105
- * headings, paragraphs, code blocks, inline code, links, bold, italic,
1106
- * lists, blockquotes, and horizontal rules.
1107
- *
1108
- * @param {string} pMarkdown - The raw markdown text
1109
- * @param {string} [pCurrentGroup] - The current group key for link resolution
1110
- * @param {string} [pCurrentModule] - The current module name for link resolution
1111
- * @param {string} [pCurrentDocPath] - The current document path for link resolution
1112
- * @returns {string} The parsed HTML
1113
- */
1114
- parseMarkdown(pMarkdown, pCurrentGroup, pCurrentModule, pCurrentDocPath)
1115
- {
1116
- if (!pMarkdown)
1117
- {
1118
- return '';
1119
- }
1120
-
1121
- let tmpLines = pMarkdown.split('\n');
1122
- let tmpHTML = [];
1123
- let tmpInCodeBlock = false;
1124
- let tmpCodeFenceLength = 0;
1125
- let tmpCodeLang = '';
1126
- let tmpCodeLines = [];
1127
- let tmpInList = false;
1128
- let tmpListType = '';
1129
- let tmpInBlockquote = false;
1130
- let tmpBlockquoteLines = [];
1131
- let tmpInMathBlock = false;
1132
- let tmpMathLines = [];
1133
-
1134
- for (let i = 0; i < tmpLines.length; i++)
1135
- {
1136
- let tmpLine = tmpLines[i];
1137
-
1138
- // Display math blocks ($$...$$) — skip if inside a code block
1139
- if (!tmpInCodeBlock && tmpLine.trim().match(/^\$\$/))
1140
- {
1141
- if (tmpInMathBlock)
1142
- {
1143
- // End math block
1144
- tmpHTML.push('<div class="docuserve-katex-display">' + tmpMathLines.join('\n') + '</div>');
1145
- tmpInMathBlock = false;
1146
- tmpMathLines = [];
1147
- }
1148
- else
1149
- {
1150
- // Close any open list or blockquote
1151
- if (tmpInList)
1152
- {
1153
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1154
- tmpInList = false;
1155
- }
1156
- if (tmpInBlockquote)
1157
- {
1158
- tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n'), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</blockquote>');
1159
- tmpInBlockquote = false;
1160
- tmpBlockquoteLines = [];
1161
- }
1162
- tmpInMathBlock = true;
1163
- }
1164
- continue;
1165
- }
1166
-
1167
- if (tmpInMathBlock)
1168
- {
1169
- tmpMathLines.push(tmpLine);
1170
- continue;
1171
- }
1172
-
1173
- // Code blocks (fenced) — track fence length so ````x```` nests around ```y```
1174
- let tmpFenceMatch = tmpLine.match(/^(`{3,})/);
1175
- if (tmpFenceMatch)
1176
- {
1177
- let tmpFenceLen = tmpFenceMatch[1].length;
1178
-
1179
- if (tmpInCodeBlock)
1180
- {
1181
- // Only close if the closing fence is at least as long as the opening
1182
- if (tmpFenceLen >= tmpCodeFenceLength && tmpLine.trim() === tmpFenceMatch[1])
1183
- {
1184
- // End code block
1185
- if (tmpCodeLang === 'mermaid')
1186
- {
1187
- // Mermaid diagrams: output raw content for client-side rendering
1188
- tmpHTML.push('<pre class="mermaid">' + tmpCodeLines.join('\n') + '</pre>');
1189
- }
1190
- else
1191
- {
1192
- tmpHTML.push('<pre><code class="language-' + this.escapeHTML(tmpCodeLang) + '">' + this.escapeHTML(tmpCodeLines.join('\n')) + '</code></pre>');
1193
- }
1194
- tmpInCodeBlock = false;
1195
- tmpCodeFenceLength = 0;
1196
- tmpCodeLang = '';
1197
- tmpCodeLines = [];
1198
- continue;
1199
- }
1200
- else
1201
- {
1202
- // Inner fence with fewer backticks — treat as content
1203
- tmpCodeLines.push(tmpLine);
1204
- continue;
1205
- }
1206
- }
1207
- else
1208
- {
1209
- // Close any open list or blockquote
1210
- if (tmpInList)
1211
- {
1212
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1213
- tmpInList = false;
1214
- }
1215
- if (tmpInBlockquote)
1216
- {
1217
- tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n'), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</blockquote>');
1218
- tmpInBlockquote = false;
1219
- tmpBlockquoteLines = [];
1220
- }
1221
- // Start code block — record fence length
1222
- tmpCodeFenceLength = tmpFenceLen;
1223
- tmpCodeLang = tmpLine.replace(/^`{3,}/, '').trim();
1224
- tmpInCodeBlock = true;
1225
- continue;
1226
- }
1227
- }
1228
-
1229
- if (tmpInCodeBlock)
1230
- {
1231
- tmpCodeLines.push(tmpLine);
1232
- continue;
1233
- }
1234
-
1235
- // Blockquotes
1236
- if (tmpLine.match(/^>\s?/))
1237
- {
1238
- if (!tmpInBlockquote)
1239
- {
1240
- // Close any open list
1241
- if (tmpInList)
1242
- {
1243
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1244
- tmpInList = false;
1245
- }
1246
- tmpInBlockquote = true;
1247
- tmpBlockquoteLines = [];
1248
- }
1249
- tmpBlockquoteLines.push(tmpLine.replace(/^>\s?/, ''));
1250
- continue;
1251
- }
1252
- else if (tmpInBlockquote)
1253
- {
1254
- tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n'), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</blockquote>');
1255
- tmpInBlockquote = false;
1256
- tmpBlockquoteLines = [];
1257
- }
1258
-
1259
- // Horizontal rule
1260
- if (tmpLine.match(/^(-{3,}|\*{3,}|_{3,})\s*$/))
1261
- {
1262
- if (tmpInList)
1263
- {
1264
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1265
- tmpInList = false;
1266
- }
1267
- tmpHTML.push('<hr>');
1268
- continue;
1269
- }
1270
-
1271
- // Headings
1272
- let tmpHeadingMatch = tmpLine.match(/^(#{1,6})\s+(.+)/);
1273
- if (tmpHeadingMatch)
1274
- {
1275
- if (tmpInList)
1276
- {
1277
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1278
- tmpInList = false;
1279
- }
1280
- let tmpLevel = tmpHeadingMatch[1].length;
1281
- let tmpText = this.parseInline(tmpHeadingMatch[2], pCurrentGroup, pCurrentModule, pCurrentDocPath);
1282
- let tmpID = tmpHeadingMatch[2].toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
1283
- tmpHTML.push('<h' + tmpLevel + ' id="' + tmpID + '">' + tmpText + '</h' + tmpLevel + '>');
1284
- continue;
1285
- }
1286
-
1287
- // Unordered list items
1288
- let tmpULMatch = tmpLine.match(/^(\s*)[-*+]\s+(.*)/);
1289
- if (tmpULMatch)
1290
- {
1291
- if (!tmpInList || tmpListType !== 'ul')
1292
- {
1293
- if (tmpInList)
1294
- {
1295
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1296
- }
1297
- tmpHTML.push('<ul>');
1298
- tmpInList = true;
1299
- tmpListType = 'ul';
1300
- }
1301
- tmpHTML.push('<li>' + this.parseInline(tmpULMatch[2], pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</li>');
1302
- continue;
1303
- }
1304
-
1305
- // Ordered list items
1306
- let tmpOLMatch = tmpLine.match(/^(\s*)\d+\.\s+(.*)/);
1307
- if (tmpOLMatch)
1308
- {
1309
- if (!tmpInList || tmpListType !== 'ol')
1310
- {
1311
- if (tmpInList)
1312
- {
1313
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1314
- }
1315
- tmpHTML.push('<ol>');
1316
- tmpInList = true;
1317
- tmpListType = 'ol';
1318
- }
1319
- tmpHTML.push('<li>' + this.parseInline(tmpOLMatch[2], pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</li>');
1320
- continue;
1321
- }
1322
-
1323
- // Close list if we've left list items
1324
- if (tmpInList && tmpLine.trim() !== '')
1325
- {
1326
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1327
- tmpInList = false;
1328
- }
1329
-
1330
- // Empty line
1331
- if (tmpLine.trim() === '')
1332
- {
1333
- continue;
1334
- }
1335
-
1336
- // Table detection
1337
- if (tmpLine.match(/^\|/) && i + 1 < tmpLines.length && tmpLines[i + 1].match(/^\|[\s-:|]+\|/))
1338
- {
1339
- // Close any open list
1340
- if (tmpInList)
1341
- {
1342
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1343
- tmpInList = false;
1344
- }
1345
-
1346
- let tmpTableHTML = '<table>';
1347
-
1348
- // Header row
1349
- let tmpHeaders = tmpLine.split('|').filter((pCell) => { return pCell.trim() !== ''; });
1350
- tmpTableHTML += '<thead><tr>';
1351
- for (let h = 0; h < tmpHeaders.length; h++)
1352
- {
1353
- tmpTableHTML += '<th>' + this.parseInline(tmpHeaders[h].trim(), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</th>';
1354
- }
1355
- tmpTableHTML += '</tr></thead>';
1356
-
1357
- // Skip separator row
1358
- i++;
1359
-
1360
- // Body rows
1361
- tmpTableHTML += '<tbody>';
1362
- while (i + 1 < tmpLines.length && tmpLines[i + 1].match(/^\|/))
1363
- {
1364
- i++;
1365
- let tmpCells = tmpLines[i].split('|').filter((pCell) => { return pCell.trim() !== ''; });
1366
- tmpTableHTML += '<tr>';
1367
- for (let c = 0; c < tmpCells.length; c++)
1368
- {
1369
- tmpTableHTML += '<td>' + this.parseInline(tmpCells[c].trim(), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</td>';
1370
- }
1371
- tmpTableHTML += '</tr>';
1372
- }
1373
- tmpTableHTML += '</tbody></table>';
1374
- tmpHTML.push(tmpTableHTML);
1375
- continue;
1376
- }
1377
-
1378
- // Regular paragraph
1379
- tmpHTML.push('<p>' + this.parseInline(tmpLine, pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</p>');
1380
- }
1381
-
1382
- // Close any trailing open elements
1383
- if (tmpInList)
1384
- {
1385
- tmpHTML.push(tmpListType === 'ul' ? '</ul>' : '</ol>');
1386
- }
1387
- if (tmpInBlockquote)
1388
- {
1389
- tmpHTML.push('<blockquote>' + this.parseMarkdown(tmpBlockquoteLines.join('\n'), pCurrentGroup, pCurrentModule, pCurrentDocPath) + '</blockquote>');
1390
- }
1391
- if (tmpInCodeBlock)
1392
- {
1393
- tmpHTML.push('<pre><code>' + this.escapeHTML(tmpCodeLines.join('\n')) + '</code></pre>');
1394
- }
1395
-
1396
- return tmpHTML.join('\n');
1397
- }
1398
-
1399
- /**
1400
- * Parse inline markdown elements (bold, italic, code, links, images).
1401
- *
1402
- * @param {string} pText - The text to parse
1403
- * @param {string} [pCurrentGroup] - The current group key for link resolution
1404
- * @param {string} [pCurrentModule] - The current module name for link resolution
1405
- * @param {string} [pCurrentDocPath] - The current document path for link resolution
1406
- * @returns {string} HTML with inline elements
1407
- */
1408
- parseInline(pText, pCurrentGroup, pCurrentModule, pCurrentDocPath)
1409
- {
1410
- if (!pText)
1411
- {
1412
- return '';
1413
- }
1414
-
1415
- let tmpResult = pText;
1416
-
1417
- // Inline code (backticks) - handle first to avoid interfering with other patterns
1418
- tmpResult = tmpResult.replace(/`([^`]+)`/g, '<code>$1</code>');
1419
-
1420
- // Inline LaTeX equations ($...$) — must be processed before other inline patterns
1421
- // Match single $ delimiters that aren't adjacent to spaces (to avoid false positives with currency)
1422
- tmpResult = tmpResult.replace(/\$([^\$\s][^\$]*?[^\$\s])\$/g, '<span class="docuserve-katex-inline">$1</span>');
1423
- // Also match single-character inline math like $x$
1424
- tmpResult = tmpResult.replace(/\$([^\$\s])\$/g, '<span class="docuserve-katex-inline">$1</span>');
1425
-
1426
- // Images
1427
- tmpResult = tmpResult.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
1428
-
1429
- // Links
1430
- tmpResult = tmpResult.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (pMatch, pLinkText, pHref) =>
1431
- {
1432
- // Convert internal doc links to hash routes
1433
- if (pHref.match(/^\//) || pHref.match(/^[^:]+\.md/))
1434
- {
1435
- let tmpRoute = this.convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
1436
- return '<a href="' + tmpRoute + '">' + pLinkText + '</a>';
1437
- }
1438
- return '<a href="' + pHref + '" target="_blank" rel="noopener">' + pLinkText + '</a>';
1439
- });
1440
-
1441
- // Bold
1442
- tmpResult = tmpResult.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1443
- tmpResult = tmpResult.replace(/__([^_]+)__/g, '<strong>$1</strong>');
1444
-
1445
- // Italic
1446
- tmpResult = tmpResult.replace(/\*([^*]+)\*/g, '<em>$1</em>');
1447
- tmpResult = tmpResult.replace(/_([^_]+)_/g, '<em>$1</em>');
1448
-
1449
- return tmpResult;
1450
- }
1451
-
1452
1190
  /**
1453
1191
  * Convert a docsify-style internal link to a hash route for docuserve.
1454
1192
  *
@@ -1506,25 +1244,6 @@ class DocuserveDocumentationProvider extends libPictProvider
1506
1244
  return '#/page/' + tmpPath;
1507
1245
  }
1508
1246
 
1509
- /**
1510
- * Escape HTML special characters.
1511
- *
1512
- * @param {string} pText - The text to escape
1513
- * @returns {string} The escaped text
1514
- */
1515
- escapeHTML(pText)
1516
- {
1517
- if (!pText)
1518
- {
1519
- return '';
1520
- }
1521
- return pText
1522
- .replace(/&/g, '&amp;')
1523
- .replace(/</g, '&lt;')
1524
- .replace(/>/g, '&gt;')
1525
- .replace(/"/g, '&quot;')
1526
- .replace(/'/g, '&#39;');
1527
- }
1528
1247
  }
1529
1248
 
1530
1249
  module.exports = DocuserveDocumentationProvider;
@@ -1,4 +1,4 @@
1
- const libPictView = require('pict-view');
1
+ const libPictContentView = require('pict-section-content');
2
2
 
3
3
  const _ViewConfiguration =
4
4
  {
@@ -10,139 +10,6 @@ const _ViewConfiguration =
10
10
  AutoRender: false,
11
11
 
12
12
  CSS: /*css*/`
13
- .docuserve-content {
14
- padding: 2em 3em;
15
- max-width: 900px;
16
- margin: 0 auto;
17
- }
18
- .docuserve-content-loading {
19
- display: flex;
20
- align-items: center;
21
- justify-content: center;
22
- min-height: 200px;
23
- color: #8A7F72;
24
- font-size: 1em;
25
- }
26
- .docuserve-content h1 {
27
- font-size: 2em;
28
- color: #3D3229;
29
- border-bottom: 1px solid #DDD6CA;
30
- padding-bottom: 0.3em;
31
- margin-top: 0;
32
- }
33
- .docuserve-content h2 {
34
- font-size: 1.5em;
35
- color: #3D3229;
36
- border-bottom: 1px solid #EAE3D8;
37
- padding-bottom: 0.25em;
38
- margin-top: 1.5em;
39
- }
40
- .docuserve-content h3 {
41
- font-size: 1.25em;
42
- color: #3D3229;
43
- margin-top: 1.25em;
44
- }
45
- .docuserve-content h4, .docuserve-content h5, .docuserve-content h6 {
46
- color: #5E5549;
47
- margin-top: 1em;
48
- }
49
- .docuserve-content p {
50
- line-height: 1.7;
51
- color: #423D37;
52
- margin: 0.75em 0;
53
- }
54
- .docuserve-content a {
55
- color: #2E7D74;
56
- text-decoration: none;
57
- }
58
- .docuserve-content a:hover {
59
- text-decoration: underline;
60
- }
61
- .docuserve-content pre {
62
- background: #3D3229;
63
- color: #E8E0D4;
64
- padding: 1.25em;
65
- border-radius: 6px;
66
- overflow-x: auto;
67
- line-height: 1.5;
68
- font-size: 0.9em;
69
- }
70
- .docuserve-content code {
71
- background: #F0ECE4;
72
- padding: 0.15em 0.4em;
73
- border-radius: 3px;
74
- font-size: 0.9em;
75
- color: #9E6B47;
76
- }
77
- .docuserve-content pre code {
78
- background: none;
79
- padding: 0;
80
- color: inherit;
81
- font-size: inherit;
82
- }
83
- .docuserve-content blockquote {
84
- border-left: 4px solid #2E7D74;
85
- margin: 1em 0;
86
- padding: 0.5em 1em;
87
- background: #F7F5F0;
88
- color: #5E5549;
89
- }
90
- .docuserve-content blockquote p {
91
- margin: 0.25em 0;
92
- }
93
- .docuserve-content ul, .docuserve-content ol {
94
- padding-left: 2em;
95
- line-height: 1.8;
96
- }
97
- .docuserve-content li {
98
- margin: 0.25em 0;
99
- color: #423D37;
100
- }
101
- .docuserve-content hr {
102
- border: none;
103
- border-top: 1px solid #DDD6CA;
104
- margin: 2em 0;
105
- }
106
- .docuserve-content table {
107
- width: 100%;
108
- border-collapse: collapse;
109
- margin: 1em 0;
110
- }
111
- .docuserve-content table th {
112
- background: #F5F0E8;
113
- border: 1px solid #DDD6CA;
114
- padding: 0.6em 0.8em;
115
- text-align: left;
116
- font-weight: 600;
117
- color: #3D3229;
118
- }
119
- .docuserve-content table td {
120
- border: 1px solid #DDD6CA;
121
- padding: 0.5em 0.8em;
122
- color: #423D37;
123
- }
124
- .docuserve-content table tr:nth-child(even) {
125
- background: #F7F5F0;
126
- }
127
- .docuserve-content img {
128
- max-width: 100%;
129
- height: auto;
130
- }
131
- .docuserve-content pre.mermaid {
132
- background: #fff;
133
- color: #3D3229;
134
- text-align: center;
135
- padding: 1em;
136
- }
137
- .docuserve-content .docuserve-katex-display {
138
- text-align: center;
139
- margin: 1em 0;
140
- padding: 0.5em;
141
- overflow-x: auto;
142
- }
143
- .docuserve-content .docuserve-katex-inline {
144
- display: inline;
145
- }
146
13
  .docuserve-not-found {
147
14
  text-align: center;
148
15
  padding: 3em 1em;
@@ -167,8 +34,8 @@ const _ViewConfiguration =
167
34
  {
168
35
  Hash: "Docuserve-Content-Template",
169
36
  Template: /*html*/`
170
- <div class="docuserve-content" id="Docuserve-Content-Body">
171
- <div class="docuserve-content-loading">Loading documentation...</div>
37
+ <div class="pict-content" id="Docuserve-Content-Body">
38
+ <div class="pict-content-loading">Loading documentation...</div>
172
39
  </div>
173
40
  `
174
41
  }
@@ -185,7 +52,7 @@ const _ViewConfiguration =
185
52
  ]
186
53
  };
187
54
 
188
- class DocuserveContentView extends libPictView
55
+ class DocuserveContentView extends libPictContentView
189
56
  {
190
57
  constructor(pFable, pOptions, pServiceHash)
191
58
  {
@@ -199,101 +66,7 @@ class DocuserveContentView extends libPictView
199
66
  */
200
67
  displayContent(pHTMLContent)
201
68
  {
202
- this.pict.ContentAssignment.assignContent('#Docuserve-Content-Body', pHTMLContent);
203
-
204
- // Scroll to top of content area
205
- let tmpContentContainer = document.getElementById('Docuserve-Content-Container');
206
- if (tmpContentContainer)
207
- {
208
- tmpContentContainer.scrollTop = 0;
209
- }
210
-
211
- // Post-render: initialize Mermaid diagrams if mermaid is available
212
- this.renderMermaidDiagrams();
213
-
214
- // Post-render: render KaTeX equations if katex is available
215
- this.renderKaTeXEquations();
216
- }
217
-
218
- /**
219
- * Render any Mermaid diagram blocks in the content area.
220
- * Mermaid blocks are `<pre class="mermaid">` elements produced by parseMarkdown.
221
- */
222
- renderMermaidDiagrams()
223
- {
224
- if (typeof mermaid === 'undefined')
225
- {
226
- return;
227
- }
228
-
229
- let tmpContentBody = document.getElementById('Docuserve-Content-Body');
230
- if (!tmpContentBody)
231
- {
232
- return;
233
- }
234
-
235
- let tmpMermaidElements = tmpContentBody.querySelectorAll('pre.mermaid');
236
- if (tmpMermaidElements.length < 1)
237
- {
238
- return;
239
- }
240
-
241
- // mermaid.run() will process all pre.mermaid elements in the container
242
- try
243
- {
244
- mermaid.run({ nodes: tmpMermaidElements });
245
- }
246
- catch (pError)
247
- {
248
- this.log.error('Mermaid rendering error: ' + pError.message);
249
- }
250
- }
251
-
252
- /**
253
- * Render KaTeX inline and display math elements in the content area.
254
- * Inline: `<span class="docuserve-katex-inline">`
255
- * Display: `<div class="docuserve-katex-display">`
256
- */
257
- renderKaTeXEquations()
258
- {
259
- if (typeof katex === 'undefined')
260
- {
261
- return;
262
- }
263
-
264
- let tmpContentBody = document.getElementById('Docuserve-Content-Body');
265
- if (!tmpContentBody)
266
- {
267
- return;
268
- }
269
-
270
- // Render inline math
271
- let tmpInlineElements = tmpContentBody.querySelectorAll('.docuserve-katex-inline');
272
- for (let i = 0; i < tmpInlineElements.length; i++)
273
- {
274
- try
275
- {
276
- katex.render(tmpInlineElements[i].textContent, tmpInlineElements[i], { throwOnError: false, displayMode: false });
277
- }
278
- catch (pError)
279
- {
280
- this.log.warn('KaTeX inline error: ' + pError.message);
281
- }
282
- }
283
-
284
- // Render display math
285
- let tmpDisplayElements = tmpContentBody.querySelectorAll('.docuserve-katex-display');
286
- for (let i = 0; i < tmpDisplayElements.length; i++)
287
- {
288
- try
289
- {
290
- katex.render(tmpDisplayElements[i].textContent, tmpDisplayElements[i], { throwOnError: false, displayMode: true });
291
- }
292
- catch (pError)
293
- {
294
- this.log.warn('KaTeX display error: ' + pError.message);
295
- }
296
- }
69
+ super.displayContent(pHTMLContent, 'Docuserve-Content-Body');
297
70
  }
298
71
 
299
72
  /**
@@ -301,7 +74,7 @@ class DocuserveContentView extends libPictView
301
74
  */
302
75
  showLoading()
303
76
  {
304
- this.pict.ContentAssignment.assignContent('#Docuserve-Content-Body', '<div class="docuserve-content-loading">Loading documentation...</div>');
77
+ super.showLoading('Loading documentation...', 'Docuserve-Content-Body');
305
78
  }
306
79
  }
307
80