pict-section-inlinedocumentation 0.0.4 → 0.0.5

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-section-inlinedocumentation",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Pict embeddable inline documentation browser with topic support",
5
5
  "main": "source/Pict-Section-InlineDocumentation.js",
6
6
  "scripts": {
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "homepage": "https://github.com/stevenvelozo/pict-section-inlinedocumentation#readme",
24
24
  "dependencies": {
25
+ "lunr": "^2.3.9",
25
26
  "pict-provider": "^1.0.12",
26
27
  "pict-section-content": "^0.1.5",
27
28
  "pict-section-markdowneditor": "^1.0.9",
@@ -3,6 +3,7 @@ const libPictSectionContent = require('pict-section-content');
3
3
  const libPictContentProvider = libPictSectionContent.PictContentProvider;
4
4
  const libPictSectionModal = require('pict-section-modal');
5
5
  const libPictSectionMarkdownEditor = require('pict-section-markdowneditor');
6
+ const libLunr = require('lunr');
6
7
 
7
8
  const libViewLayout = require('../views/Pict-View-InlineDocumentation-Layout.js');
8
9
  const libViewContent = require('../views/Pict-View-InlineDocumentation-Content.js');
@@ -90,8 +91,31 @@ class InlineDocumentationProvider extends libPictProvider
90
91
  tmpState.Topics = tmpState.Topics || {};
91
92
  tmpState.NavigationHistory = [];
92
93
 
93
- // Edit mode state
94
- tmpState.EditEnabled = tmpState.EditEnabled || false;
94
+ // Navigation outline state
95
+ tmpState.NavCollapsed = true;
96
+ tmpState.CollapsedGroups = {};
97
+ tmpState.ActiveDocumentHeadings = [];
98
+ tmpState.NavFilterText = '';
99
+
100
+ // Full-text search state
101
+ tmpState.SearchIndexLoaded = false;
102
+ tmpState.SearchQuery = '';
103
+ tmpState.SearchResults = [];
104
+
105
+ // External link resolution — paths starting with / in the
106
+ // sidebar are cross-module references. ExternalDocBaseURL
107
+ // is prepended to make them full URLs opened in a new tab.
108
+ tmpState.ExternalDocBaseURL = tmpOptions.ExternalDocBaseURL || '';
109
+
110
+ // Edit mode state — read from pOptions if provided
111
+ if (tmpOptions.EditEnabled !== undefined)
112
+ {
113
+ tmpState.EditEnabled = !!tmpOptions.EditEnabled;
114
+ }
115
+ else
116
+ {
117
+ tmpState.EditEnabled = tmpState.EditEnabled || false;
118
+ }
95
119
  tmpState.Editing = false;
96
120
  tmpState.EditingPath = '';
97
121
  tmpState.EditingContent = '';
@@ -142,17 +166,23 @@ class InlineDocumentationProvider extends libPictProvider
142
166
  }
143
167
  }
144
168
 
145
- // Load sidebar and topics in parallel
169
+ // Load sidebar, topics, and search index in parallel
146
170
  let tmpPending = 2;
171
+ if (tmpOptions.SearchIndexURL) tmpPending++;
172
+
173
+ let tmpSelf = this;
147
174
  let tmpFinish = () =>
148
175
  {
149
176
  tmpPending--;
150
177
  if (tmpPending <= 0)
151
178
  {
179
+ // Mark sidebar items as external based on ExternalDocBaseURL
180
+ tmpSelf._markExternalSidebarItems();
181
+
152
182
  // Render the layout (which contains nav and content containers)
153
- this.pict.views['InlineDoc-Layout'].render();
183
+ tmpSelf.pict.views['InlineDoc-Layout'].render();
154
184
  // Render the navigation
155
- this.pict.views['InlineDoc-Nav'].render();
185
+ tmpSelf.pict.views['InlineDoc-Nav'].render();
156
186
 
157
187
  return tmpCallback();
158
188
  }
@@ -160,6 +190,11 @@ class InlineDocumentationProvider extends libPictProvider
160
190
 
161
191
  this._loadSidebar(tmpFinish);
162
192
  this._loadTopics(tmpOptions.TopicsURL, tmpFinish);
193
+
194
+ if (tmpOptions.SearchIndexURL)
195
+ {
196
+ this._loadSearchIndex(tmpOptions.SearchIndexURL, tmpFinish);
197
+ }
163
198
  }
