retold-content-system 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-content-system",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Retold Content System - Markdown content viewer and editor",
5
5
  "main": "source/Pict-ContentSystem-Bundle.js",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "LICENSE"
18
18
  ],
19
19
  "scripts": {
20
- "start": "node source/server/Simple-Server.js",
20
+ "start": "node source/cli/ContentSystem-CLI-Run.js serve",
21
21
  "build": "npx quack build && npx quack copy",
22
22
  "build-codemirror": "node build/build-codemirror-bundle.js",
23
23
  "build-codejar": "node build/build-codejar-bundle.js",
@@ -27,11 +27,14 @@
27
27
  },
28
28
  "author": "steven velozo <steven@velozo.com>",
29
29
  "license": "MIT",
30
+ "optionalDependencies": {
31
+ "ultravisor-beacon": "^0.0.1"
32
+ },
30
33
  "dependencies": {
31
- "fable": "^3.1.65",
34
+ "fable": "^3.1.66",
32
35
  "orator": "^6.0.4",
33
36
  "orator-serviceserver-restify": "^2.0.9",
34
- "pict": "^1.0.357",
37
+ "pict": "^1.0.359",
35
38
  "pict-application": "^1.0.33",
36
39
  "pict-docuserve": "^0.0.32",
37
40
  "pict-provider": "^1.0.12",
@@ -50,7 +53,7 @@
50
53
  "codemirror": "^6.0.2",
51
54
  "esbuild": "^0.27.4",
52
55
  "highlight.js": "^11.11.1",
53
- "quackage": "^1.0.64"
56
+ "quackage": "^1.0.65"
54
57
  },
