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.
@@ -17,6 +17,7 @@ class DocuserveDocumentationProvider extends libPictProvider
17
17
  super(pFable, pOptions, pServiceHash);
18
18
 
19
19
  this._Catalog = null;
20
+ this._KeywordIndexMode = null;
20
21
  this._ContentCache = {};
21
22
  // (group, module) -> playground config | null (negative cache).
22
23
  // Loaded lazily by loadPlaygroundConfig on first navigation into
@@ -43,18 +44,44 @@ class DocuserveDocumentationProvider extends libPictProvider
43
44
  {
44
45
  return (pHref, pLinkText) =>
45
46
  {
47
+ let tmpHref = String(pHref || '');
48
+ let tmpIsModuleMode = (this.getDocsMode() === 'module');
49
+
50
+ // Built example applications (and other static .html pages) are
51
+ // served as plain files alongside the docs. Link straight to
52
+ // them in a new tab rather than SPA-routing through #/page/. In
53
+ // module mode the href is resolved against the current
54
+ // document's directory, exactly the way a .md link is — so every
55
+ // relative link in the docs shares one base.
56
+ if (!tmpHref.match(/^[a-z][a-z0-9+.-]*:/i) && tmpHref.match(/\.html($|[?#])/i))
57
+ {
58
+ let tmpAssetHref = tmpIsModuleMode ? this._toModuleAssetHref(tmpHref, pCurrentDocPath) : tmpHref;
59
+ return { href: tmpAssetHref, target: '_blank', rel: 'noopener' };
60
+ }
46
61
  // Convert internal doc links to hash routes
47
- if (pHref.match(/^\//) || pHref.match(/^[^:]+\.md/))
62
+ if (tmpHref.match(/^\//) || tmpHref.match(/^[^:]+\.md/))
48
63
  {
49
- let tmpRoute = this.convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
64
+ let tmpRoute = this.convertDocLink(tmpHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
50
65
  return { href: tmpRoute };
51
66
  }
52
67
  // Check if this is a GitHub URL that matches a catalog module
53
- let tmpCatalogRoute = this.resolveGitHubURLToRoute(pHref);
68
+ let tmpCatalogRoute = this.resolveGitHubURLToRoute(tmpHref);
54
69
  if (tmpCatalogRoute)
55
70
  {
56
71
  return { href: tmpCatalogRoute };
57
72
  }
73
+ // Module mode: a remaining relative link (a directory, a media
74
+ // file, a .json) is resolved against the current document's
75
+ // directory too, so it shares the one base every other link uses
76
+ // rather than silently falling back to the docs root.
77
+ if (tmpIsModuleMode
78
+ && tmpHref
79
+ && (tmpHref.charAt(0) !== '#')
80
+ && (tmpHref.indexOf('//') !== 0)
81
+ && !tmpHref.match(/^[a-z][a-z0-9+.-]*:/i))
82
+ {
83
+ return { href: this._toModuleAssetHref(tmpHref, pCurrentDocPath) };
84
+ }
58
85
  // Use default behavior for other links
59
86
  return null;
60
87
  };
@@ -306,15 +333,38 @@ class DocuserveDocumentationProvider extends libPictProvider
306
333
  Tagline: '',
307
334
  Description: '',
308
335
  Highlights: [],
309
- Actions: []
336
+ Actions: [],
337
+ ExamplesMarkdown: ''
310
338
  };
311
339
 
312
340
  let tmpLines = pMarkdown.split('\n');
341
+ let tmpInExamples = false;
313
342
 
314
343
  for (let i = 0; i < tmpLines.length; i++)
315
344
  {
316
345
  let tmpLine = tmpLines[i].trim();
317
346
 
347
+ // Generated examples region — collected verbatim for the splash's
348
+ // "Interactive Examples" section, never parsed as cover fields.
349
+ if (tmpLine === '<!-- docuserve:examples:start -->')
350
+ {
351
+ tmpInExamples = true;
352
+ continue;
353
+ }
354
+ if (tmpLine === '<!-- docuserve:examples:end -->')
355
+ {
356
+ tmpInExamples = false;
357
+ continue;
358
+ }
359
+ if (tmpInExamples)
360
+ {
361
+ if (tmpLine)
362
+ {
363
+ tmpCover.ExamplesMarkdown += (tmpCover.ExamplesMarkdown ? '\n' : '') + tmpLine;
364
+ }
365
+ continue;
366
+ }
367
+
318
368
  if (!tmpLine)
319
369
  {
320
370
  continue;
@@ -728,6 +778,7 @@ class DocuserveDocumentationProvider extends libPictProvider
728
778
  {
729
779
  this._LunrIndex = libLunr.Index.load(pIndexData.LunrIndex);
730
780
  this._KeywordDocuments = pIndexData.Documents;
781
+ this._KeywordIndexMode = (pIndexData.Mode === 'module' || pIndexData.Mode === 'ecosystem') ? pIndexData.Mode : null;
731
782
  this.pict.AppData.Docuserve.KeywordIndexLoaded = true;
732
783
  this.pict.AppData.Docuserve.KeywordDocumentCount = pIndexData.DocumentCount || 0;
733
784
  this.log.info(`Docuserve: Keyword index loaded (${pIndexData.DocumentCount || 0} documents).`);
@@ -746,6 +797,141 @@ class DocuserveDocumentationProvider extends libPictProvider
746
797
  });
747
798
  }
748
799
 
800
+ /**
801
+ * Resolve the documentation site mode.
802
+ *
803
+ * 'module' — a single module's own docs site; every doc is a local
804
+ * page (#/page/<docpath>).
805
+ * 'ecosystem' — a catalog of <group>/<module> repos (#/doc/...).
806
+ * 'legacy' — built before the Mode stamp existed; callers keep the
807
+ * pre-Mode heuristic so old docs sites are unaffected.
808
+ *
809
+ * @returns {string} 'module' | 'ecosystem' | 'legacy'
810
+ */
811
+ getDocsMode()
812
+ {
813
+ if (this._Catalog && (this._Catalog.Mode === 'module' || this._Catalog.Mode === 'ecosystem'))
814
+ {
815
+ return this._Catalog.Mode;
816
+ }
817
+ if (this._KeywordIndexMode === 'module' || this._KeywordIndexMode === 'ecosystem')
818
+ {
819
+ return this._KeywordIndexMode;
820
+ }
821
+ return 'legacy';
822
+ }
823
+
824
+ /**
825
+ * Module-mode link resolution: every internal documentation reference is
826
+ * a local page. Relative links (./sibling.md, ../other.md, bare names)
827
+ * resolve against the directory of the document that contains them — the
828
+ * way the links read on disk — while a /-rooted href resolves against the
829
+ * docs root. "." and ".." segments are collapsed; ".." is clamped at the
830
+ * docs root so a link can never escape above it.
831
+ *
832
+ * @param {string} pHref - The raw link href
833
+ * @param {string} [pCurrentDocPath] - Docs-root-relative path of the
834
+ * document the link lives in (e.g.
835
+ * "examples/gradebook/README.md"). Absent for root-level
836
+ * contexts such as the sidebar.
837
+ * @returns {string} A #/page/ hash route (#/Home for an empty path)
838
+ */
839
+ _toModulePageRoute(pHref, pCurrentDocPath)
840
+ {
841
+ let tmpHref = String(pHref || '').trim();
842
+
843
+ // A /-rooted href resolves against the docs root; every other href
844
+ // resolves against the current document's directory.
845
+ let tmpBaseDir = '';
846
+ if (tmpHref.charAt(0) === '/')
847
+ {
848
+ tmpHref = tmpHref.replace(/^\/+/, '');
849
+ }
850
+ else if (pCurrentDocPath)
851
+ {
852
+ let tmpDirParts = String(pCurrentDocPath).split('/');
853
+ tmpDirParts.pop();
854
+ tmpBaseDir = tmpDirParts.join('/');
855
+ }
856
+
857
+ let tmpPath = this._resolveRelativeDocPath(tmpBaseDir, tmpHref);
858
+ if (!tmpPath)
859
+ {
860
+ return '#/Home';
861
+ }
862
+ return '#/page/' + tmpPath.replace(/\.md$/i, '');
863
+ }
864
+
865
+ /**
866
+ * Resolve a relative href against a base directory, collapsing "." and
867
+ * ".." segments. ".." is clamped at the docs root — it can never escape
868
+ * above it. Both arguments are POSIX-style docs-root-relative paths.
869
+ *
870
+ * @param {string} pBaseDir - The directory the href is relative to.
871
+ * @param {string} pHref - The href to resolve.
872
+ * @returns {string} The resolved docs-root-relative path (no leading slash).
873
+ */
874
+ _resolveRelativeDocPath(pBaseDir, pHref)
875
+ {
876
+ let tmpSegments = [];
877
+ let tmpCombined = (pBaseDir ? pBaseDir + '/' : '') + String(pHref || '');
878
+ let tmpParts = tmpCombined.split('/');
879
+ for (let i = 0; i < tmpParts.length; i++)
880
+ {
881
+ let tmpPart = tmpParts[i];
882
+ if ((tmpPart === '') || (tmpPart === '.'))
883
+ {
884
+ continue;
885
+ }
886
+ if (tmpPart === '..')
887
+ {
888
+ if (tmpSegments.length > 0)
889
+ {
890
+ tmpSegments.pop();
891
+ }
892
+ continue;
893
+ }
894
+ tmpSegments.push(tmpPart);
895
+ }
896
+ return tmpSegments.join('/');
897
+ }
898
+
899
+ /**
900
+ * Module-mode resolution for a non-routed link — a built .html page, a
901
+ * media file, a directory. Resolves the href against the current
902
+ * document's directory (a /-rooted href against the docs root), the same
903
+ * way _toModulePageRoute resolves a .md link, and returns a plain
904
+ * docs-root-relative href. The browser resolves that href against the
905
+ * docs-root index.html, so it points at the right file from any page.
906
+ *
907
+ * @param {string} pHref - The raw link href
908
+ * @param {string} [pCurrentDocPath] - Docs-root-relative path of the
909
+ * document the link lives in.
910
+ * @returns {string} A docs-root-relative href.
911
+ */
912
+ _toModuleAssetHref(pHref, pCurrentDocPath)
913
+ {
914
+ let tmpHref = String(pHref || '').trim();
915
+ if (!tmpHref)
916
+ {
917
+ return tmpHref;
918
+ }
919
+
920
+ let tmpBaseDir = '';
921
+ if (tmpHref.charAt(0) === '/')
922
+ {
923
+ tmpHref = tmpHref.replace(/^\/+/, '');
924
+ }
925
+ else if (pCurrentDocPath)
926
+ {
927
+ let tmpDirParts = String(pCurrentDocPath).split('/');
928
+ tmpDirParts.pop();
929
+ tmpBaseDir = tmpDirParts.join('/');
930
+ }
931
+
932
+ return this._resolveRelativeDocPath(tmpBaseDir, tmpHref);
933
+ }
934
+
749
935
  /**
750
936
  * Check whether a group/module pair exists in the loaded catalog.
751
937
  *
@@ -864,6 +1050,7 @@ class DocuserveDocumentationProvider extends libPictProvider
864
1050
  try
865
1051
  {
866
1052
  let tmpLunrResults = this._LunrIndex.search(pQuery);
1053
+ let tmpMode = this.getDocsMode();
867
1054
 
868
1055
  for (let i = 0; i < tmpLunrResults.length; i++)
869
1056
  {
@@ -876,24 +1063,37 @@ class DocuserveDocumentationProvider extends libPictProvider
876
1063
  continue;
877
1064
  }
878
1065
 
879
- // Build the hash route from the document key (group/module/docpath)
880
- let tmpParts = tmpRef.split('/');
1066
+ // Build the hash route for this result based on the site mode.
881
1067
  let tmpRoute = '';
882
- if (tmpParts.length >= 2)
1068
+ if (tmpMode === 'module')
883
1069
  {
884
- // Check whether this group/module exists in the catalog.
885
- // If it does, route to #/doc/ which fetches from GitHub.
886
- // If not, fall back to #/page/ which fetches locally.
887
- let tmpGroup = tmpParts[0];
888
- let tmpModule = tmpParts[1];
889
-
890
- if (this.isModuleInCatalog(tmpGroup, tmpModule))
1070
+ // Single-module site: every doc is a local page; the
1071
+ // keyword-index key is the docs-relative path.
1072
+ tmpRoute = '#/page/' + (tmpDoc.DocPath || tmpRef);
1073
+ }
1074
+ else if (tmpMode === 'ecosystem')
1075
+ {
1076
+ // Ecosystem: catalog modules render from their GitHub docs.
1077
+ if (tmpDoc.Group && tmpDoc.Module && tmpDoc.DocPath)
1078
+ {
1079
+ tmpRoute = '#/doc/' + tmpDoc.Group + '/' + tmpDoc.Module + '/' + tmpDoc.DocPath;
1080
+ }
1081
+ else
1082
+ {
1083
+ tmpRoute = '#/page/' + tmpRef;
1084
+ }
1085
+ }
1086
+ else
1087
+ {
1088
+ // Legacy keyword index (no Mode stamp) — the pre-Mode
1089
+ // heuristic: split the key and check the catalog.
1090
+ let tmpParts = tmpRef.split('/');
1091
+ if (tmpParts.length >= 2 && this.isModuleInCatalog(tmpParts[0], tmpParts[1]))
891
1092
  {
892
1093
  tmpRoute = '#/doc/' + tmpRef;
893
1094
  }
894
1095
  else
895
1096
  {
896
- // Local document — route via #/page/ using the full ref path
897
1097
  tmpRoute = '#/page/' + tmpRef;
898
1098
  }
899
1099
  }
@@ -1076,6 +1276,14 @@ class DocuserveDocumentationProvider extends libPictProvider
1076
1276
  return '';
1077
1277
  }
1078
1278
 
1279
+ // Already a fully-formed hash route (e.g. "#/page/examples/foo/README").
1280
+ // Pass it straight through — the author has named the exact route, so
1281
+ // do not re-derive one (re-deriving would mangle it into "#/page/#/...").
1282
+ if (pHref.match(/^#\//))
1283
+ {
1284
+ return pHref;
1285
+ }
1286
+
1079
1287
  // Root home link
1080
1288
  if (pHref === '/')
1081
1289
  {
@@ -1112,6 +1320,21 @@ class DocuserveDocumentationProvider extends libPictProvider
1112
1320
  return '#/Home';
1113
1321
  }
1114
1322
 
1323
+ // Static .html pages (built example apps, etc.) — link straight to
1324
+ // the file rather than SPA-routing it through #/page/. Scoped
1325
+ // strictly to the .html extension.
1326
+ if (pHref.match(/\.html($|[?#])/i) && !pHref.match(/^[a-z][a-z0-9+.-]*:/i))
1327
+ {
1328
+ return pHref;
1329
+ }
1330
+
1331
+ // Single-module docs site: every internal reference is a local page —
1332
+ // no catalog #/doc/ routing, no group/module guesswork.
1333
+ if (this.getDocsMode() === 'module')
1334
+ {
1335
+ return this._toModulePageRoute(pHref);
1336
+ }
1337
+
1115
1338
  // Strip leading/trailing slashes for parsing
1116
1339
  let tmpPath = pHref.replace(/^\//, '').replace(/\/$/, '');
1117
1340
 
@@ -1572,6 +1795,13 @@ class DocuserveDocumentationProvider extends libPictProvider
1572
1795
  */
1573
1796
  convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath)
1574
1797
  {
1798
+ // Single-module docs site: every internal reference is a local page,
1799
+ // resolved relative to the current document's directory.
1800
+ if (this.getDocsMode() === 'module')
1801
+ {
1802
+ return this._toModulePageRoute(pHref, pCurrentDocPath);
1803
+ }
1804
+
1575
1805
  // Strip leading ./ prefix for relative paths
1576
1806
  let tmpPath = pHref.replace(/^\.\//, '');
1577
1807
  // Remove leading slash
@@ -111,6 +111,70 @@ const _ViewConfiguration =
111
111
  border-color: var(--theme-color-brand-primary-hover, #236660);
112
112
  color: var(--theme-color-brand-primary, #2E7D74);
113
113
  }
114
+ .docuserve-splash-examples {
115
+ max-width: 900px;
116
+ width: 100%;
117
+ margin-bottom: 2.5em;
118
+ }
119
+ /* No staged examples — collapse the section entirely. */
120
+ .docuserve-splash-examples:empty {
121
+ display: none;
122
+ margin: 0;
123
+ }
124
+ .docuserve-splash-examples-heading {
125
+ font-size: 0.95em;
126
+ font-weight: 700;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.08em;
129
+ color: var(--theme-color-text-muted, #8A7F72);
130
+ margin: 0 0 0.85em 0;
131
+ }
132
+ .docuserve-splash-examples table {
133
+ width: 100%;
134
+ border-collapse: collapse;
135
+ background: var(--theme-color-background-panel, #FFFFFF);
136
+ border: 1px solid var(--theme-color-border-default, #DDD6CA);
137
+ border-radius: 8px;
138
+ overflow: hidden;
139
+ }
140
+ .docuserve-splash-examples thead th {
141
+ text-align: left;
142
+ font-size: 0.72em;
143
+ font-weight: 700;
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.06em;
146
+ color: var(--theme-color-text-muted, #8A7F72);
147
+ padding: 0.7em 1.1em;
148
+ background: var(--theme-color-background-tertiary, #F4EFE6);
149
+ }
150
+ .docuserve-splash-examples tbody td {
151
+ padding: 0.7em 1.1em;
152
+ border-top: 1px solid var(--theme-color-border-default, #DDD6CA);
153
+ font-size: 0.9em;
154
+ color: var(--theme-color-text-secondary, #5E5549);
155
+ text-align: left;
156
+ }
157
+ .docuserve-splash-examples tbody tr:hover td {
158
+ background: var(--theme-color-background-tertiary, #F4EFE6);
159
+ }
160
+ .docuserve-splash-examples a {
161
+ color: var(--theme-color-brand-primary, #2E7D74);
162
+ font-weight: 600;
163
+ text-decoration: none;
164
+ }
165
+ .docuserve-splash-examples a:hover {
166
+ text-decoration: underline;
167
+ }
168
+ /* docs/README.md content rendered beneath the hero. */
169
+ .docuserve-splash-readme {
170
+ max-width: 820px;
171
+ margin: 0 auto;
172
+ padding: 3.5em 2em 5em 2em;
173
+ text-align: left;
174
+ }
175
+ .docuserve-splash-readme:empty {
176
+ display: none;
177
+ }
114
178
  `,
115
179
 
116
180
  Templates:
@@ -123,8 +187,10 @@ const _ViewConfiguration =
123
187
  <div class="docuserve-splash-tagline" id="Docuserve-Splash-Tagline"></div>
124
188
  <div class="docuserve-splash-description" id="Docuserve-Splash-Description"></div>
125
189
  <div class="docuserve-splash-highlights" id="Docuserve-Splash-Highlights"></div>
190
+ <div class="docuserve-splash-examples" id="Docuserve-Splash-Examples"></div>
126
191
  <div class="docuserve-splash-actions" id="Docuserve-Splash-Actions"></div>
127
192
  </div>
193
+ <div class="docuserve-splash-readme" id="Docuserve-Splash-Readme"></div>
128
194
  `
129
195
  }
130
196
  ],
@@ -154,12 +220,17 @@ class DocusserveSplashView extends libPictView
154
220
  if (tmpDocuserve.CoverLoaded && tmpDocuserve.Cover)
155
221
  {
156
222
  this.renderFromCover(tmpDocuserve.Cover);
223
+ this.renderExamples(tmpDocuserve.Cover);
157
224
  }
158
225
  else
159
226
  {
160
227
  this.renderFromCatalog(tmpDocuserve);
161
228
  }
162
229
 
230
+ // Render docs/README.md beneath the hero — the splash fills the
231
+ // viewport above the fold, the README content follows on scroll.
232
+ this.renderReadme();
233
+
163
234
  return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
164
235
  }
165
236
 
@@ -270,6 +341,61 @@ class DocusserveSplashView extends libPictView
270
341
  this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Actions', '');
271
342
  }
272
343
 
344
+ /**
345
+ * Render the "Interactive Examples" section of the splash from the
346
+ * examples region of _cover.md. When the cover carries no examples the
347
+ * section is left empty — CSS collapses it — so it appears only when a
348
+ * module has staged interactive examples.
349
+ *
350
+ * @param {Object} pCover - The parsed cover data.
351
+ */
352
+ renderExamples(pCover)
353
+ {
354
+ let tmpExamplesMarkdown = (pCover && pCover.ExamplesMarkdown) ? pCover.ExamplesMarkdown : '';
355
+ let tmpDocProvider = this.pict.providers['Docuserve-Documentation'];
356
+
357
+ if (!tmpExamplesMarkdown || !tmpDocProvider || !tmpDocProvider._ContentProvider)
358
+ {
359
+ this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Examples', '');
360
+ return;
361
+ }
362
+
363
+ let tmpLinkResolver = tmpDocProvider._createLinkResolver('', '', '');
364
+ let tmpExamplesHTML = tmpDocProvider._ContentProvider.parseMarkdown(tmpExamplesMarkdown, tmpLinkResolver);
365
+ this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Examples',
366
+ '<h2 class="docuserve-splash-examples-heading">Interactive Examples</h2>' + tmpExamplesHTML);
367
+ }
368
+
369
+ /**
370
+ * Render docs/README.md beneath the splash hero. The landing page is the
371
+ * full-viewport splash above the fold and the module's README content on
372
+ * scroll. A missing or unreadable README simply leaves the section empty.
373
+ */
374
+ renderReadme()
375
+ {
376
+ let tmpDocProvider = this.pict.providers['Docuserve-Documentation'];
377
+ let tmpDocsBase = this.pict.AppData.Docuserve.DocsBaseURL || '';
378
+
379
+ fetch(tmpDocsBase + 'README.md')
380
+ .then((pResponse) => (pResponse.ok ? pResponse.text() : null))
381
+ .then((pMarkdown) =>
382
+ {
383
+ if (!pMarkdown || !tmpDocProvider || !tmpDocProvider._ContentProvider)
384
+ {
385
+ this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Readme', '');
386
+ return;
387
+ }
388
+ let tmpLinkResolver = tmpDocProvider._createLinkResolver('', '', 'README.md');
389
+ let tmpImageResolver = tmpDocProvider._createImageResolver(tmpDocsBase + 'README.md');
390
+ let tmpHTML = tmpDocProvider._ContentProvider.parseMarkdown(pMarkdown, tmpLinkResolver, tmpImageResolver);
391
+ this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Readme', '<div class="pict-content">' + tmpHTML + '</div>');
392
+ })
393
+ .catch(() =>
394
+ {
395
+ this.pict.ContentAssignment.assignContent('#Docuserve-Splash-Readme', '');
396
+ });
397
+ }
398
+
273
399
  /**
274
400
  * Sanitize a title string, preserving only <small> tags.
275
401
  * All other HTML is escaped.