pict-docuserve 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,27 +44,44 @@ class DocuserveDocumentationProvider extends libPictProvider
44
44
  {
45
45
  return (pHref, pLinkText) =>
46
46
  {
47
+ let tmpHref = String(pHref || '');
48
+ let tmpIsModuleMode = (this.getDocsMode() === 'module');
49
+
47
50
  // Built example applications (and other static .html pages) are
48
51
  // served as plain files alongside the docs. Link straight to
49
- // them in a new tab rather than SPA-routing through #/page/.
50
- // Scoped strictly to the .html extension .md pages, catalog
51
- // routes and http(s):// links fall through unaffected.
52
- if (!pHref.match(/^[a-z][a-z0-9+.-]*:/i) && pHref.match(/\.html($|[?#])/i))
53
- {
54
- return { href: pHref, target: '_blank', rel: 'noopener' };
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' };
55
60
  }
56
61
  // Convert internal doc links to hash routes
57
- if (pHref.match(/^\//) || pHref.match(/^[^:]+\.md/))
62
+ if (tmpHref.match(/^\//) || tmpHref.match(/^[^:]+\.md/))
58
63
  {
59
- let tmpRoute = this.convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
64
+ let tmpRoute = this.convertDocLink(tmpHref, pCurrentGroup, pCurrentModule, pCurrentDocPath);
60
65
  return { href: tmpRoute };
61
66
  }
62
67
  // Check if this is a GitHub URL that matches a catalog module
63
- let tmpCatalogRoute = this.resolveGitHubURLToRoute(pHref);
68
+ let tmpCatalogRoute = this.resolveGitHubURLToRoute(tmpHref);
64
69
  if (tmpCatalogRoute)
65
70
  {
66
71
  return { href: tmpCatalogRoute };
67
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
+ }
68
85
  // Use default behavior for other links
69
86
  return null;
70
87
  };
@@ -316,15 +333,38 @@ class DocuserveDocumentationProvider extends libPictProvider
316
333
  Tagline: '',
317
334
  Description: '',
318
335
  Highlights: [],
319
- Actions: []
336
+ Actions: [],
337
+ ExamplesMarkdown: ''
320
338
  };
321
339
 
322
340
  let tmpLines = pMarkdown.split('\n');
341
+ let tmpInExamples = false;
323
342
 
324
343
  for (let i = 0; i < tmpLines.length; i++)
325
344
  {
326
345
  let tmpLine = tmpLines[i].trim();
327
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
+
328
368
  if (!tmpLine)
329
369
  {
330
370
  continue;
@@ -783,14 +823,38 @@ class DocuserveDocumentationProvider extends libPictProvider
783
823
 
784
824
  /**
785
825
  * Module-mode link resolution: every internal documentation reference is
786
- * a local page. Normalizes an href to a #/page/ hash route.
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.
787
831
  *
788
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.
789
837
  * @returns {string} A #/page/ hash route (#/Home for an empty path)
790
838
  */
791
- _toModulePageRoute(pHref)
839
+ _toModulePageRoute(pHref, pCurrentDocPath)
792
840
  {
793
- let tmpPath = String(pHref || '').replace(/^\.\//, '').replace(/^\//, '').replace(/\/+$/, '');
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);
794
858
  if (!tmpPath)
795
859
  {
796
860
  return '#/Home';
@@ -798,6 +862,76 @@ class DocuserveDocumentationProvider extends libPictProvider
798
862
  return '#/page/' + tmpPath.replace(/\.md$/i, '');
799
863
  }
800
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
+
801
935
  /**
802
936
  * Check whether a group/module pair exists in the loaded catalog.
803
937
  *
@@ -1661,10 +1795,11 @@ class DocuserveDocumentationProvider extends libPictProvider
1661
1795
  */
1662
1796
  convertDocLink(pHref, pCurrentGroup, pCurrentModule, pCurrentDocPath)
1663
1797
  {
1664
- // Single-module docs site: every internal reference is a local page.
1798
+ // Single-module docs site: every internal reference is a local page,
1799
+ // resolved relative to the current document's directory.
1665
1800
  if (this.getDocsMode() === 'module')
1666
1801
  {
1667
- return this._toModulePageRoute(pHref);
1802
+ return this._toModulePageRoute(pHref, pCurrentDocPath);
1668
1803
  }
1669
1804
 
1670
1805
  // Strip leading ./ prefix for relative paths
@@ -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.