55
58
  "copyFilesSettings": {
56
59
  "whenFileExists": "overwrite"
@@ -117,7 +117,7 @@ class ContentEditorApplication extends libPictApplication
117
117
  // Settings
118
118
  AutoSegmentMarkdown: false,
119
119
  AutoSegmentDepth: 1,
120
- AutoContentPreview: true,
120
+ ContentPreviewMode: 'off',
121
121
  MarkdownEditingControls: true,
122
122
  MarkdownWordWrap: true,
123
123
  CodeWordWrap: false,
@@ -738,19 +738,8 @@ class ContentEditorApplication extends libPictApplication
738
738
  tmpEditorView.render();
739
739
  tmpEditorView.marshalToView();
740
740
 
741
- // Always ensure the global preview class is clear so
742
- // per-segment toggles work.
743
- tmpEditorView.togglePreview(true);
744
-
745
- // Set per-segment preview visibility based on the
746
- // Auto Content Preview setting. We must always loop
747
- // to clear any stale _hiddenPreviewSegments state
748
- // from previous file loads.
749
- let tmpShowPreviews = !!tmpSelf.pict.AppData.ContentEditor.AutoContentPreview;
750
- for (let tmpIdx in tmpEditorView._segmentEditors)
751
- {
752
- tmpEditorView.toggleSegmentPreview(parseInt(tmpIdx, 10), tmpShowPreviews);
753
- }
741
+ // Apply the Content Preview Mode setting
742
+ tmpEditorView.setPreviewMode(tmpSelf.pict.AppData.ContentEditor.ContentPreviewMode || 'off');
754
743
 
755
744
  // Apply the Editing Controls setting (line numbers
756
745
  // and right sidebar) via the library's toggleControls.
@@ -1426,7 +1415,7 @@ class ContentEditorApplication extends libPictApplication
1426
1415
  {
1427
1416
  AutoSegmentMarkdown: tmpSettings.AutoSegmentMarkdown,
1428
1417
  AutoSegmentDepth: tmpSettings.AutoSegmentDepth,
1429
- AutoContentPreview: tmpSettings.AutoContentPreview,
1418
+ ContentPreviewMode: tmpSettings.ContentPreviewMode,
1430
1419
  MarkdownEditingControls: tmpSettings.MarkdownEditingControls,
1431
1420
  MarkdownWordWrap: tmpSettings.MarkdownWordWrap,
1432
1421
  CodeWordWrap: tmpSettings.CodeWordWrap,
@@ -1479,9 +1468,14 @@ class ContentEditorApplication extends libPictApplication
1479
1468
  {
1480
1469
  tmpSettings.AutoSegmentDepth = tmpStored.AutoSegmentDepth;
1481
1470
  }
1482
- if (typeof (tmpStored.AutoContentPreview) === 'boolean')
1471
+ if (typeof (tmpStored.ContentPreviewMode) === 'string')
1472
+ {
1473
+ tmpSettings.ContentPreviewMode = tmpStored.ContentPreviewMode;
1474
+ }
1475
+ else if (typeof (tmpStored.AutoContentPreview) === 'boolean')
1483
1476
  {
1484
- tmpSettings.AutoContentPreview = tmpStored.AutoContentPreview;
1477
+ // Backward compat: migrate old boolean setting
1478
+ tmpSettings.ContentPreviewMode = tmpStored.AutoContentPreview ? 'bottom' : 'off';
1485
1479
  }
1486
1480
  if (typeof (tmpStored.MarkdownEditingControls) === 'boolean')
1487
1481
  {
@@ -402,6 +402,15 @@ function setupContentSystemServer(pOptions, fCallback)
402
402
  tmpOrator.startService(
403
403
  function ()
404
404
  {
405
+ // --- Optional Ultravisor Beacon Integration ---
406
+ // When pOptions.Beacon is provided and Enabled is true,
407
+ // register this content system as a beacon worker.
408
+ let tmpBeaconConfig = pOptions.Beacon || {};
409
+ if (tmpBeaconConfig.Enabled)
410
+ {
411
+ _initializeBeacon(tmpFable, tmpContentPath, tmpBeaconConfig);
412
+ }
413
+
405
414
  return fCallback(null,
406
415
  {
407
416
  Fable: tmpFable,
@@ -412,4 +421,266 @@ function setupContentSystemServer(pOptions, fCallback)
412
421
  });
413
422
  }
414
423
 
424
+ // ═══════════════════════════════════════════════════════════════════
425
+ // Ultravisor Beacon Integration
426
+ // ═══════════════════════════════════════════════════════════════════
427
+
428
+ /**
429
+ * Initialize the Ultravisor beacon service and register ContentSystem
430
+ * capabilities. This is opt-in — only called when Beacon.Enabled is true.
431
+ *
432
+ * @param {object} pFable - Fable instance
433
+ * @param {string} pContentPath - Resolved content directory path
434
+ * @param {object} pBeaconConfig - Beacon configuration object
435
+ */
436
+ function _initializeBeacon(pFable, pContentPath, pBeaconConfig)
437
+ {
438
+ let libBeaconService;
439
+ try
440
+ {
441
+ libBeaconService = require('ultravisor-beacon');
442
+ }
443
+ catch (pError)
444
+ {
445
+ pFable.log.warn(`Content System Beacon: ultravisor-beacon not installed. Skipping beacon init.`);
446
+ return;
447
+ }
448
+
449
+ pFable.serviceManager.addAndInstantiateServiceType('UltravisorBeacon', libBeaconService, pBeaconConfig);
450
+ let tmpBeacon = pFable.services.UltravisorBeacon;
451
+
452
+ tmpBeacon.registerCapability({
453
+ Capability: 'ContentSystem',
454
+ Name: 'ContentSystemProvider',
455
+ actions:
456
+ {
457
+ 'ReadFile':
458
+ {
459
+ Description: 'Read a content file',
460
+ SettingsSchema: [
461
+ { Name: 'FilePath', DataType: 'String', Required: true }
462
+ ],
463
+ Handler: function (pWorkItem, pContext, fCallback)
464
+ {
465
+ try
466
+ {
467
+ let tmpFilePath = sanitizePath(pWorkItem.Settings.FilePath);
468
+ if (!tmpFilePath)
469
+ {
470
+ return fCallback(null, {
471
+ Outputs: { Content: '', StdOut: 'Invalid file path.' },
472
+ Log: ['ReadFile: invalid or unsafe path rejected.']
473
+ });
474
+ }
475
+
476
+ let tmpFullPath = libPath.join(pContentPath, tmpFilePath);
477
+ let tmpRealContent = libFs.realpathSync(pContentPath);
478
+ if (!tmpFullPath.startsWith(tmpRealContent))
479
+ {
480
+ return fCallback(null, {
481
+ Outputs: { Content: '', StdOut: 'Access denied.' },
482
+ Log: ['ReadFile: path outside content directory.']
483
+ });
484
+ }
485
+
486
+ if (!libFs.existsSync(tmpFullPath))
487
+ {
488
+ return fCallback(null, {
489
+ Outputs: { Content: '', StdOut: 'File not found.' },
490
+ Log: [`ReadFile: ${tmpFilePath} not found.`]
491
+ });
492
+ }
493
+
494
+ let tmpContent = libFs.readFileSync(tmpFullPath, 'utf8');
495
+ pFable.log.info(`Beacon ReadFile: ${tmpFilePath} (${tmpContent.length} bytes)`);
496
+ return fCallback(null, {
497
+ Outputs: { Content: tmpContent, StdOut: `Read ${tmpContent.length} bytes from ${tmpFilePath}` },
498
+ Log: [`ReadFile: read ${tmpContent.length} bytes from ${tmpFilePath}`]
499
+ });
500
+ }
501
+ catch (pError)
502
+ {
503
+ return fCallback(pError);
504
+ }
505
+ }
506
+ },
507
+
508
+ 'SaveFile':
509
+ {
510
+ Description: 'Save content to a file',
511
+ SettingsSchema: [
512
+ { Name: 'FilePath', DataType: 'String', Required: true },
513
+ { Name: 'Content', DataType: 'String', Required: true }
514
+ ],
515
+ Handler: function (pWorkItem, pContext, fCallback)
516
+ {
517
+ try
518
+ {
519
+ let tmpFilePath = sanitizePath(pWorkItem.Settings.FilePath);
520
+ if (!tmpFilePath)
521
+ {
522
+ return fCallback(null, {
523
+ Outputs: { FilePath: '', StdOut: 'Invalid file path.' },
524
+ Log: ['SaveFile: invalid or unsafe path rejected.']
525
+ });
526
+ }
527
+
528
+ let tmpFullPath = libPath.join(pContentPath, tmpFilePath);
529
+ let tmpRealContent = libFs.realpathSync(pContentPath);
530
+ let tmpTargetDir = libPath.dirname(tmpFullPath);
531
+ if (!tmpTargetDir.startsWith(tmpRealContent))
532
+ {
533
+ return fCallback(null, {
534
+ Outputs: { FilePath: '', StdOut: 'Access denied.' },
535
+ Log: ['SaveFile: path outside content directory.']
536
+ });
537
+ }
538
+
539
+ // Ensure directory exists
540
+ if (!libFs.existsSync(tmpTargetDir))
541
+ {
542
+ libFs.mkdirSync(tmpTargetDir, { recursive: true });
543
+ }
544
+
545
+ let tmpContent = pWorkItem.Settings.Content || '';
546
+ libFs.writeFileSync(tmpFullPath, tmpContent, 'utf8');
547
+ pFable.log.info(`Beacon SaveFile: ${tmpFilePath} (${tmpContent.length} bytes)`);
548
+ return fCallback(null, {
549
+ Outputs: { FilePath: tmpFilePath, StdOut: `Saved ${tmpContent.length} bytes to ${tmpFilePath}` },
550
+ Log: [`SaveFile: wrote ${tmpContent.length} bytes to ${tmpFilePath}`]
551
+ });
552
+ }
553
+ catch (pError)
554
+ {
555
+ return fCallback(pError);
556
+ }
557
+ }
558
+ },
559
+
560
+ 'ListFiles':
561
+ {
562
+ Description: 'List files in a content directory',
563
+ SettingsSchema: [
564
+ { Name: 'Path', DataType: 'String', Required: false }
565
+ ],
566
+ Handler: function (pWorkItem, pContext, fCallback)
567
+ {
568
+ try
569
+ {
570
+ let tmpRelPath = pWorkItem.Settings.Path || '';
571
+ let tmpSafePath = tmpRelPath ? sanitizePath(tmpRelPath) : '';
572
+ let tmpTargetPath = tmpSafePath
573
+ ? libPath.join(pContentPath, tmpSafePath)
574
+ : pContentPath;
575
+
576
+ let tmpRealContent = libFs.realpathSync(pContentPath);
577
+ if (!tmpTargetPath.startsWith(tmpRealContent))
578
+ {
579
+ return fCallback(null, {
580
+ Outputs: { Files: '[]', StdOut: 'Access denied.' },
581
+ Log: ['ListFiles: path outside content directory.']
582
+ });
583
+ }
584
+
585
+ if (!libFs.existsSync(tmpTargetPath))
586
+ {
587
+ return fCallback(null, {
588
+ Outputs: { Files: '[]', StdOut: 'Directory not found.' },
589
+ Log: [`ListFiles: ${tmpSafePath || '/'} not found.`]
590
+ });
591
+ }
592
+
593
+ let tmpEntries = libFs.readdirSync(tmpTargetPath, { withFileTypes: true });
594
+ let tmpFiles = tmpEntries.map(function (pEntry)
595
+ {
596
+ return {
597
+ Name: pEntry.name,
598
+ IsDirectory: pEntry.isDirectory()
599
+ };
600
+ });
601
+
602
+ pFable.log.info(`Beacon ListFiles: ${tmpSafePath || '/'} (${tmpFiles.length} entries)`);
603
+ return fCallback(null, {
604
+ Outputs: { Files: JSON.stringify(tmpFiles), StdOut: `Found ${tmpFiles.length} entries in ${tmpSafePath || '/'}` },
605
+ Log: [`ListFiles: ${tmpFiles.length} entries in ${tmpSafePath || '/'}`]
606
+ });
607
+ }
608
+ catch (pError)
609
+ {
610
+ return fCallback(pError);
611
+ }
612
+ }
613
+ },
614
+
615
+ 'CreateFolder':
616
+ {
617
+ Description: 'Create a folder in the content directory',
618
+ SettingsSchema: [
619
+ { Name: 'Path', DataType: 'String', Required: true }
620
+ ],
621
+ Handler: function (pWorkItem, pContext, fCallback)
622
+ {
623
+ try
624
+ {
625
+ let tmpSafePath = sanitizePath(pWorkItem.Settings.Path);
626
+ if (!tmpSafePath)
627
+ {
628
+ return fCallback(null, {
629
+ Outputs: { StdOut: 'Invalid folder path.' },
630
+ Log: ['CreateFolder: invalid or unsafe path rejected.']
631
+ });
632
+ }
633
+
634
+ let tmpFullPath = libPath.join(pContentPath, tmpSafePath);
635
+ let tmpRealContent = libFs.realpathSync(pContentPath);
636
+ let tmpTargetParent = libPath.dirname(tmpFullPath);
637
+ if (libFs.existsSync(tmpTargetParent))
638
+ {
639
+ tmpTargetParent = libFs.realpathSync(tmpTargetParent);
640
+ }
641
+ if (!tmpTargetParent.startsWith(tmpRealContent))
642
+ {
643
+ return fCallback(null, {
644
+ Outputs: { StdOut: 'Access denied.' },
645
+ Log: ['CreateFolder: path outside content directory.']
646
+ });
647
+ }
648
+
649
+ if (libFs.existsSync(tmpFullPath))
650
+ {
651
+ return fCallback(null, {
652
+ Outputs: { StdOut: 'Folder already exists.' },
653
+ Log: [`CreateFolder: ${tmpSafePath} already exists.`]
654
+ });
655
+ }
656
+
657
+ libFs.mkdirSync(tmpFullPath, { recursive: true });
658
+ pFable.log.info(`Beacon CreateFolder: ${tmpSafePath}`);
659
+ return fCallback(null, {
660
+ Outputs: { StdOut: `Created folder ${tmpSafePath}` },
661
+ Log: [`CreateFolder: created ${tmpSafePath}`]
662
+ });
663
+ }
664
+ catch (pError)
665
+ {
666
+ return fCallback(pError);
667
+ }
668
+ }
669
+ }
670
+ }
671
+ });
672
+
673
+ tmpBeacon.enable(function (pError)
674
+ {
675
+ if (pError)
676
+ {
677
+ pFable.log.warn(`Content System Beacon: enable failed: ${pError.message}`);
678
+ }
679
+ else
680
+ {
681
+ pFable.log.info('Content System Beacon: enabled and connected to Ultravisor.');
682
+ }
683
+ });
684
+ }
685
+
415
686
  module.exports = setupContentSystemServer;
@@ -18,6 +18,15 @@ class ContentSystemCommandServe extends libCommandLineCommand
18
18
  this.options.CommandOptions.push(
19
19
  { Name: '-p, --port [port]', Description: 'Port to serve on (defaults to random 7000-7999).', Default: '0' });
20
20
 
21
+ this.options.CommandOptions.push(
22
+ { Name: '-b, --beacon [url]', Description: 'Enable beacon mode and connect to the Ultravisor server at [url] (e.g. http://localhost:54321).' });
23
+
24
+ this.options.CommandOptions.push(
25
+ { Name: '--beacon-name [name]', Description: 'Beacon identity name (defaults to "content-system-1").', Default: 'content-system-1' });
26
+
27
+ this.options.CommandOptions.push(
28
+ { Name: '--beacon-password [password]', Description: 'Beacon authentication password.', Default: '' });
29
+
21
30
  this.addCommand();
22
31
  }
23
32
 
@@ -55,6 +64,17 @@ class ContentSystemCommandServe extends libCommandLineCommand
55
64
  this.log.info(`Created content directory: ${tmpContentPath}`);
56
65
  }
57
66
 
67
+ let tmpBeaconConfig = {};
68
+ if (this.CommandOptions.beacon)
69
+ {
70
+ tmpBeaconConfig = {
71
+ Enabled: true,
72
+ ServerURL: (typeof this.CommandOptions.beacon === 'string') ? this.CommandOptions.beacon : 'http://localhost:54321',
73
+ Name: this.CommandOptions.beaconName || 'content-system-1',
74
+ Password: this.CommandOptions.beaconPassword || ''
75
+ };
76
+ }
77
+
58
78
  let tmpSelf = this;
59
79
  let tmpSetupServer = require('../ContentSystem-Server-Setup.js');
60
80
 
@@ -62,7 +82,8 @@ class ContentSystemCommandServe extends libCommandLineCommand
62
82
  {
63
83
  ContentPath: tmpContentPath,
64
84
  DistPath: tmpDistPath,
65
- Port: tmpPort
85
+ Port: tmpPort,
86
+ Beacon: tmpBeaconConfig
66
87
  },
67
88
  function (pError, pServerInfo)
68
89
  {
@@ -274,11 +274,15 @@ const _ViewConfiguration =
274
274
  onchange="{~P~}.views['ContentEditor-SettingsPanel'].onEditingControlsChanged(this.checked)">
275
275
  </div>
276
276
  <div class="content-editor-settings-row">
277
- <label class="content-editor-settings-checkbox-label"
278
- for="ContentEditor-Setting-AutoPreview">Auto Content Preview</label>
279
- <input type="checkbox" class="content-editor-settings-checkbox"
280
- id="ContentEditor-Setting-AutoPreview"
281
- onchange="{~P~}.views['ContentEditor-SettingsPanel'].onAutoPreviewChanged(this.checked)">
277
+ <span class="content-editor-settings-select-label">Content Preview</span>
278
+ <select class="content-editor-settings-select"
279
+ id="ContentEditor-Setting-ContentPreviewMode"
280
+ onchange="{~P~}.views['ContentEditor-SettingsPanel'].onContentPreviewModeChanged(this.value)">
281
+ <option value="off">Off</option>
282
+ <option value="bottom">Underneath</option>
283
+ <option value="side">Beside</option>
284
+ <option value="tabbed">Tab</option>
285
+ </select>
282
286
  </div>
283
287
  <div class="content-editor-settings-row">
284
288
  <label class="content-editor-settings-checkbox-label"
@@ -387,10 +391,10 @@ class ContentEditorSettingsPanelView extends libPictView
387
391
  tmpControlsCheckbox[0].checked = tmpSettings.MarkdownEditingControls;
388
392
  }
389
393
 
390
- let tmpPreviewCheckbox = this.pict.ContentAssignment.getElement('#ContentEditor-Setting-AutoPreview');
391
- if (tmpPreviewCheckbox && tmpPreviewCheckbox[0])
394
+ let tmpPreviewSelect = this.pict.ContentAssignment.getElement('#ContentEditor-Setting-ContentPreviewMode');
395
+ if (tmpPreviewSelect && tmpPreviewSelect[0])
392
396
  {
393
- tmpPreviewCheckbox[0].checked = tmpSettings.AutoContentPreview;
397
+ tmpPreviewSelect[0].value = tmpSettings.ContentPreviewMode || 'off';
394
398
  }
395
399
 
396
400
  let tmpSegmentCheckbox = this.pict.ContentAssignment.getElement('#ContentEditor-Setting-AutoSegment');
@@ -535,32 +539,15 @@ class ContentEditorSettingsPanelView extends libPictView
535
539
  }
536
540
  }
