retold-content-system 1.0.15 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -5
- package/source/Pict-Application-ContentEditor.js +156 -7
- package/source/cli/ContentSystem-Server-Setup.js +88 -0
- package/source/providers/Pict-Provider-ContentEditor.js +2 -2
- package/source/views/PictView-Editor-Layout.js +217 -315
- package/source/views/PictView-Editor-MarkdownReference.js +9 -9
- package/source/views/PictView-Editor-TopBar.js +4 -1
- package/source/views/PictView-Editor-Topics.js +109 -112
- package/web-application/retold-content-system.compatible.js +1711 -566
- package/web-application/retold-content-system.compatible.js.map +1 -1
- package/web-application/retold-content-system.compatible.min.js +41 -39
- package/web-application/retold-content-system.compatible.min.js.map +1 -1
- package/web-application/retold-content-system.js +1644 -500
- package/web-application/retold-content-system.js.map +1 -1
- package/web-application/retold-content-system.min.js +49 -47
- package/web-application/retold-content-system.min.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retold-content-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Retold Content System - Markdown content viewer and editor",
|
|
5
5
|
"main": "source/Pict-ContentSystem-Bundle.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,17 +31,19 @@
|
|
|
31
31
|
"ultravisor-beacon": "^0.0.11"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"fable": "^3.1.
|
|
34
|
+
"fable": "^3.1.70",
|
|
35
35
|
"orator": "^6.0.4",
|
|
36
36
|
"orator-serviceserver-restify": "^2.0.10",
|
|
37
37
|
"pict": "^1.0.363",
|
|
38
38
|
"pict-application": "^1.0.33",
|
|
39
39
|
"pict-provider": "^1.0.12",
|
|
40
|
+
"pict-provider-vocabulary": "^0.0.6",
|
|
40
41
|
"pict-section-code": "^1.0.4",
|
|
41
|
-
"pict-
|
|
42
|
-
"pict-section-content": "^0.1.4",
|
|
42
|
+
"pict-section-content": "^0.1.5",
|
|
43
43
|
"pict-section-filebrowser": "^0.0.2",
|
|
44
|
-
"pict-section-
|
|
44
|
+
"pict-section-inlinedocumentation": "^0.0.5",
|
|
45
|
+
"pict-section-markdowneditor": "^1.0.10",
|
|
46
|
+
"pict-section-modal": "^0.0.4",
|
|
45
47
|
"pict-service-commandlineutility": "^1.0.19",
|
|
46
48
|
"pict-view": "^1.0.68"
|
|
47
49
|
},
|
|
@@ -6,6 +6,15 @@ const libPictSectionFileBrowser = require('pict-section-filebrowser');
|
|
|
6
6
|
// Vocabulary auto-linking + popover system (shared with retold-labs)
|
|
7
7
|
const libPictProviderVocabulary = require('pict-provider-vocabulary');
|
|
8
8
|
|
|
9
|
+
// Inline documentation (right-side panel)
|
|
10
|
+
const libPictSectionInlineDocumentation = require('pict-section-inlinedocumentation');
|
|
11
|
+
|
|
12
|
+
// Modal system (panels, dialogs, tooltips, toasts)
|
|
13
|
+
const libPictSectionModal = require('pict-section-modal');
|
|
14
|
+
|
|
15
|
+
// Content rendering
|
|
16
|
+
const libPictSectionContent = require('pict-section-content');
|
|
17
|
+
|
|
9
18
|
// Provider
|
|
10
19
|
const libContentEditorProvider = require('./providers/Pict-Provider-ContentEditor.js');
|
|
11
20
|
|
|
@@ -44,16 +53,44 @@ class ContentEditorApplication extends libPictApplication
|
|
|
44
53
|
libPictProviderVocabulary);
|
|
45
54
|
|
|
46
55
|
// Register the vocabulary management view from the provider.
|
|
47
|
-
// Mount it at the vocabulary panel container
|
|
48
|
-
//
|
|
56
|
+
// Mount it at the vocabulary panel container in the sidebar.
|
|
57
|
+
// The onEditTerm callback opens the term file in the main
|
|
58
|
+
// markdown editor instead of rendering an inline textarea.
|
|
59
|
+
let tmpPictRef = this.pict;
|
|
49
60
|
this.pict.addView('ContentEditor-Vocabulary',
|
|
50
61
|
Object.assign({}, libPictProviderVocabulary.VocabularyManagerView.default_configuration,
|
|
51
62
|
{
|
|
52
63
|
DefaultDestinationAddress: '#ContentEditor-Vocabulary-Container',
|
|
53
|
-
VocabularyRoute: '#/vocabulary'
|
|
64
|
+
VocabularyRoute: '#/vocabulary',
|
|
65
|
+
VocabularyFolderPath: 'vocabulary/',
|
|
66
|
+
onEditTerm: function (pSlug, pFilePath)
|
|
67
|
+
{
|
|
68
|
+
// Open the vocabulary term file in the main editor
|
|
69
|
+
if (tmpPictRef && tmpPictRef.PictApplication && typeof tmpPictRef.PictApplication.navigateToFile === 'function')
|
|
70
|
+
{
|
|
71
|
+
tmpPictRef.PictApplication.navigateToFile(pFilePath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
}),
|
|
55
75
|
libPictProviderVocabulary.VocabularyManagerView);
|
|
56
76
|
|
|
77
|
+
// Inline documentation provider — powers the right-side
|
|
78
|
+
// documentation panel. Renders markdown docs from the content
|
|
79
|
+
// tree's docs/ folder with topic navigation, editing, and
|
|
80
|
+
// vocabulary auto-linking.
|
|
81
|
+
this.pict.addProvider('Pict-InlineDocumentation',
|
|
82
|
+
libPictSectionInlineDocumentation.default_configuration,
|
|
83
|
+
libPictSectionInlineDocumentation);
|
|
84
|
+
|
|
85
|
+
// Content rendering provider (for markdown parsing with
|
|
86
|
+
// vocabulary resolver in the documentation panel).
|
|
87
|
+
this.pict.addProvider('Pict-Content',
|
|
88
|
+
libPictSectionContent.PictContentProvider.default_configuration,
|
|
89
|
+
libPictSectionContent.PictContentProvider);
|
|
90
|
+
|
|
91
|
+
// Register the modal system (panels, dialogs, tooltips, toasts)
|
|
92
|
+
this.pict.addView('Pict-Section-Modal', libPictSectionModal.default_configuration, libPictSectionModal);
|
|
93
|
+
|
|
57
94
|
// Register views
|
|
58
95
|
this.pict.addView('ContentEditor-Layout', libViewLayout.default_configuration, libViewLayout);
|
|
59
96
|
this.pict.addView('ContentEditor-TopBar', libViewTopBar.default_configuration, libViewTopBar);
|
|
@@ -118,6 +155,17 @@ class ContentEditorApplication extends libPictApplication
|
|
|
118
155
|
if (typeof (window) !== 'undefined')
|
|
119
156
|
{
|
|
120
157
|
window.pict = this.pict;
|
|
158
|
+
|
|
159
|
+
// Warn the user before closing the tab/window with unsaved changes
|
|
160
|
+
let tmpPictRef = this.pict;
|
|
161
|
+
window.addEventListener('beforeunload', function (pEvent)
|
|
162
|
+
{
|
|
163
|
+
if (tmpPictRef.AppData.ContentEditor && tmpPictRef.AppData.ContentEditor.IsDirty)
|
|
164
|
+
{
|
|
165
|
+
pEvent.preventDefault();
|
|
166
|
+
pEvent.returnValue = '';
|
|
167
|
+
}
|
|
168
|
+
});
|
|
121
169
|
}
|
|
122
170
|
|
|
123
171
|
// Initialize application state
|
|
@@ -225,6 +273,76 @@ class ContentEditorApplication extends libPictApplication
|
|
|
225
273
|
tmpVocabProvider.loadFromURL('/api/vocabulary/index');
|
|
226
274
|
}
|
|
227
275
|
|
|
276
|
+
// Initialize the inline documentation panel. It reads from
|
|
277
|
+
// the content tree's /content/ path via the same server.
|
|
278
|
+
// The panel starts collapsed — the user expands it via the
|
|
279
|
+
// edge tab or the Docs button in the top bar.
|
|
280
|
+
// Initialize the inline documentation panel, then attach the
|
|
281
|
+
// resizable/collapsible panel behavior once the layout has
|
|
282
|
+
// rendered into the container. The panel() call must happen
|
|
283
|
+
// AFTER the async render so the edge element isn't wiped out.
|
|
284
|
+
let tmpDocProvider = this.pict.providers && this.pict.providers['Pict-InlineDocumentation'];
|
|
285
|
+
if (tmpDocProvider && typeof tmpDocProvider.initializeDocumentation === 'function')
|
|
286
|
+
{
|
|
287
|
+
let tmpSelfApp = this;
|
|
288
|
+
tmpDocProvider.initializeDocumentation(
|
|
289
|
+
{
|
|
290
|
+
DocsBaseURL: '/content/',
|
|
291
|
+
ContainerAddress: '#ContentEditor-Documentation-Panel',
|
|
292
|
+
SearchIndexURL: '/content/retold-keyword-index.json',
|
|
293
|
+
ExternalDocBaseURL: 'https://stevenvelozo.github.io/retold/#/doc/',
|
|
294
|
+
EditEnabled: false,
|
|
295
|
+
TopicManagerEnabled: false
|
|
296
|
+
},
|
|
297
|
+
function ()
|
|
298
|
+
{
|
|
299
|
+
// Layout has rendered — attach resize + collapse.
|
|
300
|
+
// Full persistence: width and collapsed state both
|
|
301
|
+
// survive across page loads via localStorage.
|
|
302
|
+
let tmpModal = tmpSelfApp.pict.views['Pict-Section-Modal'];
|
|
303
|
+
if (tmpModal && typeof tmpModal.panel === 'function')
|
|
304
|
+
{
|
|
305
|
+
tmpSelfApp._docPanel = tmpModal.panel('#ContentEditor-Documentation-Panel',
|
|
306
|
+
{
|
|
307
|
+
position: 'right',
|
|
308
|
+
width: 340,
|
|
309
|
+
minWidth: 260,
|
|
310
|
+
maxWidth: 600,
|
|
311
|
+
collapsible: true,
|
|
312
|
+
collapsed: true,
|
|
313
|
+
persist: true,
|
|
314
|
+
persistKey: 'ContentEditor-DocPanel'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Restore the last-viewed document from localStorage,
|
|
319
|
+
// or default to README.md. This ensures the panel
|
|
320
|
+
// shows the right content whether it starts collapsed
|
|
321
|
+
// or expanded from persisted state.
|
|
322
|
+
let tmpInlineDoc = tmpSelfApp.pict.providers['Pict-InlineDocumentation'];
|
|
323
|
+
if (tmpInlineDoc && typeof tmpInlineDoc.loadDocument === 'function')
|
|
324
|
+
{
|
|
325
|
+
let tmpLastDoc = 'README.md';
|
|
326
|
+
try
|
|
327
|
+
{
|
|
328
|
+
let tmpStored = localStorage.getItem('ContentEditor-DocPanel-LastDoc');
|
|
329
|
+
if (tmpStored) tmpLastDoc = tmpStored;
|
|
330
|
+
}
|
|
331
|
+
catch (e) { /* ignore */ }
|
|
332
|
+
tmpInlineDoc.loadDocument(tmpLastDoc);
|
|
333
|
+
|
|
334
|
+
// Persist the current document path on each navigation
|
|
335
|
+
let tmpOrigLoad = tmpInlineDoc.loadDocument.bind(tmpInlineDoc);
|
|
336
|
+
tmpInlineDoc.loadDocument = function (pPath, fCb)
|
|
337
|
+
{
|
|
338
|
+
try { localStorage.setItem('ContentEditor-DocPanel-LastDoc', pPath); }
|
|
339
|
+
catch (e) { /* ignore */ }
|
|
340
|
+
return tmpOrigLoad(pPath, fCb);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
228
346
|
return super.onAfterInitializeAsync(fCallback);
|
|
229
347
|
}
|
|
230
348
|
|
|
@@ -488,7 +606,7 @@ class ContentEditorApplication extends libPictApplication
|
|
|
488
606
|
*/
|
|
489
607
|
loadMediaPreview(pMediaType, pContentURL, pFileName)
|
|
490
608
|
{
|
|
491
|
-
let tmpContainer =
|
|
609
|
+
let tmpContainer = this.pict.ContentAssignment.getElement('#ContentEditor-MediaPreviewPlaceholder')[0];
|
|
492
610
|
if (!tmpContainer)
|
|
493
611
|
{
|
|
494
612
|
return;
|
|
@@ -704,6 +822,37 @@ class ContentEditorApplication extends libPictApplication
|
|
|
704
822
|
return;
|
|
705
823
|
}
|
|
706
824
|
|
|
825
|
+
// Guard: if the current file has unsaved changes, confirm
|
|
826
|
+
// before navigating away. This catches all entry points —
|
|
827
|
+
// file browser clicks, vocabulary edits, topic navigation,
|
|
828
|
+
// hash changes, and new file creation.
|
|
829
|
+
if (this.pict.AppData.ContentEditor.IsDirty)
|
|
830
|
+
{
|
|
831
|
+
let tmpSelf = this;
|
|
832
|
+
let tmpModal = this.pict.views['Pict-Section-Modal'];
|
|
833
|
+
if (tmpModal && typeof tmpModal.confirm === 'function')
|
|
834
|
+
{
|
|
835
|
+
tmpModal.confirm(
|
|
836
|
+
'You have unsaved changes to ' + this.pict.AppData.ContentEditor.CurrentFile + '. Discard and open a different file?',
|
|
837
|
+
{ title: 'Unsaved Changes', confirmLabel: 'Discard', dangerous: true })
|
|
838
|
+
.then(function (pConfirmed)
|
|
839
|
+
{
|
|
840
|
+
if (pConfirmed)
|
|
841
|
+
{
|
|
842
|
+
tmpSelf.pict.AppData.ContentEditor.IsDirty = false;
|
|
843
|
+
tmpSelf.navigateToFile(pFilePath);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
// Fallback: native confirm
|
|
849
|
+
if (typeof confirm !== 'undefined' && !confirm('You have unsaved changes. Discard and open a different file?'))
|
|
850
|
+
{
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
this.pict.AppData.ContentEditor.IsDirty = false;
|
|
854
|
+
}
|
|
855
|
+
|
|
707
856
|
let tmpSelf = this;
|
|
708
857
|
|
|
709
858
|
// Determine which editor to use before fetching content
|
|
@@ -981,7 +1130,7 @@ class ContentEditorApplication extends libPictApplication
|
|
|
981
1130
|
*/
|
|
982
1131
|
_showCloseConfirmation()
|
|
983
1132
|
{
|
|
984
|
-
let tmpOverlay =
|
|
1133
|
+
let tmpOverlay = this.pict.ContentAssignment.getElement('#ContentEditor-ConfirmOverlay')[0];
|
|
985
1134
|
if (tmpOverlay)
|
|
986
1135
|
{
|
|
987
1136
|
tmpOverlay.classList.add('open');
|
|
@@ -1015,7 +1164,7 @@ class ContentEditorApplication extends libPictApplication
|
|
|
1015
1164
|
*/
|
|
1016
1165
|
_hideCloseConfirmation()
|
|
1017
1166
|
{
|
|
1018
|
-
let tmpOverlay =
|
|
1167
|
+
let tmpOverlay = this.pict.ContentAssignment.getElement('#ContentEditor-ConfirmOverlay')[0];
|
|
1019
1168
|
if (tmpOverlay)
|
|
1020
1169
|
{
|
|
1021
1170
|
tmpOverlay.classList.remove('open');
|
|
@@ -1355,7 +1504,7 @@ class ContentEditorApplication extends libPictApplication
|
|
|
1355
1504
|
*/
|
|
1356
1505
|
updateStats()
|
|
1357
1506
|
{
|
|
1358
|
-
let tmpStatsEl =
|
|
1507
|
+
let tmpStatsEl = this.pict.ContentAssignment.getElement('#ContentEditor-Stats')[0];
|
|
1359
1508
|
if (!tmpStatsEl)
|
|
1360
1509
|
{
|
|
1361
1510
|
return;
|
|
@@ -466,6 +466,94 @@ function setupContentSystemServer(pOptions, fCallback)
|
|
|
466
466
|
return fNext();
|
|
467
467
|
});
|
|
468
468
|
|
|
469
|
+
// --- GET /api/vocabulary/term/:slug ---
|
|
470
|
+
// Read a single vocabulary term's markdown body.
|
|
471
|
+
tmpServiceServer.get('/api/vocabulary/term/:slug',
|
|
472
|
+
(pRequest, pResponse, fNext) =>
|
|
473
|
+
{
|
|
474
|
+
try
|
|
475
|
+
{
|
|
476
|
+
let tmpSlug = sanitizePath(pRequest.params.slug);
|
|
477
|
+
if (!tmpSlug)
|
|
478
|
+
{
|
|
479
|
+
pResponse.send(400, { Error: 'Invalid slug' });
|
|
480
|
+
return fNext();
|
|
481
|
+
}
|
|
482
|
+
let tmpFile = libPath.join(tmpContentPath, 'vocabulary', tmpSlug + '.md');
|
|
483
|
+
if (!libFs.existsSync(tmpFile))
|
|
484
|
+
{
|
|
485
|
+
pResponse.send(404, { Error: 'Term not found: ' + tmpSlug });
|
|
486
|
+
return fNext();
|
|
487
|
+
}
|
|
488
|
+
let tmpBody = libFs.readFileSync(tmpFile, 'utf8');
|
|
489
|
+
pResponse.send({ Slug: tmpSlug, Body: tmpBody });
|
|
490
|
+
}
|
|
491
|
+
catch (pError)
|
|
492
|
+
{
|
|
493
|
+
pResponse.send(500, { Error: pError.message });
|
|
494
|
+
}
|
|
495
|
+
return fNext();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// --- PUT /api/vocabulary/term/:slug ---
|
|
499
|
+
// Create or update a vocabulary term.
|
|
500
|
+
tmpServiceServer.put('/api/vocabulary/term/:slug',
|
|
501
|
+
(pRequest, pResponse, fNext) =>
|
|
502
|
+
{
|
|
503
|
+
try
|
|
504
|
+
{
|
|
505
|
+
let tmpSlug = sanitizePath(pRequest.params.slug);
|
|
506
|
+
if (!tmpSlug)
|
|
507
|
+
{
|
|
508
|
+
pResponse.send(400, { Error: 'Invalid slug' });
|
|
509
|
+
return fNext();
|
|
510
|
+
}
|
|
511
|
+
let tmpVocabDir = libPath.join(tmpContentPath, 'vocabulary');
|
|
512
|
+
if (!libFs.existsSync(tmpVocabDir))
|
|
513
|
+
{
|
|
514
|
+
libFs.mkdirSync(tmpVocabDir, { recursive: true });
|
|
515
|
+
}
|
|
516
|
+
let tmpBody = (pRequest.body && pRequest.body.body) || '';
|
|
517
|
+
let tmpFile = libPath.join(tmpVocabDir, tmpSlug + '.md');
|
|
518
|
+
libFs.writeFileSync(tmpFile, tmpBody, 'utf8');
|
|
519
|
+
pResponse.send({ Success: true, Slug: tmpSlug });
|
|
520
|
+
}
|
|
521
|
+
catch (pError)
|
|
522
|
+
{
|
|
523
|
+
pResponse.send(500, { Error: pError.message });
|
|
524
|
+
}
|
|
525
|
+
return fNext();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// --- DELETE /api/vocabulary/term/:slug ---
|
|
529
|
+
// Delete a vocabulary term's markdown file.
|
|
530
|
+
tmpServiceServer.del('/api/vocabulary/term/:slug',
|
|
531
|
+
(pRequest, pResponse, fNext) =>
|
|
532
|
+
{
|
|
533
|
+
try
|
|
534
|
+
{
|
|
535
|
+
let tmpSlug = sanitizePath(pRequest.params.slug);
|
|
536
|
+
if (!tmpSlug)
|
|
537
|
+
{
|
|
538
|
+
pResponse.send(400, { Error: 'Invalid slug' });
|
|
539
|
+
return fNext();
|
|
540
|
+
}
|
|
541
|
+
let tmpFile = libPath.join(tmpContentPath, 'vocabulary', tmpSlug + '.md');
|
|
542
|
+
if (!libFs.existsSync(tmpFile))
|
|
543
|
+
{
|
|
544
|
+
pResponse.send(404, { Error: 'Term not found: ' + tmpSlug });
|
|
545
|
+
return fNext();
|
|
546
|
+
}
|
|
547
|
+
libFs.unlinkSync(tmpFile);
|
|
548
|
+
pResponse.send({ Success: true, Slug: tmpSlug });
|
|
549
|
+
}
|
|
550
|
+
catch (pError)
|
|
551
|
+
{
|
|
552
|
+
pResponse.send(500, { Error: pError.message });
|
|
553
|
+
}
|
|
554
|
+
return fNext();
|
|
555
|
+
});
|
|
556
|
+
|
|
469
557
|
// Serve content files (markdown, images, etc.) at /content/
|
|
470
558
|
tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
|
|
471
559
|
|
|
@@ -29,7 +29,7 @@ class ContentEditorProvider extends libPictProvider
|
|
|
29
29
|
return tmpCallback('No file path specified', '');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
fetch('/api/content/read/' + encodeURIComponent(
|
|
32
|
+
fetch('/api/content/read/' + pFilePath.split('/').map(encodeURIComponent).join('/'))
|
|
33
33
|
.then((pResponse) =>
|
|
34
34
|
{
|
|
35
35
|
if (!pResponse.ok)
|
|
@@ -69,7 +69,7 @@ class ContentEditorProvider extends libPictProvider
|
|
|
69
69
|
return tmpCallback('No file path specified');
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
fetch('/api/content/save/' + encodeURIComponent(
|
|
72
|
+
fetch('/api/content/save/' + pFilePath.split('/').map(encodeURIComponent).join('/'),
|
|
73
73
|
{
|
|
74
74
|
method: 'PUT',
|
|
75
75
|
headers: { 'Content-Type': 'application/json' },
|