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.
- package/README.md +107 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +83 -0
- package/docs/_cover.md +15 -0
- package/docs/_sidebar.md +24 -0
- package/docs/_topbar.md +8 -0
- package/docs/_version.json +7 -0
- package/docs/api-reference.md +185 -0
- package/docs/architecture.md +103 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/embedding-level1-sidebar.md +92 -0
- package/docs/embedding-level2-routes.md +86 -0
- package/docs/embedding-level3-tooltips.md +97 -0
- package/docs/embedding-level4-autogen.md +126 -0
- package/docs/index.html +39 -0
- package/docs/overview.md +42 -0
- package/docs/quickstart.md +95 -0
- package/docs/reference.md +73 -0
- package/docs/retold-catalog.json +181 -0
- package/docs/retold-keyword-index.json +4374 -0
- package/example_applications/basic/docs/README.md +40 -0
- package/example_applications/basic/docs/_sidebar.md +4 -0
- package/example_applications/basic/docs/_topics.json +10 -0
- package/example_applications/basic/docs/advanced-topics.md +47 -0
- package/example_applications/basic/docs/getting-started.md +70 -0
- package/example_applications/basic/index.html +100 -0
- package/example_applications/bookshop/.quackage.json +10 -0
- package/example_applications/bookshop/Pict-Application-Bookshop-Configuration.json +15 -0
- package/example_applications/bookshop/Pict-Application-Bookshop.js +218 -0
- package/example_applications/bookshop/data/BookshopData.json +65 -0
- package/example_applications/bookshop/data/pict_documentation_topics.json +46 -0
- package/example_applications/bookshop/docs/_sidebar.md +6 -0
- package/example_applications/bookshop/docs/book-detail.md +21 -0
- package/example_applications/bookshop/docs/book-list.md +21 -0
- package/example_applications/bookshop/docs/search-filter.md +18 -0
- package/example_applications/bookshop/docs/store.md +29 -0
- package/example_applications/bookshop/docs/welcome.md +23 -0
- package/example_applications/bookshop/html/index.html +236 -0
- package/example_applications/bookshop/package.json +34 -0
- package/example_applications/bookshop/views/PictView-Bookshop-BookList.js +324 -0
- package/example_applications/bookshop/views/PictView-Bookshop-HelpToggle.js +44 -0
- package/example_applications/bookshop/views/PictView-Bookshop-Store.js +271 -0
- package/package.json +55 -0
- package/source/Pict-Section-InlineDocumentation.js +10 -0
- package/source/providers/Pict-Provider-InlineDocumentation.js +1995 -0
- package/source/views/Pict-View-InlineDocumentation-Content.js +542 -0
- package/source/views/Pict-View-InlineDocumentation-Layout.js +206 -0
- package/source/views/Pict-View-InlineDocumentation-Nav.js +475 -0
- package/source/views/Pict-View-InlineDocumentation-TopicManager.js +1623 -0
- package/test/Browser_Integration_tests.js +1449 -0
- 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 = '❓';
|
|
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, '&')
|
|
1514
|
+
.replace(/</g, '<')
|
|
1515
|
+
.replace(/>/g, '>')
|
|
1516
|
+
.replace(/"/g, '"');
|
|
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;
|