pict-section-inlinedocumentation 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.
Files changed (51) hide show
  1. package/README.md +107 -0
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +83 -0
  4. package/docs/_cover.md +15 -0
  5. package/docs/_sidebar.md +24 -0
  6. package/docs/_topbar.md +8 -0
  7. package/docs/_version.json +7 -0
  8. package/docs/api-reference.md +185 -0
  9. package/docs/architecture.md +103 -0
  10. package/docs/css/docuserve.css +327 -0
  11. package/docs/embedding-level1-sidebar.md +92 -0
  12. package/docs/embedding-level2-routes.md +86 -0
  13. package/docs/embedding-level3-tooltips.md +97 -0
  14. package/docs/embedding-level4-autogen.md +126 -0
  15. package/docs/index.html +39 -0
  16. package/docs/overview.md +42 -0
  17. package/docs/quickstart.md +95 -0
  18. package/docs/reference.md +73 -0
  19. package/docs/retold-catalog.json +181 -0
  20. package/docs/retold-keyword-index.json +4374 -0
  21. package/example_applications/basic/docs/README.md +40 -0
  22. package/example_applications/basic/docs/_sidebar.md +4 -0
  23. package/example_applications/basic/docs/_topics.json +10 -0
  24. package/example_applications/basic/docs/advanced-topics.md +47 -0
  25. package/example_applications/basic/docs/getting-started.md +70 -0
  26. package/example_applications/basic/index.html +100 -0
  27. package/example_applications/bookshop/.quackage.json +10 -0
  28. package/example_applications/bookshop/Pict-Application-Bookshop-Configuration.json +15 -0
  29. package/example_applications/bookshop/Pict-Application-Bookshop.js +218 -0
  30. package/example_applications/bookshop/data/BookshopData.json +65 -0
  31. package/example_applications/bookshop/data/pict_documentation_topics.json +46 -0
  32. package/example_applications/bookshop/docs/_sidebar.md +6 -0
  33. package/example_applications/bookshop/docs/book-detail.md +21 -0
  34. package/example_applications/bookshop/docs/book-list.md +21 -0
  35. package/example_applications/bookshop/docs/search-filter.md +18 -0
  36. package/example_applications/bookshop/docs/store.md +29 -0
  37. package/example_applications/bookshop/docs/welcome.md +23 -0
  38. package/example_applications/bookshop/html/index.html +236 -0
  39. package/example_applications/bookshop/package.json +34 -0
  40. package/example_applications/bookshop/views/PictView-Bookshop-BookList.js +324 -0
  41. package/example_applications/bookshop/views/PictView-Bookshop-HelpToggle.js +44 -0
  42. package/example_applications/bookshop/views/PictView-Bookshop-Store.js +271 -0
  43. package/package.json +55 -0
  44. package/source/Pict-Section-InlineDocumentation.js +10 -0
  45. package/source/providers/Pict-Provider-InlineDocumentation.js +1995 -0
  46. package/source/views/Pict-View-InlineDocumentation-Content.js +542 -0
  47. package/source/views/Pict-View-InlineDocumentation-Layout.js +206 -0
  48. package/source/views/Pict-View-InlineDocumentation-Nav.js +475 -0
  49. package/source/views/Pict-View-InlineDocumentation-TopicManager.js +1623 -0
  50. package/test/Browser_Integration_tests.js +1449 -0
  51. package/test/Pict-Section-InlineDocumentation_test.js +1285 -0