537
541
 
538
- onAutoPreviewChanged(pChecked)
542
+ onContentPreviewModeChanged(pMode)
539
543
  {
540
- this.pict.AppData.ContentEditor.AutoContentPreview = pChecked;
544
+ this.pict.AppData.ContentEditor.ContentPreviewMode = pMode;
541
545
  this.pict.PictApplication.saveSettings();
542
546
 
543
547
  let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
544
548
  if (tmpEditorView && this.pict.AppData.ContentEditor.ActiveEditor === 'markdown')
545
549
  {
546
- if (pChecked)
547
- {
548
- // Turning ON: clear global hidden class and show all previews
549
- tmpEditorView.togglePreview(true);
550
- for (let tmpIdx in tmpEditorView._segmentEditors)
551
- {
552
- tmpEditorView.toggleSegmentPreview(parseInt(tmpIdx, 10), true);
553
- }
554
- }
555
- else
556
- {
557
- // Turning OFF: hide each segment individually so
558
- // per-segment toggle buttons still work
559
- for (let tmpIdx in tmpEditorView._segmentEditors)
560
- {
561
- tmpEditorView.toggleSegmentPreview(parseInt(tmpIdx, 10), false);
562
- }
563
- }
550
+ tmpEditorView.setPreviewMode(pMode);
564
551
  }
565
552
  }
566
553