164
199
 
165
200
  /**
@@ -226,7 +261,11 @@ class InlineDocumentationProvider extends libPictProvider
226
261
  this._scrollToAnchor(tmpAnchor);
227
262
  }
228
263
 
229
- // Update nav to reflect active document
264
+ // Collapse the nav outline and clear search/filter
265
+ tmpState.NavCollapsed = true;
266
+ tmpState.NavFilterText = '';
267
+ tmpState.SearchQuery = '';
268
+ tmpState.SearchResults = [];
230
269
  this.pict.views['InlineDoc-Nav'].render();
231
270
 
232
271
  return tmpCallback(null, pHTML);
@@ -1615,6 +1654,251 @@ class InlineDocumentationProvider extends libPictProvider
1615
1654
  }, 50);
1616
1655
  }
1617
1656
 
1657
+ // ================================================================
1658
+ // Full-text search
1659
+ // ================================================================
1660
+
1661
+ /**
1662
+ * Load a lunr.js keyword index from a URL.
1663
+ *
1664
+ * The index file is the same format generated by Indoctrinate's
1665
+ * generate_keyword_index command: { LunrIndex, Documents, DocumentCount }.
1666
+ *
1667
+ * @param {string} pURL - URL to the retold-keyword-index.json file
1668
+ * @param {Function} [fCallback] - Callback when done
1669
+ */
1670
+ _loadSearchIndex(pURL, fCallback)
1671
+ {
1672
+ let tmpCallback = (typeof fCallback === 'function') ? fCallback : () => {};
1673
+ let tmpSelf = this;
1674
+
1675
+ if (typeof fetch === 'undefined')
1676
+ {
1677
+ return tmpCallback();
1678
+ }
1679
+
1680
+ fetch(pURL)
1681
+ .then((pResponse) =>
1682
+ {
1683
+ if (!pResponse.ok)
1684
+ {
1685
+ return null;
1686
+ }
1687
+ return pResponse.json();
1688
+ })
1689
+ .then((pIndexData) =>
1690
+ {
1691
+ if (!pIndexData || !pIndexData.LunrIndex || !pIndexData.Documents)
1692
+ {
1693
+ if (tmpSelf.log) tmpSelf.log.info('InlineDocumentation: No keyword index found; search unavailable.');
1694
+ return tmpCallback();
1695
+ }
1696
+
1697
+ try
1698
+ {
1699
+ tmpSelf._LunrIndex = libLunr.Index.load(pIndexData.LunrIndex);
1700
+ tmpSelf._SearchDocuments = pIndexData.Documents;
1701
+
1702
+ let tmpState = tmpSelf.pict.AppData.InlineDocumentation;
1703
+ if (tmpState)
1704
+ {
1705
+ tmpState.SearchIndexLoaded = true;
1706
+ }
1707
+
1708
+ if (tmpSelf.log) tmpSelf.log.info('InlineDocumentation: Search index loaded (' + (pIndexData.DocumentCount || 0) + ' documents).');
1709
+ }
1710
+ catch (pError)
1711
+ {
1712
+ if (tmpSelf.log) tmpSelf.log.warn('InlineDocumentation: Error hydrating lunr index: ' + pError);
1713
+ }
1714
+
1715
+ return tmpCallback();
1716
+ })
1717
+ .catch((pError) =>
1718
+ {
1719
+ if (tmpSelf.log) tmpSelf.log.warn('InlineDocumentation: Error loading search index: ' + pError);
1720
+ return tmpCallback();
1721
+ });
1722
+ }
1723
+
1724
+ /**
1725
+ * Search the loaded lunr index.
1726
+ *
1727
+ * @param {string} pQuery - The search query
1728
+ * @returns {Array} Array of { Key, Title, Group, DocPath, Score }
1729
+ */
1730
+ search(pQuery)
1731
+ {
1732
+ if (!this._LunrIndex || !this._SearchDocuments || !pQuery || !pQuery.trim())
1733
+ {
1734
+ return [];
1735
+ }
1736
+
1737
+ let tmpResults = [];
1738
+
1739
+ try
1740
+ {
1741
+ let tmpLunrResults = this._LunrIndex.search(pQuery);
1742
+
1743
+ for (let i = 0; i < tmpLunrResults.length; i++)
1744
+ {
1745
+ let tmpRef = tmpLunrResults[i].ref;
1746
+ let tmpScore = tmpLunrResults[i].score;
1747
+ let tmpDoc = this._SearchDocuments[tmpRef];
1748
+
1749
+ if (!tmpDoc)
1750
+ {
1751
+ continue;
1752
+ }
1753
+
1754
+ tmpResults.push(
1755
+ {
1756
+ Key: tmpRef,
1757
+ Title: tmpDoc.Title || tmpRef,
1758
+ Group: tmpDoc.Group || '',
1759
+ Module: tmpDoc.Module || '',
1760
+ DocPath: tmpDoc.DocPath || tmpRef,
1761
+ Score: tmpScore
1762
+ });
1763
+ }
1764
+ }
1765
+ catch (pError)
1766
+ {
1767
+ if (this.log) this.log.warn('InlineDocumentation: Search error: ' + pError);
1768
+ }
1769
+
1770
+ return tmpResults;
1771
+ }
1772
+
1773
+ // ================================================================
1774
+ // External link resolution (catalog-based)
1775
+ // ================================================================
1776
+
1777
+ /**
1778
+ * Check if a path is an external reference.
1779
+ *
1780
+ * Paths that started with / in the sidebar are cross-module
1781
+ * references. They get normalized during parsing (leading /
1782
+ * stripped), but they contain 2+ path segments (group/module/).
1783
+ * Simple filenames like README.md are local.
1784
+ *
1785
+ * An ExternalDocBaseURL must be configured for this to return true.
1786
+ *
1787
+ * @param {string} pPath - The normalized document path
1788
+ * @returns {boolean}
1789
+ */
1790
+ isExternalPath(pPath)
1791
+ {
1792
+ if (!pPath) return false;
1793
+
1794
+ let tmpState = this.pict.AppData.InlineDocumentation;
1795
+ if (!tmpState || !tmpState.ExternalDocBaseURL) return false;
1796
+
1797
+ // Paths with 2+ segments (e.g., pict/pict/, fable/fable/)
1798
+ // are cross-module references. Simple filenames are local.
1799
+ let tmpPath = pPath.replace(/^\.\//, '').replace(/^\//, '');
1800
+ let tmpParts = tmpPath.split('/').filter((p) => p.length > 0);
1801
+ return tmpParts.length >= 2;
1802
+ }
1803
+
1804
+ /**
1805
+ * Resolve an external path to a full URL.
1806
+ *
1807
+ * @param {string} pPath - The document path
1808
+ * @returns {string|null} The external URL, or null if not external
1809
+ */
1810
+ resolveExternalURL(pPath)
1811
+ {
1812
+ if (!this.isExternalPath(pPath)) return null;
1813
+
1814
+ let tmpState = this.pict.AppData.InlineDocumentation;
1815
+ let tmpBaseURL = (tmpState && tmpState.ExternalDocBaseURL) || '';
1816
+ if (!tmpBaseURL) return null;
1817
+
1818
+ let tmpPath = pPath.replace(/^\.\//, '').replace(/^\//, '');
1819
+ return tmpBaseURL + tmpPath;
1820
+ }
1821
+
1822
+ /**
1823
+ * After sidebar is loaded, mark items that are external
1824
+ * references with External: true and ExternalURL.
1825
+ */
1826
+ _markExternalSidebarItems()
1827
+ {
1828
+ let tmpState = this.pict.AppData.InlineDocumentation;
1829
+ if (!tmpState || !tmpState.ExternalDocBaseURL) return;
1830
+
1831
+ let tmpGroups = tmpState.SidebarGroups || [];
1832
+ for (let i = 0; i < tmpGroups.length; i++)
1833
+ {
1834
+ let tmpItems = tmpGroups[i].Items || [];
1835
+ for (let j = 0; j < tmpItems.length; j++)
1836
+ {
1837
+ let tmpItem = tmpItems[j];
1838
+ if (this.isExternalPath(tmpItem.Path))
1839
+ {
1840
+ tmpItem.External = true;
1841
+ tmpItem.ExternalURL = this.resolveExternalURL(tmpItem.Path);
1842
+ }
1843
+ }
1844
+ }
1845
+ }
1846
+
1847
+ /**
1848
+ * Extract h2 and h3 headings from the rendered content body.
1849
+ *
1850
+ * Queries #InlineDoc-Content-Body for heading elements, extracts
1851
+ * their text, generates slugified IDs for anchor scrolling, and
1852
+ * stores the results in AppData for the nav outline.
1853
+ *
1854
+ * @returns {Array} Array of { Text, Slug, Level }
1855
+ */
1856
+ _extractHeadings()
1857
+ {
1858
+ let tmpHeadings = [];
1859
+
1860
+ if (typeof document === 'undefined')
1861
+ {
1862
+ return tmpHeadings;
1863
+ }
1864
+
1865
+ let tmpContentBody = document.getElementById('InlineDoc-Content-Body');
1866
+ if (!tmpContentBody)
1867
+ {
1868
+ return tmpHeadings;
1869
+ }
1870
+
1871
+ let tmpElements = tmpContentBody.querySelectorAll('h2, h3');
1872
+
1873
+ for (let i = 0; i < tmpElements.length; i++)
1874
+ {
1875
+ let tmpElement = tmpElements[i];
1876
+ let tmpText = (tmpElement.textContent || '').trim();
1877
+ let tmpLevel = parseInt(tmpElement.tagName.substring(1), 10);
1878
+ let tmpSlug = tmpText.toLowerCase()
1879
+ .replace(/[^a-z0-9\s-]/g, '')
1880
+ .replace(/\s+/g, '-')
1881
+ .replace(/-+/g, '-');
1882
+
1883
+ // Assign the ID to the heading element if not already set
1884
+ // so scrollIntoView can find it
1885
+ if (!tmpElement.id)
1886
+ {
1887
+ tmpElement.id = tmpSlug;
1888
+ }
1889
+
1890
+ tmpHeadings.push({ Text: tmpText, Slug: tmpSlug, Level: tmpLevel });
1891
+ }
1892
+
1893
+ let tmpState = this.pict.AppData.InlineDocumentation;
1894
+ if (tmpState)
1895
+ {
1896
+ tmpState.ActiveDocumentHeadings = tmpHeadings;
1897
+ }
1898
+
1899
+ return tmpHeadings;
1900
+ }
1901
+
1618
1902
  // -- Internal methods --
1619
1903
 
1620
1904
  /**
@@ -150,6 +150,62 @@ const _ViewConfiguration =
150
150
  font-size: 0.85em;
151
151
  color: #9E6B47;
152
152
  }
153
+ /* Code block action buttons (copy, fullscreen) from pict-section-content */
154
+ .pict-content-code-actions {
155
+ position: sticky;
156
+ top: 64px;
157
+ align-self: flex-start;
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 6px;
161
+ flex: 0 0 auto;
162
+ padding-top: 6px;
163
+ opacity: 0;
164
+ transform: translateX(-4px);
165
+ transition: opacity 0.15s ease, transform 0.15s ease;
166
+ pointer-events: none;
167
+ }
168
+ .pict-content-code-container:hover .pict-content-code-actions,
169
+ .pict-content-code-container:focus-within .pict-content-code-actions {
170
+ opacity: 1;
171
+ transform: translateX(0);
172
+ pointer-events: auto;
173
+ }
174
+ .pict-content-code-action-btn {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ width: 28px;
179
+ height: 28px;
180
+ padding: 0;
181
+ background: #FFFFFF;
182
+ color: #5E5549;
183
+ border: 1px solid #DDD6CA;
184
+ border-radius: 6px;
185
+ cursor: pointer;
186
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
187
+ transition: background-color 0.15s ease, color 0.15s ease;
188
+ }
189
+ .pict-content-code-action-btn svg {
190
+ display: block;
191
+ width: 14px;
192
+ height: 14px;
193
+ stroke: currentColor;
194
+ fill: none;
195
+ stroke-width: 1.6;
196
+ stroke-linecap: round;
197
+ stroke-linejoin: round;
198
+ }
199
+ .pict-content-code-action-btn:hover {
200
+ background: #2E7D74;
201
+ color: #FFFFFF;
202
+ border-color: #2E7D74;
203
+ }
204
+ .pict-content-code-action-btn.is-copied {
205
+ background: #2E7D74;
206
+ color: #FFFFFF;
207
+ border-color: #2E7D74;
208
+ }
153
209
  .pict-inline-doc-edit-toolbar {
154
210
  display: none;
155
211
  align-items: center;
@@ -525,6 +581,19 @@ class InlineDocumentationContentView extends libPictContentView
525
581
  let tmpRel = tmpLink.getAttribute('rel');
526
582
  let tmpPath = tmpRel.replace('pict-inline-doc-link:', '');
527
583
 
584
+ // Check if this is a cross-module link that should open externally
585
+ if (tmpProvider && typeof tmpProvider.isExternalPath === 'function' && tmpProvider.isExternalPath(tmpPath))
586
+ {
587
+ let tmpExternalURL = tmpProvider.resolveExternalURL(tmpPath);
588
+ if (tmpExternalURL)
589
+ {
590
+ tmpLink.setAttribute('href', tmpExternalURL);
591
+ tmpLink.setAttribute('target', '_blank');
592
+ tmpLink.setAttribute('rel', 'noopener');
593
+ continue;
594
+ }
595
+ }
596
+
528
597
  tmpLink.addEventListener('click', (pEvent) =>
529
598
  {
530
599
  pEvent.preventDefault();
@@ -42,7 +42,7 @@ const _ViewConfiguration =
42
42
  .pict-inline-doc-nav-container.pict-inline-doc-nav-hidden {
43
43
  display: none;
44
44
  }
45
- /* Compact mode: nav moves to a horizontal top bar when container is narrow */
45
+ /* Compact mode: stack nav above content when container is narrow */
46
46
  .pict-inline-doc.pict-inline-doc-compact {
47
47
  flex-direction: column;
48
48
  }
@@ -53,54 +53,7 @@ const _ViewConfiguration =
53
53
  border-right: none;
54
54
  border-bottom: 1px solid #E5DED4;
55
55
  overflow-y: visible;
56
- overflow-x: auto;
57
56
  flex-shrink: 0;
58
- max-height: none;
59
- }
60
- /* Compact mode: nav items flow horizontally */
61
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav {
62
- padding: 0.4em 0.5em;
63
- display: flex;
64
- flex-wrap: wrap;
65
- align-items: center;
66
- gap: 0.15em 0.3em;
67
- }
68
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-group {
69
- display: flex;
70
- flex-wrap: wrap;
71
- align-items: center;
72
- margin-bottom: 0;
73
- gap: 0.1em 0.2em;
74
- }
75
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-group-header {
76
- padding: 0.2em 0.5em;
77
- font-size: 0.75em;
78
- white-space: nowrap;
79
- }
80
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-group-toggle {
81
- display: none;
82
- }
83
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-group-items {
84
- display: flex !important;
85
- flex-wrap: wrap;
86
- gap: 0.1em;
87
- }
88
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-item {
89
- padding: 0.2em 0.5em;
90
- font-size: 0.8em;
91
- border-left: none;
92
- border-radius: 3px;
93
- white-space: nowrap;
94
- }
95
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-item.active {
96
- border-left: none;
97
- background: #2E7D74;
98
- color: #fff;
99
- }
100
- .pict-inline-doc.pict-inline-doc-compact .pict-inline-doc-nav-topic-badge {
101
- margin: 0 0.3em;
102
- padding: 0.15em 0.5em;
103
- font-size: 0.75em;
104
57
  }
105
58
  `,
106
59