@@ -0,0 +1,1995 @@
1
+ const libPictProvider = require('pict-provider');
2
+ const libPictSectionContent = require('pict-section-content');
3
+ const libPictContentProvider = libPictSectionContent.PictContentProvider;
4
+ const libPictSectionModal = require('pict-section-modal');
5
+ const libPictSectionMarkdownEditor = require('pict-section-markdowneditor');
6
+
7
+ const libViewLayout = require('../views/Pict-View-InlineDocumentation-Layout.js');
8
+ const libViewContent = require('../views/Pict-View-InlineDocumentation-Content.js');
9
+ const libViewNav = require('../views/Pict-View-InlineDocumentation-Nav.js');
10
+ const libViewTopicManager = require('../views/Pict-View-InlineDocumentation-TopicManager.js');
11
+
12
+ /**
13
+ * Inline Documentation Provider
14
+ *
15
+ * The primary API for embedding a documentation browser in a Pict application.
16
+ * Instantiates all necessary views and sub-providers, manages documentation
17
+ * state, and exposes methods for loading documents and navigating topics.
18
+ */
19
+ class InlineDocumentationProvider extends libPictProvider
20
+ {
21
+ constructor(pFable, pOptions, pServiceHash)
22
+ {
23
+ let tmpOptions = Object.assign({},
24
+ JSON.parse(JSON.stringify(_DefaultConfiguration)),
25
+ pOptions);
26
+ super(pFable, tmpOptions, pServiceHash);
27
+
28
+ this._ContentCache = {};
29
+ this._ActiveTooltipBindings = [];
30
+ this._tooltipHelpLinkHandler = null;
31
+
32
+ // Instantiate the content provider for markdown parsing
33
+ this._ContentProvider = this.pict.addProviderSingleton('Pict-Content', libPictContentProvider.default_configuration, libPictContentProvider);
34
+
35
+ // Register views
36
+ this.pict.addViewSingleton('InlineDoc-Layout', libViewLayout.default_configuration, libViewLayout);
37
+ this.pict.addViewSingleton('InlineDoc-Content', libViewContent.default_configuration, libViewContent);
38
+ this.pict.addViewSingleton('InlineDoc-Nav', libViewNav.default_configuration, libViewNav);
39
+ this.pict.addViewSingleton('InlineDoc-TopicManager', libViewTopicManager.default_configuration, libViewTopicManager);
40
+
41
+ // Register pict-section-modal if not already present (needed by topic manager)
42
+ if (!this.pict.views['Pict-Section-Modal'])
43
+ {
44
+ this.pict.addViewSingleton('Pict-Section-Modal', libPictSectionModal.default_configuration, libPictSectionModal);
45
+ }
46
+
47
+ // Register the markdown editor for edit mode
48
+ let tmpEditorConfig = JSON.parse(JSON.stringify(libPictSectionMarkdownEditor.default_configuration));
49
+ tmpEditorConfig.DefaultDestinationAddress = '#InlineDoc-Editor-Container';
50
+ tmpEditorConfig.TargetElementAddress = '#InlineDoc-Editor-Container';
51
+ tmpEditorConfig.ContentDataAddress = 'AppData.InlineDocumentation.EditorSegments';
52
+ tmpEditorConfig.DefaultPreviewMode = 'off';
53
+ tmpEditorConfig.Renderables =
54
+ [
55
+ {
56
+ RenderableHash: 'MarkdownEditor-Wrap',
57
+ TemplateHash: 'MarkdownEditor-Container',
58
+ DestinationAddress: '#InlineDoc-Editor-Container'
59
+ }
60
+ ];
61
+ this.pict.addViewSingleton('InlineDoc-MarkdownEditor', tmpEditorConfig, libPictSectionMarkdownEditor);
62
+ }
63
+
64
+ /**
65
+ * Initialize the inline documentation system.
66
+ *
67
+ * Sets up application state, loads sidebar navigation and optionally
68
+ * topic definitions, then renders the layout.
69
+ *
70
+ * @param {Object} [pOptions] - Options: { DocsBaseURL, TopicsURL, ContainerAddress }
71
+ * @param {Function} [fCallback] - Callback when initialization is complete
72
+ */
73
+ initializeDocumentation(pOptions, fCallback)
74
+ {
75
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
76
+ let tmpOptions = pOptions || {};
77
+
78
+ // Initialize application state
79
+ if (!this.pict.AppData.InlineDocumentation)
80
+ {
81
+ this.pict.AppData.InlineDocumentation = {};
82
+ }
83
+
84
+ let tmpState = this.pict.AppData.InlineDocumentation;
85
+ tmpState.DocsBaseURL = tmpOptions.DocsBaseURL || tmpState.DocsBaseURL || '';
86
+ tmpState.CurrentPath = '';
87
+ tmpState.CurrentRoute = '';
88
+ tmpState.SidebarGroups = tmpState.SidebarGroups || [];
89
+ tmpState.Topic = null;
90
+ tmpState.Topics = tmpState.Topics || {};
91
+ tmpState.NavigationHistory = [];
92
+
93
+ // Edit mode state
94
+ tmpState.EditEnabled = tmpState.EditEnabled || false;
95
+ tmpState.Editing = false;
96
+ tmpState.EditingPath = '';
97
+ tmpState.EditingContent = '';
98
+ tmpState.TooltipEditMode = false;
99
+
100
+ // Store the onSave callback if provided
101
+ if (typeof tmpOptions.onSave === 'function')
102
+ {
103
+ this._onSave = tmpOptions.onSave;
104
+ }
105
+
106
+ // Store the onTopicsSave callback if provided
107
+ if (typeof tmpOptions.onTopicsSave === 'function')
108
+ {
109
+ this._onTopicsSave = tmpOptions.onTopicsSave;
110
+ }
111
+
112
+ // Store the onImageUpload callback if provided
113
+ if (typeof tmpOptions.onImageUpload === 'function')
114
+ {
115
+ this._onImageUpload = tmpOptions.onImageUpload;
116
+ this._wireEditorImageUpload();
117
+ }
118
+
119
+ // Topic manager enabled state
120
+ // If explicitly set, use that; otherwise track EditEnabled
121
+ if (tmpOptions.TopicManagerEnabled !== undefined)
122
+ {
123
+ tmpState.TopicManagerEnabled = !!tmpOptions.TopicManagerEnabled;
124
+ this._topicManagerExplicitlySet = true;
125
+ }
126
+ else
127
+ {
128
+ tmpState.TopicManagerEnabled = tmpState.EditEnabled || false;
129
+ this._topicManagerExplicitlySet = false;
130
+ }
131
+
132
+ // Optionally override the layout container address
133
+ if (tmpOptions.ContainerAddress)
134
+ {
135
+ let tmpLayoutView = this.pict.views['InlineDoc-Layout'];
136
+ if (tmpLayoutView && tmpLayoutView.options && tmpLayoutView.options.Renderables)
137
+ {
138
+ for (let i = 0; i < tmpLayoutView.options.Renderables.length; i++)
139
+ {
140
+ tmpLayoutView.options.Renderables[i].DestinationAddress = tmpOptions.ContainerAddress;
141
+ }
142
+ }
143
+ }
144
+
145
+ // Load sidebar and topics in parallel
146
+ let tmpPending = 2;
147
+ let tmpFinish = () =>
148
+ {
149
+ tmpPending--;
150
+ if (tmpPending <= 0)
151
+ {
152
+ // Render the layout (which contains nav and content containers)
153
+ this.pict.views['InlineDoc-Layout'].render();
154
+ // Render the navigation
155
+ this.pict.views['InlineDoc-Nav'].render();
156
+
157
+ return tmpCallback();
158
+ }
159
+ };
160
+
161
+ this._loadSidebar(tmpFinish);
162
+ this._loadTopics(tmpOptions.TopicsURL, tmpFinish);
163
+ }
164
+
165
+ /**
166
+ * Load and display a markdown document.
167
+ *
168
+ * Fetches the document relative to DocsBaseURL, parses it to HTML via
169
+ * pict-section-content, and displays it in the content view.
170
+ *
171
+ * @param {string} pPath - Relative document path (e.g. 'getting-started.md')
172
+ * @param {Function} [fCallback] - Callback receiving (error, htmlContent)
173
+ */
174
+ loadDocument(pPath, fCallback)
175
+ {
176
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
177
+
178
+ if (!pPath)
179
+ {
180
+ return tmpCallback('No document path provided');
181
+ }
182
+
183
+ let tmpState = this.pict.AppData.InlineDocumentation;
184
+ let tmpContentView = this.pict.views['InlineDoc-Content'];
185
+
186
+ // Parse anchor from path (e.g. 'page.md#section-heading')
187
+ let tmpAnchor = '';
188
+ let tmpPath = pPath;
189
+ let tmpHashIndex = tmpPath.indexOf('#');
190
+ if (tmpHashIndex >= 0)
191
+ {
192
+ tmpAnchor = tmpPath.substring(tmpHashIndex + 1);
193
+ tmpPath = tmpPath.substring(0, tmpHashIndex);
194
+ }
195
+
196
+ // Ensure .md extension
197
+ if (!tmpPath.match(/\.md$/))
198
+ {
199
+ tmpPath = tmpPath + '.md';
200
+ }
201
+
202
+ // Update state
203
+ tmpState.CurrentPath = tmpPath;
204
+ tmpState.NavigationHistory.push(tmpPath);
205
+
206
+ // Render the content view template (creates the container element)
207
+ tmpContentView.render();
208
+ // Show loading indicator
209
+ tmpContentView.showLoading();
210
+
211
+ // Fetch the document
212
+ let tmpURL = (tmpState.DocsBaseURL || '') + tmpPath;
213
+ this._fetchDocument(tmpURL, (pError, pHTML) =>
214
+ {
215
+ if (pError)
216
+ {
217
+ tmpContentView.displayContent(this._getErrorPageHTML(tmpPath));
218
+ return tmpCallback(pError);
219
+ }
220
+
221
+ tmpContentView.displayContent(pHTML);
222
+
223
+ // Scroll to anchor if specified
224
+ if (tmpAnchor)
225
+ {
226
+ this._scrollToAnchor(tmpAnchor);
227
+ }
228
+
229
+ // Update nav to reflect active document
230
+ this.pict.views['InlineDoc-Nav'].render();
231
+
232
+ return tmpCallback(null, pHTML);
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Set the active topic, filtering navigation to that topic's documents.
238
+ *
239
+ * @param {string} pTopicKey - The topic key (TopicCode from pict_documentation_topics.json)
240
+ */
241
+ setTopic(pTopicKey)
242
+ {
243
+ let tmpState = this.pict.AppData.InlineDocumentation;
244
+
245
+ if (!pTopicKey || !tmpState.Topics || !tmpState.Topics[pTopicKey])
246
+ {
247
+ this.log.warn(`InlineDocumentation: Topic [${pTopicKey}] not found.`);
248
+ return;
249
+ }
250
+
251
+ tmpState.Topic = pTopicKey;
252
+
253
+ // Re-render navigation with topic filter
254
+ this.pict.views['InlineDoc-Nav'].render();
255
+ }
256
+
257
+ /**
258
+ * Load and display a topic's help document by TopicCode.
259
+ *
260
+ * Looks up the topic in the pict_documentation_topics.json format,
261
+ * sets it as active, and loads its TopicHelpFilePath.
262
+ *
263
+ * @param {string} pTopicCode - The TopicCode (e.g. 'BOOKSHOP-BOOKLIST')
264
+ * @param {Function} [fCallback] - Callback receiving (error, htmlContent)
265
+ */
266
+ loadTopicDocument(pTopicCode, fCallback)
267
+ {
268
+ let tmpState = this.pict.AppData.InlineDocumentation;
269
+
270
+ if (!pTopicCode || !tmpState.Topics || !tmpState.Topics[pTopicCode])
271
+ {
272
+ this.log.warn(`InlineDocumentation: Topic [${pTopicCode}] not found.`);
273
+ if (typeof fCallback === 'function')
274
+ {
275
+ return fCallback('Topic not found');
276
+ }
277
+ return;
278
+ }
279
+
280
+ let tmpTopic = tmpState.Topics[pTopicCode];
281
+ tmpState.Topic = pTopicCode;
282
+
283
+ // Re-render navigation
284
+ this.pict.views['InlineDoc-Nav'].render();
285
+
286
+ // Load the topic's help file
287
+ let tmpPath = tmpTopic.TopicHelpFilePath || '';
288
+ if (tmpPath)
289
+ {
290
+ this.loadDocument(tmpPath, fCallback);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Clear the active topic, showing full navigation.
296
+ */
297
+ clearTopic()
298
+ {
299
+ let tmpState = this.pict.AppData.InlineDocumentation;
300
+ tmpState.Topic = null;
301
+
302
+ // Re-render navigation without filter
303
+ this.pict.views['InlineDoc-Nav'].render();
304
+ }
305
+
306
+ /**
307
+ * Add a new topic definition at runtime.
308
+ *
309
+ * @param {string} pTopicCode - Unique topic code
310
+ * @param {Object} pTopicDefinition - Topic object: { TopicCode, TopicHelpFilePath, TopicTitle, Routes }
311
+ */
312
+ addTopic(pTopicCode, pTopicDefinition)
313
+ {
314
+ let tmpState = this.pict.AppData.InlineDocumentation;
315
+
316
+ if (!pTopicCode)
317
+ {
318
+ this.log.warn('InlineDocumentation: addTopic requires a TopicCode.');
319
+ return;
320
+ }
321
+
322
+ if (!tmpState.Topics)
323
+ {
324
+ tmpState.Topics = {};
325
+ }
326
+
327
+ let tmpDefinition = Object.assign({ TopicCode: pTopicCode }, pTopicDefinition || {});
328
+ tmpState.Topics[pTopicCode] = tmpDefinition;
329
+ }
330
+
331
+ /**
332
+ * Add a route pattern to an existing topic.
333
+ *
334
+ * @param {string} pTopicCode - The topic to add the route to
335
+ * @param {string} pRoutePattern - Route pattern (e.g. '/settings', '/admin/*')
336
+ */
337
+ addRouteToTopic(pTopicCode, pRoutePattern)
338
+ {
339
+ let tmpState = this.pict.AppData.InlineDocumentation;
340
+
341
+ if (!pTopicCode || !tmpState.Topics || !tmpState.Topics[pTopicCode])
342
+ {
343
+ this.log.warn(`InlineDocumentation: Topic [${pTopicCode}] not found for addRouteToTopic.`);
344
+ return;
345
+ }
346
+
347
+ let tmpTopic = tmpState.Topics[pTopicCode];
348
+
349
+ if (!tmpTopic.Routes)
350
+ {
351
+ tmpTopic.Routes = [];
352
+ }
353
+
354
+ if (tmpTopic.Routes.indexOf(pRoutePattern) < 0)
355
+ {
356
+ tmpTopic.Routes.push(pRoutePattern);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Get a list of all topics in a UI-friendly array format.
362
+ *
363
+ * @returns {Array} Array of { TopicCode, TopicTitle, TopicHelpFilePath, RouteCount }
364
+ */
365
+ getTopicList()
366
+ {
367
+ let tmpState = this.pict.AppData.InlineDocumentation;
368
+ let tmpResult = [];
369
+
370
+ if (!tmpState || !tmpState.Topics)
371
+ {
372
+ return tmpResult;
373
+ }
374
+
375
+ let tmpTopicCodes = Object.keys(tmpState.Topics);
376
+
377
+ for (let i = 0; i < tmpTopicCodes.length; i++)
378
+ {
379
+ let tmpTopic = tmpState.Topics[tmpTopicCodes[i]];
380
+ tmpResult.push(
381
+ {
382
+ TopicCode: tmpTopicCodes[i],
383
+ TopicTitle: tmpTopic.TopicTitle || tmpTopic.Name || tmpTopicCodes[i],
384
+ TopicHelpFilePath: tmpTopic.TopicHelpFilePath || '',
385
+ RouteCount: (tmpTopic.Routes && Array.isArray(tmpTopic.Routes)) ? tmpTopic.Routes.length : 0
386
+ });
387
+ }
388
+
389
+ return tmpResult;
390
+ }
391
+
392
+ /**
393
+ * Update an existing topic definition.
394
+ *
395
+ * Merges only the properties present in pUpdates into the topic.
396
+ *
397
+ * @param {string} pTopicCode - The topic to update
398
+ * @param {Object} pUpdates - Properties to merge: { TopicTitle, TopicHelpFilePath, Routes }
399
+ * @returns {boolean} True if updated, false if topic not found
400
+ */
401
+ updateTopic(pTopicCode, pUpdates)
402
+ {
403
+ let tmpState = this.pict.AppData.InlineDocumentation;
404
+
405
+ if (!pTopicCode || !tmpState.Topics || !tmpState.Topics[pTopicCode])
406
+ {
407
+ return false;
408
+ }
409
+
410
+ let tmpTopic = tmpState.Topics[pTopicCode];
411
+ let tmpUpdates = pUpdates || {};
412
+
413
+ if (tmpUpdates.hasOwnProperty('TopicTitle'))
414
+ {
415
+ tmpTopic.TopicTitle = tmpUpdates.TopicTitle;
416
+ }
417
+ if (tmpUpdates.hasOwnProperty('TopicHelpFilePath'))
418
+ {
419
+ tmpTopic.TopicHelpFilePath = tmpUpdates.TopicHelpFilePath;
420
+ }
421
+ if (tmpUpdates.hasOwnProperty('Routes'))
422
+ {
423
+ tmpTopic.Routes = tmpUpdates.Routes;
424
+ }
425
+
426
+ return true;
427
+ }
428
+
429
+ /**
430
+ * Remove a topic definition.
431
+ *
432
+ * If the removed topic is the currently active topic, clears it.
433
+ *
434
+ * @param {string} pTopicCode - The topic to remove
435
+ * @returns {boolean} True if removed, false if topic not found
436
+ */
437
+ removeTopic(pTopicCode)
438
+ {
439
+ let tmpState = this.pict.AppData.InlineDocumentation;
440
+
441
+ if (!pTopicCode || !tmpState.Topics || !tmpState.Topics[pTopicCode])
442
+ {
443
+ return false;
444
+ }
445
+
446
+ delete tmpState.Topics[pTopicCode];
447
+
448
+ // Clear active topic if it was the one removed
449
+ if (tmpState.Topic === pTopicCode)
450
+ {
451
+ tmpState.Topic = null;
452
+ }
453
+
454
+ return true;
455
+ }
456
+
457
+ /**
458
+ * Remove a specific route pattern from a topic.
459
+ *
460
+ * @param {string} pTopicCode - The topic to modify
461
+ * @param {string} pRoutePattern - The route pattern to remove
462
+ * @returns {boolean} True if removed, false if not found
463
+ */
464
+ removeRouteFromTopic(pTopicCode, pRoutePattern)
465
+ {
466
+ let tmpState = this.pict.AppData.InlineDocumentation;
467
+
468
+ if (!pTopicCode || !tmpState.Topics || !tmpState.Topics[pTopicCode])
469
+ {
470
+ return false;
471
+ }
472
+
473
+ let tmpTopic = tmpState.Topics[pTopicCode];
474
+
475
+ if (!tmpTopic.Routes || !Array.isArray(tmpTopic.Routes))
476
+ {
477
+ return false;
478
+ }
479
+
480
+ let tmpIndex = tmpTopic.Routes.indexOf(pRoutePattern);
481
+
482
+ if (tmpIndex < 0)
483
+ {
484
+ return false;
485
+ }
486
+
487
+ tmpTopic.Routes.splice(tmpIndex, 1);
488
+ return true;
489
+ }
490
+
491
+ /**
492
+ * Persist the current topics via the onTopicsSave callback.
493
+ *
494
+ * If no onTopicsSave handler was provided, succeeds locally.
495
+ *
496
+ * @param {Function} [fCallback] - Callback receiving (error)
497
+ */
498
+ saveTopics(fCallback)
499
+ {
500
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
501
+ let tmpState = this.pict.AppData.InlineDocumentation;
502
+
503
+ if (typeof this._onTopicsSave === 'function')
504
+ {
505
+ this._onTopicsSave(tmpState.Topics, (pError) =>
506
+ {
507
+ if (pError)
508
+ {
509
+ this.log.warn(`InlineDocumentation: Topics save failed: ${pError}`);
510
+ return tmpCallback(pError);
511
+ }
512
+ return tmpCallback(null);
513
+ });
514
+ }
515
+ else
516
+ {
517
+ // No save handler — succeed locally
518
+ return tmpCallback(null);
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Enable or disable the topic manager UI.
524
+ *
525
+ * When enabled, management buttons appear in the navigation toolbar.
526
+ *
527
+ * @param {boolean} pEnabled - Whether topic management is available
528
+ */
529
+ setTopicManagerEnabled(pEnabled)
530
+ {
531
+ let tmpState = this.pict.AppData.InlineDocumentation;
532
+ tmpState.TopicManagerEnabled = !!pEnabled;
533
+
534
+ // Re-render navigation to show/hide management buttons
535
+ let tmpNavView = this.pict.views['InlineDoc-Nav'];
536
+ if (tmpNavView)
537
+ {
538
+ tmpNavView.render();
539
+ }
540
+ }
541
+
542
+ // -- Wildcard builder helpers --
543
+
544
+ /**
545
+ * Split a route into segments with wildcard pattern options.
546
+ *
547
+ * For a route like '/books/detail/5', returns:
548
+ * [
549
+ * { Segment: 'books', Path: '/books', WildcardPattern: '/books/*', Index: 0 },
550
+ * { Segment: 'detail', Path: '/books/detail', WildcardPattern: '/books/detail/*', Index: 1 },
551
+ * { Segment: '5', Path: '/books/detail/5', WildcardPattern: '/books/detail/5/*', Index: 2 }
552
+ * ]
553
+ *
554
+ * @param {string} pRoute - The route to split
555
+ * @returns {Array} Array of segment objects
556
+ */
557
+ getRouteSegments(pRoute)
558
+ {
559
+ if (!pRoute || typeof pRoute !== 'string')
560
+ {
561
+ return [];
562
+ }
563
+
564
+ // Strip leading slash and split
565
+ let tmpClean = pRoute.replace(/^\//, '');
566
+ if (!tmpClean)
567
+ {
568
+ return [];
569
+ }
570
+
571
+ let tmpParts = tmpClean.split('/');
572
+ let tmpSegments = [];
573
+
574
+ for (let i = 0; i < tmpParts.length; i++)
575
+ {
576
+ let tmpPath = '/' + tmpParts.slice(0, i + 1).join('/');
577
+ tmpSegments.push(
578
+ {
579
+ Segment: tmpParts[i],
580
+ Path: tmpPath,
581
+ WildcardPattern: tmpPath + '/*',
582
+ Index: i
583
+ });
584
+ }
585
+
586
+ return tmpSegments;
587
+ }
588
+
589
+ /**
590
+ * Build a wildcard pattern from a route at a given segment index.
591
+ *
592
+ * The wildcard replaces everything after the segment at pSegmentIndex.
593
+ * For '/books/detail/5' with index 1, returns '/books/detail/*'.
594
+ *
595
+ * @param {string} pRoute - The route
596
+ * @param {number} pSegmentIndex - The segment index (0-based) where the wildcard starts after
597
+ * @returns {string} The wildcard pattern, or empty string if invalid
598
+ */
599
+ buildWildcardPattern(pRoute, pSegmentIndex)
600
+ {
601
+ let tmpSegments = this.getRouteSegments(pRoute);
602
+
603
+ if (tmpSegments.length < 1 || pSegmentIndex < 0 || pSegmentIndex >= tmpSegments.length)
604
+ {
605
+ return '';
606
+ }
607
+
608
+ return tmpSegments[pSegmentIndex].WildcardPattern;
609
+ }
610
+
611
+ /**
612
+ * Get all topics whose Routes match a given route.
613
+ *
614
+ * Unlike resolveHelpForRoute (which returns only the best match),
615
+ * this returns all matching topic codes — useful when multiple
616
+ * documents are relevant to the same route.
617
+ *
618
+ * @param {string} pRoute - The application route
619
+ * @returns {Array} Array of { TopicCode, Pattern, MatchLength } objects, sorted by match length descending
620
+ */
621
+ getTopicsForRoute(pRoute)
622
+ {
623
+ let tmpState = this.pict.AppData.InlineDocumentation;
624
+ let tmpMatches = [];
625
+
626
+ if (!pRoute || !tmpState.Topics)
627
+ {
628
+ return tmpMatches;
629
+ }
630
+
631
+ let tmpTopicCodes = Object.keys(tmpState.Topics);
632
+
633
+ for (let i = 0; i < tmpTopicCodes.length; i++)
634
+ {
635
+ let tmpTopic = tmpState.Topics[tmpTopicCodes[i]];
636
+
637
+ if (!tmpTopic.Routes || !Array.isArray(tmpTopic.Routes))
638
+ {
639
+ continue;
640
+ }
641
+
642
+ for (let j = 0; j < tmpTopic.Routes.length; j++)
643
+ {
644
+ let tmpPattern = tmpTopic.Routes[j];
645
+
646
+ if (!tmpPattern)
647
+ {
648
+ continue;
649
+ }
650
+
651
+ let tmpMatchLength = 0;
652
+ let tmpIsMatch = false;
653
+
654
+ if (tmpPattern.endsWith('/*'))
655
+ {
656
+ let tmpPrefix = tmpPattern.slice(0, -2);
657
+ if (pRoute === tmpPrefix || pRoute.indexOf(tmpPrefix + '/') === 0)
658
+ {
659
+ tmpIsMatch = true;
660
+ tmpMatchLength = tmpPrefix.length;
661
+ }
662
+ }
663
+ else
664
+ {
665
+ if (pRoute === tmpPattern)
666
+ {
667
+ tmpIsMatch = true;
668
+ tmpMatchLength = tmpPattern.length;
669
+ }
670
+ }
671
+
672
+ if (tmpIsMatch)
673
+ {
674
+ tmpMatches.push({ TopicCode: tmpTopicCodes[i], Pattern: tmpPattern, MatchLength: tmpMatchLength });
675
+ }
676
+ }
677
+ }
678
+
679
+ // Sort by match length descending (best match first)
680
+ tmpMatches.sort((a, b) => b.MatchLength - a.MatchLength);
681
+
682
+ return tmpMatches;
683
+ }
684
+
685
+ /**
686
+ * Change the documentation base URL.
687
+ *
688
+ * @param {string} pURL - The new base URL
689
+ */
690
+ setDocsBaseURL(pURL)
691
+ {
692
+ if (!this.pict.AppData.InlineDocumentation)
693
+ {
694
+ this.pict.AppData.InlineDocumentation = {};
695
+ }
696
+ this.pict.AppData.InlineDocumentation.DocsBaseURL = pURL || '';
697
+ // Clear cache when base URL changes
698
+ this._ContentCache = {};
699
+ }
700
+
701
+ /**
702
+ * Get the navigation history.
703
+ *
704
+ * @returns {Array} Array of visited document paths
705
+ */
706
+ getNavigationHistory()
707
+ {
708
+ let tmpState = this.pict.AppData.InlineDocumentation;
709
+ return (tmpState && tmpState.NavigationHistory) ? tmpState.NavigationHistory : [];
710
+ }
711
+
712
+ /**
713
+ * Navigate back to the previous document.
714
+ *
715
+ * @param {Function} [fCallback] - Callback when navigation is complete
716
+ */
717
+ navigateBack(fCallback)
718
+ {
719
+ let tmpHistory = this.getNavigationHistory();
720
+
721
+ if (tmpHistory.length < 2)
722
+ {
723
+ return;
724
+ }
725
+
726
+ // Remove current page
727
+ tmpHistory.pop();
728
+ // Get previous page
729
+ let tmpPreviousPath = tmpHistory.pop();
730
+
731
+ this.loadDocument(tmpPreviousPath, fCallback);
732
+ }
733
+
734
+ // -- Edit mode --
735
+
736
+ /**
737
+ * Enable or disable edit permissions.
738
+ *
739
+ * When enabled, a pencil icon appears in the content area allowing
740
+ * the user to toggle into edit mode.
741
+ *
742
+ * @param {boolean} pEnabled - Whether edit mode is available
743
+ */
744
+ setEditEnabled(pEnabled)
745
+ {
746
+ let tmpState = this.pict.AppData.InlineDocumentation;
747
+ tmpState.EditEnabled = !!pEnabled;
748
+
749
+ // If TopicManagerEnabled was not explicitly configured, mirror EditEnabled
750
+ if (!this._topicManagerExplicitlySet)
751
+ {
752
+ tmpState.TopicManagerEnabled = !!pEnabled;
753
+
754
+ // Re-render navigation to show/hide management buttons
755
+ let tmpNavView = this.pict.views['InlineDoc-Nav'];
756
+ if (tmpNavView)
757
+ {
758
+ tmpNavView.render();
759
+ }
760
+ }
761
+
762
+ // Re-render content view to show/hide edit toolbar
763
+ let tmpContentView = this.pict.views['InlineDoc-Content'];
764
+ if (tmpContentView)
765
+ {
766
+ tmpContentView.renderEditToolbar();
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Toggle between view and edit mode.
772
+ */
773
+ toggleEdit()
774
+ {
775
+ let tmpState = this.pict.AppData.InlineDocumentation;
776
+
777
+ if (tmpState.Editing)
778
+ {
779
+ this.cancelEdit();
780
+ }
781
+ else
782
+ {
783
+ this.beginEdit();
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Enter edit mode for the current document.
789
+ *
790
+ * Retrieves the raw markdown from cache and displays it in the markdown editor.
791
+ */
792
+ beginEdit()
793
+ {
794
+ let tmpState = this.pict.AppData.InlineDocumentation;
795
+
796
+ if (!tmpState.EditEnabled || !tmpState.CurrentPath)
797
+ {
798
+ return;
799
+ }
800
+
801
+ // Get the raw markdown from cache
802
+ let tmpURL = (tmpState.DocsBaseURL || '') + tmpState.CurrentPath;
803
+ let tmpCacheEntry = this._ContentCache[tmpURL];
804
+ let tmpMarkdown = (tmpCacheEntry && tmpCacheEntry.markdown) ? tmpCacheEntry.markdown : '';
805
+
806
+ tmpState.Editing = true;
807
+ tmpState.EditingPath = tmpState.CurrentPath;
808
+ tmpState.EditingContent = tmpMarkdown;
809
+
810
+ // Show the editor
811
+ let tmpContentView = this.pict.views['InlineDoc-Content'];
812
+ if (tmpContentView)
813
+ {
814
+ tmpContentView.showEditor(tmpMarkdown);
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Cancel editing and restore the rendered view.
820
+ */
821
+ cancelEdit()
822
+ {
823
+ let tmpState = this.pict.AppData.InlineDocumentation;
824
+
825
+ tmpState.Editing = false;
826
+ tmpState.EditingPath = '';
827
+ tmpState.EditingContent = '';
828
+ tmpState.EditorSegments = [];
829
+
830
+ // Restore the rendered content
831
+ let tmpContentView = this.pict.views['InlineDoc-Content'];
832
+ if (tmpContentView)
833
+ {
834
+ tmpContentView.hideEditor();
835
+
836
+ // Re-display the cached HTML
837
+ let tmpURL = (tmpState.DocsBaseURL || '') + tmpState.CurrentPath;
838
+ let tmpCacheEntry = this._ContentCache[tmpURL];
839
+ if (tmpCacheEntry && tmpCacheEntry.html)
840
+ {
841
+ tmpContentView.displayContent(tmpCacheEntry.html);
842
+ }
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Save the current edits.
848
+ *
849
+ * Reads the markdown editor content, calls the onSave callback provided by the
850
+ * host app, re-parses the markdown, and returns to view mode.
851
+ *
852
+ * @param {Function} [fCallback] - Callback receiving (error)
853
+ */
854
+ saveEdit(fCallback)
855
+ {
856
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
857
+ let tmpState = this.pict.AppData.InlineDocumentation;
858
+ let tmpContentView = this.pict.views['InlineDoc-Content'];
859
+
860
+ if (!tmpState.Editing)
861
+ {
862
+ return tmpCallback('Not in edit mode');
863
+ }
864
+
865
+ // Read content from the markdown editor
866
+ // First marshal editor state to data, then read from the data address
867
+ let tmpMarkdown = '';
868
+ let tmpEditorView = this.pict.views['InlineDoc-MarkdownEditor'];
869
+ if (tmpEditorView && typeof tmpEditorView.marshalFromView === 'function')
870
+ {
871
+ tmpEditorView.marshalFromView();
872
+ }
873
+ if (tmpState.EditorSegments && tmpState.EditorSegments.length > 0)
874
+ {
875
+ tmpMarkdown = tmpState.EditorSegments[0].Content || '';
876
+ }
877
+
878
+ let tmpPath = tmpState.EditingPath;
879
+ let tmpURL = (tmpState.DocsBaseURL || '') + tmpPath;
880
+
881
+ let tmpSaveData = { Path: tmpPath, Content: tmpMarkdown };
882
+
883
+ let tmpFinishSave = () =>
884
+ {
885
+ // Re-parse the markdown and update cache
886
+ let tmpHTML = this._ContentProvider.parseMarkdown(
887
+ tmpMarkdown,
888
+ this._createLinkResolver(),
889
+ this._createImageResolver(tmpURL));
890
+ this._ContentCache[tmpURL] = { html: tmpHTML, markdown: tmpMarkdown };
891
+
892
+ // Exit edit mode
893
+ tmpState.Editing = false;
894
+ tmpState.EditingPath = '';
895
+ tmpState.EditingContent = '';
896
+ tmpState.EditorSegments = [];
897
+
898
+ // Display the updated content
899
+ if (tmpContentView)
900
+ {
901
+ tmpContentView.hideEditor();
902
+ tmpContentView.displayContent(tmpHTML);
903
+ }
904
+
905
+ return tmpCallback(null);
906
+ };
907
+
908
+ // Call the onSave callback if provided
909
+ if (typeof this._onSave === 'function')
910
+ {
911
+ this._onSave(tmpSaveData, (pError) =>
912
+ {
913
+ if (pError)
914
+ {
915
+ this.log.warn(`InlineDocumentation: Save failed: ${pError}`);
916
+ return tmpCallback(pError);
917
+ }
918
+ tmpFinishSave();
919
+ });
920
+ }
921
+ else
922
+ {
923
+ // No save handler — just update locally
924
+ tmpFinishSave();
925
+ }
926
+ }
927
+
928
+ // -- Route-based help --
929
+
930
+ /**
931
+ * Find the best-matching topic for a given route.
932
+ *
933
+ * Iterates all topics looking for ones with a Routes array. Supports
934
+ * exact match and wildcard suffix (e.g. "/books/store/*" matches
935
+ * "/books/store/123"). Returns the TopicCode with the longest matching
936
+ * route pattern, or null if no match.
937
+ *
938
+ * @param {string} pRoute - The application route (e.g. '/books/store/5')
939
+ * @returns {string|null} The TopicCode of the best match, or null
940
+ */
941
+ resolveHelpForRoute(pRoute)
942
+ {
943
+ let tmpState = this.pict.AppData.InlineDocumentation;
944
+
945
+ if (!pRoute || !tmpState.Topics)
946
+ {
947
+ return null;
948
+ }
949
+
950
+ let tmpBestMatch = null;
951
+ let tmpBestLength = -1;
952
+
953
+ let tmpTopicCodes = Object.keys(tmpState.Topics);
954
+
955
+ for (let i = 0; i < tmpTopicCodes.length; i++)
956
+ {
957
+ let tmpTopic = tmpState.Topics[tmpTopicCodes[i]];
958
+
959
+ if (!tmpTopic.Routes || !Array.isArray(tmpTopic.Routes))
960
+ {
961
+ continue;
962
+ }
963
+
964
+ for (let j = 0; j < tmpTopic.Routes.length; j++)
965
+ {
966
+ let tmpPattern = tmpTopic.Routes[j];
967
+
968
+ if (!tmpPattern)
969
+ {
970
+ continue;
971
+ }
972
+
973
+ let tmpMatches = false;
974
+ let tmpMatchLength = 0;
975
+
976
+ if (tmpPattern.endsWith('/*'))
977
+ {
978
+ // Wildcard suffix — match if route starts with prefix
979
+ let tmpPrefix = tmpPattern.slice(0, -2);
980
+ if (pRoute === tmpPrefix || pRoute.indexOf(tmpPrefix + '/') === 0)
981
+ {
982
+ tmpMatches = true;
983
+ tmpMatchLength = tmpPrefix.length;
984
+ }
985
+ }
986
+ else
987
+ {
988
+ // Exact match
989
+ if (pRoute === tmpPattern)
990
+ {
991
+ tmpMatches = true;
992
+ tmpMatchLength = tmpPattern.length;
993
+ }
994
+ }
995
+
996
+ if (tmpMatches && tmpMatchLength > tmpBestLength)
997
+ {
998
+ tmpBestMatch = tmpTopicCodes[i];
999
+ tmpBestLength = tmpMatchLength;
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ return tmpBestMatch;
1005
+ }
1006
+
1007
+ /**
1008
+ * Navigate help to the topic matching a given route.
1009
+ *
1010
+ * Convenience method: resolves the route to a topic, then loads it.
1011
+ * If no topic matches, does nothing.
1012
+ *
1013
+ * @param {string} pRoute - The application route
1014
+ * @param {Function} [fCallback] - Callback receiving (error, htmlContent)
1015
+ * @returns {boolean} True if a matching topic was found
1016
+ */
1017
+ navigateToRoute(pRoute, fCallback)
1018
+ {
1019
+ let tmpState = this.pict.AppData.InlineDocumentation;
1020
+ tmpState.CurrentRoute = pRoute || '';
1021
+
1022
+ let tmpTopicCode = this.resolveHelpForRoute(pRoute);
1023
+
1024
+ if (tmpTopicCode)
1025
+ {
1026
+ this.loadTopicDocument(tmpTopicCode, fCallback);
1027
+ return true;
1028
+ }
1029
+
1030
+ return false;
1031
+ }
1032
+
1033
+ // -- Tooltip placeholders --
1034
+
1035
+ /**
1036
+ * Enable or disable tooltip edit mode.
1037
+ *
1038
+ * When enabled, all tooltip placeholders are visible and clickable
1039
+ * for content editors to author tooltip content. When disabled,
1040
+ * only placeholders with content show tooltips on hover.
1041
+ *
1042
+ * Automatically re-scans tooltips after toggling.
1043
+ *
1044
+ * @param {boolean} pEnabled - Whether tooltip edit mode is active
1045
+ */
1046
+ setTooltipEditMode(pEnabled)
1047
+ {
1048
+ let tmpState = this.pict.AppData.InlineDocumentation;
1049
+
1050
+ if (!tmpState)
1051
+ {
1052
+ return;
1053
+ }
1054
+
1055
+ tmpState.TooltipEditMode = !!pEnabled;
1056
+ this.scanTooltips();
1057
+ }
1058
+
1059
+ /**
1060
+ * Get the tooltip content for a key from the active topic.
1061
+ *
1062
+ * Looks up the currently active topic's Tooltips hash for the
1063
+ * given key and returns the Content string, or null if not found.
1064
+ *
1065
+ * @param {string} pTooltipKey - The tooltip key (from data-d-tooltip attribute)
1066
+ * @returns {string|null} The tooltip content, or null
1067
+ */
1068
+ getTooltipContent(pTooltipKey)
1069
+ {
1070
+ let tmpState = this.pict.AppData.InlineDocumentation;
1071
+
1072
+ if (!pTooltipKey || !tmpState || !tmpState.Topic || !tmpState.Topics)
1073
+ {
1074
+ return null;
1075
+ }
1076
+
1077
+ let tmpTopic = tmpState.Topics[tmpState.Topic];
1078
+
1079
+ if (!tmpTopic || !tmpTopic.Tooltips || !tmpTopic.Tooltips[pTooltipKey])
1080
+ {
1081
+ return null;
1082
+ }
1083
+
1084
+ return tmpTopic.Tooltips[pTooltipKey].Content || null;
1085
+ }
1086
+
1087
+ /**
1088
+ * Set tooltip content for a key on the active topic.
1089
+ *
1090
+ * Lazily creates the Tooltips hash on the topic if needed.
1091
+ * Passing null or empty string removes the tooltip entry.
1092
+ *
1093
+ * @param {string} pTooltipKey - The tooltip key
1094
+ * @param {string|null} pContent - The markdown content, or null to remove
1095
+ * @returns {boolean} True if set, false if no active topic
1096
+ */
1097
+ setTooltipContent(pTooltipKey, pContent)
1098
+ {
1099
+ let tmpState = this.pict.AppData.InlineDocumentation;
1100
+
1101
+ if (!pTooltipKey || !tmpState || !tmpState.Topic || !tmpState.Topics)
1102
+ {
1103
+ return false;
1104
+ }
1105
+
1106
+ let tmpTopic = tmpState.Topics[tmpState.Topic];
1107
+
1108
+ if (!tmpTopic)
1109
+ {
1110
+ return false;
1111
+ }
1112
+
1113
+ if (!pContent)
1114
+ {
1115
+ // Remove the entry
1116
+ if (tmpTopic.Tooltips && tmpTopic.Tooltips[pTooltipKey])
1117
+ {
1118
+ delete tmpTopic.Tooltips[pTooltipKey];
1119
+ }
1120
+ return true;
1121
+ }
1122
+
1123
+ // Lazily create Tooltips hash
1124
+ if (!tmpTopic.Tooltips)
1125
+ {
1126
+ tmpTopic.Tooltips = {};
1127
+ }
1128
+
1129
+ tmpTopic.Tooltips[pTooltipKey] = { Content: pContent };
1130
+ return true;
1131
+ }
1132
+
1133
+ /**
1134
+ * Remove all active tooltip bindings from the DOM.
1135
+ *
1136
+ * Destroys tooltip handles, removes click listeners, removes
1137
+ * injected icons, and restores original element state.
1138
+ */
1139
+ clearTooltipBindings()
1140
+ {
1141
+ for (let i = 0; i < this._ActiveTooltipBindings.length; i++)
1142
+ {
1143
+ let tmpBinding = this._ActiveTooltipBindings[i];
1144
+
1145
+ // Destroy the modal tooltip handle
1146
+ if (tmpBinding.TooltipHandle && typeof tmpBinding.TooltipHandle.destroy === 'function')
1147
+ {
1148
+ tmpBinding.TooltipHandle.destroy();
1149
+ }
1150
+
1151
+ // Remove click handler
1152
+ if (tmpBinding.ClickHandler && tmpBinding.Element)
1153
+ {
1154
+ tmpBinding.Element.removeEventListener('click', tmpBinding.ClickHandler);
1155
+ }
1156
+
1157
+ // Remove injected icon
1158
+ if (tmpBinding.InjectedIcon && tmpBinding.InjectedIcon.parentNode)
1159
+ {
1160
+ tmpBinding.InjectedIcon.parentNode.removeChild(tmpBinding.InjectedIcon);
1161
+ }
1162
+
1163
+ // Remove edit-mode CSS classes
1164
+ if (tmpBinding.Element)
1165
+ {
1166
+ tmpBinding.Element.classList.remove(
1167
+ 'pict-inline-doc-tooltip-edit-target',
1168
+ 'pict-inline-doc-tooltip-empty');
1169
+ }
1170
+
1171
+ // Restore original display for hidden icon spans
1172
+ if (tmpBinding.OriginalDisplay !== undefined && tmpBinding.Element)
1173
+ {
1174
+ tmpBinding.Element.style.display = tmpBinding.OriginalDisplay;
1175
+ }
1176
+ }
1177
+
1178
+ this._ActiveTooltipBindings = [];
1179
+
1180
+ // Remove document-level help link handler
1181
+ if (this._tooltipHelpLinkHandler && typeof document !== 'undefined')
1182
+ {
1183
+ document.removeEventListener('click', this._tooltipHelpLinkHandler);
1184
+ this._tooltipHelpLinkHandler = null;
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Scan the document for tooltip placeholder elements and wire them up.
1190
+ *
1191
+ * Finds all elements with data-d-tooltip attributes and:
1192
+ * - In normal mode: attaches hover tooltips for those with content
1193
+ * - In edit mode: adds visual indicators and click-to-edit handlers
1194
+ *
1195
+ * Call this after your application views render.
1196
+ */
1197
+ scanTooltips()
1198
+ {
1199
+ this.clearTooltipBindings();
1200
+
1201
+ if (typeof document === 'undefined')
1202
+ {
1203
+ return;
1204
+ }
1205
+
1206
+ let tmpState = this.pict.AppData.InlineDocumentation;
1207
+
1208
+ if (!tmpState)
1209
+ {
1210
+ return;
1211
+ }
1212
+
1213
+ let tmpModal = this.pict.views['Pict-Section-Modal'];
1214
+ let tmpElements = document.querySelectorAll('[data-d-tooltip]');
1215
+ let tmpEditMode = tmpState.TooltipEditMode || false;
1216
+
1217
+ for (let i = 0; i < tmpElements.length; i++)
1218
+ {
1219
+ let tmpElement = tmpElements[i];
1220
+ let tmpKey = tmpElement.getAttribute('data-d-tooltip');
1221
+
1222
+ if (!tmpKey)
1223
+ {
1224
+ continue;
1225
+ }
1226
+
1227
+ let tmpContent = this.getTooltipContent(tmpKey);
1228
+ let tmpIsIcon = tmpElement.hasAttribute('data-d-tooltip-icon');
1229
+
1230
+ let tmpBinding =
1231
+ {
1232
+ Element: tmpElement,
1233
+ Key: tmpKey,
1234
+ Type: tmpIsIcon ? 'icon' : 'attribute',
1235
+ TooltipHandle: null,
1236
+ ClickHandler: null,
1237
+ InjectedIcon: null,
1238
+ OriginalDisplay: undefined
1239
+ };
1240
+
1241
+ if (tmpEditMode)
1242
+ {
1243
+ this._wireTooltipEditMode(tmpElement, tmpKey, tmpContent, tmpIsIcon, tmpBinding, tmpModal);
1244
+ }
1245
+ else
1246
+ {
1247
+ this._wireTooltipNormalMode(tmpElement, tmpKey, tmpContent, tmpIsIcon, tmpBinding, tmpModal);
1248
+ }
1249
+
1250
+ this._ActiveTooltipBindings.push(tmpBinding);
1251
+ }
1252
+
1253
+ // Install document-level click delegation for help: links in tooltips
1254
+ this._installTooltipHelpLinkHandler();
1255
+ }
1256
+
1257
+ /**
1258
+ * Install a document-level click handler for help links inside tooltips.
1259
+ *
1260
+ * Tooltip elements are created/destroyed dynamically by the modal system,
1261
+ * so we use event delegation on the document to catch clicks on
1262
+ * [rel^="pict-inline-doc-help:"] links wherever they appear.
1263
+ */
1264
+ _installTooltipHelpLinkHandler()
1265
+ {
1266
+ if (typeof document === 'undefined')
1267
+ {
1268
+ return;
1269
+ }
1270
+
1271
+ // Remove existing handler if any
1272
+ if (this._tooltipHelpLinkHandler)
1273
+ {
1274
+ document.removeEventListener('click', this._tooltipHelpLinkHandler);
1275
+ }
1276
+
1277
+ let tmpSelf = this;
1278
+
1279
+ this._tooltipHelpLinkHandler = (pEvent) =>
1280
+ {
1281
+ let tmpTarget = pEvent.target;
1282
+
1283
+ // Walk up to find the link element
1284
+ while (tmpTarget && tmpTarget !== document)
1285
+ {
1286
+ if (tmpTarget.tagName === 'A' && tmpTarget.getAttribute('rel') && tmpTarget.getAttribute('rel').indexOf('pict-inline-doc-help:') === 0)
1287
+ {
1288
+ pEvent.preventDefault();
1289
+ pEvent.stopPropagation();
1290
+
1291
+ let tmpPath = tmpTarget.getAttribute('rel').replace('pict-inline-doc-help:', '');
1292
+ tmpSelf.loadDocument(tmpPath);
1293
+ return;
1294
+ }
1295
+ tmpTarget = tmpTarget.parentNode;
1296
+ }
1297
+ };
1298
+
1299
+ document.addEventListener('click', this._tooltipHelpLinkHandler);
1300
+ }
1301
+
1302
+ /**
1303
+ * Wire a tooltip element for normal (non-edit) mode.
1304
+ *
1305
+ * @param {HTMLElement} pElement - The placeholder element
1306
+ * @param {string} pKey - The tooltip key
1307
+ * @param {string|null} pContent - The tooltip content (or null)
1308
+ * @param {boolean} pIsIcon - Whether this is an icon-type placeholder
1309
+ * @param {Object} pBinding - The binding tracking object
1310
+ * @param {Object} pModal - The modal view instance
1311
+ */
1312
+ _wireTooltipNormalMode(pElement, pKey, pContent, pIsIcon, pBinding, pModal)
1313
+ {
1314
+ if (pIsIcon)
1315
+ {
1316
+ if (pContent)
1317
+ {
1318
+ // Inject an icon and attach tooltip
1319
+ let tmpIcon = this._createTooltipIcon(pElement);
1320
+ pBinding.InjectedIcon = tmpIcon;
1321
+
1322
+ if (pModal && pModal.richTooltip)
1323
+ {
1324
+ let tmpHTML = this._ContentProvider.parseMarkdown(pContent, this._createTooltipLinkResolver());
1325
+ pBinding.TooltipHandle = pModal.richTooltip(pElement, tmpHTML, { interactive: true, maxWidth: '350px' });
1326
+ }
1327
+ }
1328
+ else
1329
+ {
1330
+ // No content — hide the span
1331
+ pBinding.OriginalDisplay = pElement.style.display;
1332
+ pElement.style.display = 'none';
1333
+ }
1334
+ }
1335
+ else
1336
+ {
1337
+ // Attribute tooltip
1338
+ if (pContent && pModal && pModal.richTooltip)
1339
+ {
1340
+ let tmpHTML = this._ContentProvider.parseMarkdown(pContent, this._createTooltipLinkResolver());
1341
+ pBinding.TooltipHandle = pModal.richTooltip(pElement, tmpHTML, { interactive: true, maxWidth: '350px' });
1342
+ }
1343
+ // No content = do nothing, element stays as-is
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Wire a tooltip element for edit mode.
1349
+ *
1350
+ * @param {HTMLElement} pElement - The placeholder element
1351
+ * @param {string} pKey - The tooltip key
1352
+ * @param {string|null} pContent - The tooltip content (or null)
1353
+ * @param {boolean} pIsIcon - Whether this is an icon-type placeholder
1354
+ * @param {Object} pBinding - The binding tracking object
1355
+ * @param {Object} pModal - The modal view instance
1356
+ */
1357
+ _wireTooltipEditMode(pElement, pKey, pContent, pIsIcon, pBinding, pModal)
1358
+ {
1359
+ // Add edit-mode indicator class
1360
+ pElement.classList.add('pict-inline-doc-tooltip-edit-target');
1361
+
1362
+ if (pIsIcon)
1363
+ {
1364
+ // Always show icon in edit mode
1365
+ let tmpIcon = this._createTooltipIcon(pElement);
1366
+ pBinding.InjectedIcon = tmpIcon;
1367
+
1368
+ if (!pContent)
1369
+ {
1370
+ pElement.classList.add('pict-inline-doc-tooltip-empty');
1371
+ }
1372
+ }
1373
+
1374
+ // Click handler to open editor
1375
+ let tmpSelf = this;
1376
+ let tmpClickHandler = (pEvent) =>
1377
+ {
1378
+ pEvent.preventDefault();
1379
+ pEvent.stopPropagation();
1380
+ tmpSelf._showTooltipEditor(pKey);
1381
+ };
1382
+
1383
+ pElement.addEventListener('click', tmpClickHandler);
1384
+ pBinding.ClickHandler = tmpClickHandler;
1385
+ }
1386
+
1387
+ /**
1388
+ * Create and inject a tooltip icon element into a span.
1389
+ *
1390
+ * @param {HTMLElement} pElement - The span element to inject into
1391
+ * @returns {HTMLElement} The created icon element
1392
+ */
1393
+ _createTooltipIcon(pElement)
1394
+ {
1395
+ let tmpIcon = document.createElement('span');
1396
+ tmpIcon.className = 'pict-inline-doc-tooltip-icon';
1397
+
1398
+ // Check for custom icon class
1399
+ let tmpCustomClass = pElement.getAttribute('data-d-tooltip-icon');
1400
+ if (tmpCustomClass && tmpCustomClass !== '')
1401
+ {
1402
+ tmpIcon.className += ' ' + tmpCustomClass;
1403
+ }
1404
+ else
1405
+ {
1406
+ tmpIcon.innerHTML = '&#x2753;';
1407
+ }
1408
+
1409
+ pElement.appendChild(tmpIcon);
1410
+ return tmpIcon;
1411
+ }
1412
+
1413
+ /**
1414
+ * Show the tooltip content editor modal.
1415
+ *
1416
+ * @param {string} pTooltipKey - The tooltip key to edit
1417
+ */
1418
+ _showTooltipEditor(pTooltipKey)
1419
+ {
1420
+ let tmpModal = this.pict.views['Pict-Section-Modal'];
1421
+
1422
+ if (!tmpModal)
1423
+ {
1424
+ return;
1425
+ }
1426
+
1427
+ let tmpCurrentContent = this.getTooltipContent(pTooltipKey) || '';
1428
+ let tmpSelf = this;
1429
+
1430
+ let tmpEditorHTML = '<div class="pict-inline-doc-tm-form-group">';
1431
+ tmpEditorHTML += '<label class="pict-inline-doc-tm-form-label">Tooltip Key</label>';
1432
+ tmpEditorHTML += '<div style="font-family:monospace;font-size:0.85em;color:#8A7F72;padding:0.3em 0;">' + this._escapeTooltipHTML(pTooltipKey) + '</div>';
1433
+ tmpEditorHTML += '</div>';
1434
+ tmpEditorHTML += '<div class="pict-inline-doc-tm-form-group">';
1435
+ tmpEditorHTML += '<label class="pict-inline-doc-tm-form-label">Content (Markdown)</label>';
1436
+ tmpEditorHTML += '<textarea class="pict-inline-doc-tooltip-editor-textarea" id="InlineDoc-Tooltip-Editor-Textarea">' + this._escapeTooltipHTML(tmpCurrentContent) + '</textarea>';
1437
+ tmpEditorHTML += '</div>';
1438
+ tmpEditorHTML += '<div class="pict-inline-doc-tooltip-preview-label">Preview</div>';
1439
+ tmpEditorHTML += '<div class="pict-inline-doc-tooltip-preview" id="InlineDoc-Tooltip-Editor-Preview"></div>';
1440
+
1441
+ tmpModal.show(
1442
+ {
1443
+ title: 'Edit Tooltip',
1444
+ content: tmpEditorHTML,
1445
+ closeable: true,
1446
+ width: '480px',
1447
+ buttons:
1448
+ [
1449
+ { Hash: 'cancel', Label: 'Cancel' },
1450
+ { Hash: 'save', Label: 'Save', Style: 'primary' }
1451
+ ],
1452
+ onOpen: (pDialog) =>
1453
+ {
1454
+ let tmpTextarea = document.getElementById('InlineDoc-Tooltip-Editor-Textarea');
1455
+ let tmpPreview = document.getElementById('InlineDoc-Tooltip-Editor-Preview');
1456
+
1457
+ if (tmpTextarea && tmpPreview)
1458
+ {
1459
+ // Initial preview
1460
+ let tmpLinkResolver = tmpSelf._createTooltipLinkResolver();
1461
+ let tmpInitialHTML = tmpCurrentContent ? tmpSelf._ContentProvider.parseMarkdown(tmpCurrentContent, tmpLinkResolver) : '<span style="color:#8A7F72;">No content yet.</span>';
1462
+ tmpPreview.innerHTML = tmpInitialHTML;
1463
+
1464
+ // Live preview on input
1465
+ tmpTextarea.addEventListener('input', () =>
1466
+ {
1467
+ let tmpValue = tmpTextarea.value.trim();
1468
+ if (tmpValue)
1469
+ {
1470
+ tmpPreview.innerHTML = tmpSelf._ContentProvider.parseMarkdown(tmpValue, tmpLinkResolver);
1471
+ }
1472
+ else
1473
+ {
1474
+ tmpPreview.innerHTML = '<span style="color:#8A7F72;">No content yet.</span>';
1475
+ }
1476
+ });
1477
+
1478
+ tmpTextarea.focus();
1479
+ }
1480
+ }
1481
+ }).then((pResult) =>
1482
+ {
1483
+ if (pResult === 'save')
1484
+ {
1485
+ let tmpTextarea = document.getElementById('InlineDoc-Tooltip-Editor-Textarea');
1486
+ let tmpNewContent = tmpTextarea ? tmpTextarea.value.trim() : '';
1487
+
1488
+ tmpSelf.setTooltipContent(pTooltipKey, tmpNewContent || null);
1489
+ tmpSelf.saveTopics();
1490
+ tmpSelf.scanTooltips();
1491
+
1492
+ if (tmpModal.toast)
1493
+ {
1494
+ tmpModal.toast('Tooltip saved.', { type: 'success' });
1495
+ }
1496
+ }
1497
+ });
1498
+ }
1499
+
1500
+ /**
1501
+ * Escape HTML for safe insertion into tooltip editor.
1502
+ *
1503
+ * @param {string} pText - Text to escape
1504
+ * @returns {string} Escaped text
1505
+ */
1506
+ _escapeTooltipHTML(pText)
1507
+ {
1508
+ if (!pText)
1509
+ {
1510
+ return '';
1511
+ }
1512
+ return pText
1513
+ .replace(/&/g, '&amp;')
1514
+ .replace(/</g, '&lt;')
1515
+ .replace(/>/g, '&gt;')
1516
+ .replace(/"/g, '&quot;');
1517
+ }
1518
+
1519
+ // -- Image upload --
1520
+
1521
+ /**
1522
+ * Wire the onImageUpload handler onto the markdown editor view.
1523
+ *
1524
+ * Overrides the editor's onImageUpload method so that when a user
1525
+ * drops or pastes an image, it is routed through the host app's
1526
+ * onImageUpload callback with the current document path for context.
1527
+ *
1528
+ * The host callback signature is:
1529
+ * onImageUpload(pFile, pDocumentPath, fCallback)
1530
+ * where fCallback is fCallback(pError, pRelativeURL).
1531
+ */
1532
+ _wireEditorImageUpload()
1533
+ {
1534
+ let tmpSelf = this;
1535
+ let tmpEditorView = this.pict.views['InlineDoc-MarkdownEditor'];
1536
+
1537
+ if (!tmpEditorView)
1538
+ {
1539
+ return;
1540
+ }
1541
+
1542
+ tmpEditorView.onImageUpload = (pFile, pSegmentIndex, fCallback) =>
1543
+ {
1544
+ if (typeof tmpSelf._onImageUpload !== 'function')
1545
+ {
1546
+ return false;
1547
+ }
1548
+
1549
+ let tmpState = tmpSelf.pict.AppData.InlineDocumentation;
1550
+ let tmpDocumentPath = (tmpState && tmpState.EditingPath) ? tmpState.EditingPath : '';
1551
+
1552
+ tmpSelf._onImageUpload(pFile, tmpDocumentPath, (pError, pURL) =>
1553
+ {
1554
+ if (pError)
1555
+ {
1556
+ tmpSelf.log.warn(`InlineDocumentation: Image upload failed: ${pError}`);
1557
+ }
1558
+ fCallback(pError, pURL);
1559
+ });
1560
+
1561
+ return true;
1562
+ };
1563
+ }
1564
+
1565
+ /**
1566
+ * Scroll the content area to a heading that matches an anchor string.
1567
+ *
1568
+ * Looks for headings (h1-h6) in the content body whose text, when
1569
+ * slugified, matches the anchor. Uses the standard GitHub-style
1570
+ * slugification: lowercase, spaces to hyphens, strip non-alphanumeric.
1571
+ *
1572
+ * @param {string} pAnchor - The anchor string (without #)
1573
+ */
1574
+ _scrollToAnchor(pAnchor)
1575
+ {
1576
+ if (typeof document === 'undefined' || !pAnchor)
1577
+ {
1578
+ return;
1579
+ }
1580
+
1581
+ // Delay slightly to ensure content is rendered
1582
+ setTimeout(() =>
1583
+ {
1584
+ let tmpContentBody = document.getElementById('InlineDoc-Content-Body');
1585
+ if (!tmpContentBody)
1586
+ {
1587
+ return;
1588
+ }
1589
+
1590
+ let tmpSlug = pAnchor.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
1591
+
1592
+ // Check for an element with a matching id first
1593
+ let tmpTarget = tmpContentBody.querySelector('#' + CSS.escape(tmpSlug));
1594
+
1595
+ // If no id match, search heading text
1596
+ if (!tmpTarget)
1597
+ {
1598
+ let tmpHeadings = tmpContentBody.querySelectorAll('h1, h2, h3, h4, h5, h6');
1599
+ for (let i = 0; i < tmpHeadings.length; i++)
1600
+ {
1601
+ let tmpHeadingText = tmpHeadings[i].textContent || '';
1602
+ let tmpHeadingSlug = tmpHeadingText.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
1603
+ if (tmpHeadingSlug === tmpSlug)
1604
+ {
1605
+ tmpTarget = tmpHeadings[i];
1606
+ break;
1607
+ }
1608
+ }
1609
+ }
1610
+
1611
+ if (tmpTarget)
1612
+ {
1613
+ tmpTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
1614
+ }
1615
+ }, 50);
1616
+ }
1617
+
1618
+ // -- Internal methods --
1619
+
1620
+ /**
1621
+ * Fetch a markdown document and convert it to HTML.
1622
+ *
1623
+ * @param {string} pURL - The URL to fetch
1624
+ * @param {Function} fCallback - Callback receiving (error, htmlContent)
1625
+ */
1626
+ _fetchDocument(pURL, fCallback)
1627
+ {
1628
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
1629
+
1630
+ if (!pURL)
1631
+ {
1632
+ return tmpCallback('No URL provided', '');
1633
+ }
1634
+
1635
+ // Check cache
1636
+ if (this._ContentCache[pURL])
1637
+ {
1638
+ return tmpCallback(null, this._ContentCache[pURL].html);
1639
+ }
1640
+
1641
+ fetch(pURL)
1642
+ .then((pResponse) =>
1643
+ {
1644
+ if (!pResponse.ok)
1645
+ {
1646
+ return null;
1647
+ }
1648
+ return pResponse.text();
1649
+ })
1650
+ .then((pMarkdown) =>
1651
+ {
1652
+ if (!pMarkdown)
1653
+ {
1654
+ return tmpCallback('Document not found', '');
1655
+ }
1656
+
1657
+ let tmpHTML = this._ContentProvider.parseMarkdown(
1658
+ pMarkdown,
1659
+ this._createLinkResolver(),
1660
+ this._createImageResolver(pURL));
1661
+ this._ContentCache[pURL] = { html: tmpHTML, markdown: pMarkdown };
1662
+ return tmpCallback(null, tmpHTML);
1663
+ })
1664
+ .catch((pError) =>
1665
+ {
1666
+ this.log.warn(`InlineDocumentation: Error fetching [${pURL}]: ${pError}`);
1667
+ return tmpCallback(pError, '');
1668
+ });
1669
+ }
1670
+
1671
+ /**
1672
+ * Create a link resolver that converts internal doc links to provider API calls.
1673
+ *
1674
+ * Internal links (relative .md paths) get converted to javascript:void(0) with
1675
+ * a data attribute so the content view's click handler can intercept them and
1676
+ * call loadDocument().
1677
+ *
1678
+ * @returns {Function} A link resolver callback
1679
+ */
1680
+ _createLinkResolver()
1681
+ {
1682
+ return (pHref, pLinkText) =>
1683
+ {
1684
+ // Only intercept internal markdown links
1685
+ if (pHref.match(/^https?:\/\//))
1686
+ {
1687
+ // External link — open in new tab
1688
+ return { href: pHref, target: '_blank', rel: 'noopener' };
1689
+ }
1690
+
1691
+ // help: prefix — internal documentation link (e.g. help:book-list.md#section)
1692
+ if (pHref.match(/^help:/))
1693
+ {
1694
+ let tmpHelpPath = pHref.replace(/^help:/, '');
1695
+ return {
1696
+ href: 'javascript:void(0)',
1697
+ target: '',
1698
+ rel: 'pict-inline-doc-link:' + tmpHelpPath
1699
+ };
1700
+ }
1701
+
1702
+ // Internal doc link — mark for interception
1703
+ let tmpPath = pHref.replace(/^\.\//, '').replace(/^\//, '');
1704
+ return {
1705
+ href: 'javascript:void(0)',
1706
+ // Use a data attribute for the click handler
1707
+ // The content view will wire up click interception
1708
+ target: '',
1709
+ rel: 'pict-inline-doc-link:' + tmpPath
1710
+ };
1711
+ };
1712
+ }
1713
+
1714
+ /**
1715
+ * Create a link resolver for tooltip content.
1716
+ *
1717
+ * Handles help:path.md#anchor links that open documents in the help
1718
+ * panel. These links get a special rel attribute so click handlers
1719
+ * can intercept them.
1720
+ *
1721
+ * External links open in a new tab. All other links are treated as
1722
+ * help: links within the tooltip context.
1723
+ *
1724
+ * @returns {Function} A link resolver callback
1725
+ */
1726
+ _createTooltipLinkResolver()
1727
+ {
1728
+ return (pHref, pLinkText) =>
1729
+ {
1730
+ // External links — open in new tab
1731
+ if (pHref.match(/^https?:\/\//))
1732
+ {
1733
+ return { href: pHref, target: '_blank', rel: 'noopener' };
1734
+ }
1735
+
1736
+ // Strip help: prefix if present
1737
+ let tmpPath = pHref.replace(/^help:/, '');
1738
+ // Clean relative prefixes
1739
+ tmpPath = tmpPath.replace(/^\.\//, '').replace(/^\//, '');
1740
+
1741
+ return {
1742
+ href: 'javascript:void(0)',
1743
+ target: '',
1744
+ rel: 'pict-inline-doc-help:' + tmpPath
1745
+ };
1746
+ };
1747
+ }
1748
+
1749
+ /**
1750
+ * Create an image resolver closure.
1751
+ *
1752
+ * @param {string} pDocURL - The URL the document was fetched from
1753
+ * @returns {Function} An image resolver callback
1754
+ */
1755
+ _createImageResolver(pDocURL)
1756
+ {
1757
+ let tmpBaseDir = '';
1758
+ if (pDocURL)
1759
+ {
1760
+ let tmpLastSlash = pDocURL.lastIndexOf('/');
1761
+ if (tmpLastSlash >= 0)
1762
+ {
1763
+ tmpBaseDir = pDocURL.substring(0, tmpLastSlash + 1);
1764
+ }
1765
+ }
1766
+
1767
+ return (pSrc, pAlt) =>
1768
+ {
1769
+ if (pSrc.match(/^https?:\/\//) || pSrc.match(/^data:/) || pSrc.match(/^\//))
1770
+ {
1771
+ return pSrc;
1772
+ }
1773
+ return tmpBaseDir + pSrc;
1774
+ };
1775
+ }
1776
+
1777
+ /**
1778
+ * Load and parse _sidebar.md from the docs base URL.
1779
+ *
1780
+ * @param {Function} fCallback - Callback when done
1781
+ */
1782
+ _loadSidebar(fCallback)
1783
+ {
1784
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
1785
+ let tmpState = this.pict.AppData.InlineDocumentation;
1786
+ let tmpDocsBase = tmpState.DocsBaseURL || '';
1787
+
1788
+ fetch(tmpDocsBase + '_sidebar.md')
1789
+ .then((pResponse) =>
1790
+ {
1791
+ if (!pResponse.ok)
1792
+ {
1793
+ return null;
1794
+ }
1795
+ return pResponse.text();
1796
+ })
1797
+ .then((pMarkdown) =>
1798
+ {
1799
+ if (!pMarkdown)
1800
+ {
1801
+ this.log.info('InlineDocumentation: No _sidebar.md found.');
1802
+ return tmpCallback();
1803
+ }
1804
+
1805
+ tmpState.SidebarGroups = this._parseSidebarMarkdown(pMarkdown);
1806
+ return tmpCallback();
1807
+ })
1808
+ .catch((pError) =>
1809
+ {
1810
+ this.log.warn(`InlineDocumentation: Error loading _sidebar.md: ${pError}`);
1811
+ return tmpCallback();
1812
+ });
1813
+ }
1814
+
1815
+ /**
1816
+ * Load topic definitions from a JSON file.
1817
+ *
1818
+ * Expected format:
1819
+ * {
1820
+ * "getting-started": {
1821
+ * "Name": "Getting Started",
1822
+ * "Documents": ["README.md", "getting-started.md"]
1823
+ * }
1824
+ * }
1825
+ *
1826
+ * @param {string} [pTopicsURL] - URL to fetch topics from (defaults to DocsBaseURL + '_topics.json')
1827
+ * @param {Function} fCallback - Callback when done
1828
+ */
1829
+ _loadTopics(pTopicsURL, fCallback)
1830
+ {
1831
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
1832
+ let tmpState = this.pict.AppData.InlineDocumentation;
1833
+ let tmpDocsBase = tmpState.DocsBaseURL || '';
1834
+
1835
+ let tmpURL = pTopicsURL || (tmpDocsBase + '_topics.json');
1836
+
1837
+ fetch(tmpURL)
1838
+ .then((pResponse) =>
1839
+ {
1840
+ if (!pResponse.ok)
1841
+ {
1842
+ return null;
1843
+ }
1844
+ return pResponse.json();
1845
+ })
1846
+ .then((pTopics) =>
1847
+ {
1848
+ if (pTopics)
1849
+ {
1850
+ tmpState.Topics = pTopics;
1851
+ this.log.info(`InlineDocumentation: Topics loaded (${Object.keys(pTopics).length} topics).`);
1852
+ }
1853
+ else
1854
+ {
1855
+ this.log.info('InlineDocumentation: No _topics.json found.');
1856
+ }
1857
+ return tmpCallback();
1858
+ })
1859
+ .catch((pError) =>
1860
+ {
1861
+ this.log.info(`InlineDocumentation: No topics loaded (${pError}).`);
1862
+ return tmpCallback();
1863
+ });
1864
+ }
1865
+
1866
+ /**
1867
+ * Parse _sidebar.md into a navigation structure.
1868
+ *
1869
+ * Returns an array of group objects:
1870
+ * [{ Name, Key, Path, Items: [{ Name, Path }] }]
1871
+ *
1872
+ * @param {string} pMarkdown - Raw _sidebar.md content
1873
+ * @returns {Array} Parsed sidebar groups
1874
+ */
1875
+ _parseSidebarMarkdown(pMarkdown)
1876
+ {
1877
+ let tmpGroups = [];
1878
+ let tmpCurrentGroup = null;
1879
+ let tmpLines = pMarkdown.split('\n');
1880
+
1881
+ for (let i = 0; i < tmpLines.length; i++)
1882
+ {
1883
+ let tmpLine = tmpLines[i];
1884
+
1885
+ if (!tmpLine.trim())
1886
+ {
1887
+ continue;
1888
+ }
1889
+
1890
+ let tmpIndentMatch = tmpLine.match(/^(\s*)/);
1891
+ let tmpIndent = tmpIndentMatch ? tmpIndentMatch[1].length : 0;
1892
+ let tmpContent = tmpLine.trim();
1893
+
1894
+ let tmpListMatch = tmpContent.match(/^[-*+]\s+(.*)/);
1895
+ if (!tmpListMatch)
1896
+ {
1897
+ continue;
1898
+ }
1899
+
1900
+ let tmpItemContent = tmpListMatch[1].trim();
1901
+ let tmpLinkMatch = tmpItemContent.match(/^\[([^\]]+)\]\(([^)]+)\)/);
1902
+
1903
+ if (tmpIndent < 2)
1904
+ {
1905
+ // Top-level item — group header
1906
+ if (tmpLinkMatch)
1907
+ {
1908
+ let tmpName = tmpLinkMatch[1].trim();
1909
+ let tmpPath = tmpLinkMatch[2].trim();
1910
+
1911
+ tmpCurrentGroup = {
1912
+ Name: tmpName,
1913
+ Key: tmpName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
1914
+ Path: this._normalizePath(tmpPath),
1915
+ Items: []
1916
+ };
1917
+ tmpGroups.push(tmpCurrentGroup);
1918
+ }
1919
+ else
1920
+ {
1921
+ tmpCurrentGroup = {
1922
+ Name: tmpItemContent,
1923
+ Key: tmpItemContent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
1924
+ Path: '',
1925
+ Items: []
1926
+ };
1927
+ tmpGroups.push(tmpCurrentGroup);
1928
+ }
1929
+ }
1930
+ else if (tmpCurrentGroup)
1931
+ {
1932
+ // Indented item — document within the current group
1933
+ if (tmpLinkMatch)
1934
+ {
1935
+ tmpCurrentGroup.Items.push({
1936
+ Name: tmpLinkMatch[1].trim(),
1937
+ Path: this._normalizePath(tmpLinkMatch[2].trim())
1938
+ });
1939
+ }
1940
+ else
1941
+ {
1942
+ tmpCurrentGroup.Items.push({
1943
+ Name: tmpItemContent,
1944
+ Path: ''
1945
+ });
1946
+ }
1947
+ }
1948
+ }
1949
+
1950
+ return tmpGroups;
1951
+ }
1952
+
1953
+ /**
1954
+ * Normalize a document path from sidebar links.
1955
+ * Strips leading slashes and ./ prefixes.
1956
+ *
1957
+ * @param {string} pPath - The raw path
1958
+ * @returns {string} The normalized path
1959
+ */
1960
+ _normalizePath(pPath)
1961
+ {
1962
+ if (!pPath)
1963
+ {
1964
+ return '';
1965
+ }
1966
+ return pPath.replace(/^\.\//, '').replace(/^\//, '');
1967
+ }
1968
+
1969
+ /**
1970
+ * Get an error page HTML block for a missing document.
1971
+ *
1972
+ * @param {string} pPath - The path that was not found
1973
+ * @returns {string} HTML to display
1974
+ */
1975
+ _getErrorPageHTML(pPath)
1976
+ {
1977
+ let tmpPath = this._ContentProvider.escapeHTML(pPath || 'unknown');
1978
+ return '<div class="pict-inline-doc-not-found">'
1979
+ + '<h2>Page Not Found</h2>'
1980
+ + '<p>The document <code>' + tmpPath + '</code> could not be loaded.</p>'
1981
+ + '</div>';
1982
+ }
1983
+ }
1984
+
1985
+ const _DefaultConfiguration =
1986
+ {
1987
+ ProviderIdentifier: "Pict-InlineDocumentation",
1988
+
1989
+ AutoInitialize: true,
1990
+ AutoInitializeOrdinal: 0
1991
+ };
1992
+
1993
+ module.exports = InlineDocumentationProvider;
1994
+
1995
+ module.exports.default_configuration = _DefaultConfiguration;