retold-content-system 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/build-codejar-bundle.js +29 -0
  4. package/build-codemirror-bundle.js +29 -0
  5. package/codejar-entry.js +10 -0
  6. package/codemirror-entry.js +16 -0
  7. package/content/Dogs.txt.md +2 -0
  8. package/content/README.md +35 -0
  9. package/content/_sidebar.md +3 -0
  10. package/content/_topbar.md +1 -0
  11. package/content/cover.md +12 -0
  12. package/content/getting-started.md +73 -0
  13. package/css/content-system.css +42 -0
  14. package/css/github.css +118 -0
  15. package/docs/.nojekyll +0 -0
  16. package/docs/README.md +24 -0
  17. package/docs/_sidebar.md +16 -0
  18. package/docs/_topbar.md +6 -0
  19. package/docs/cli.md +119 -0
  20. package/docs/cover.md +16 -0
  21. package/docs/css/docuserve.css +73 -0
  22. package/docs/editor-guide.md +137 -0
  23. package/docs/getting-started.md +73 -0
  24. package/docs/index.html +39 -0
  25. package/docs/keyboard-shortcuts.md +40 -0
  26. package/docs/retold-catalog.json +81 -0
  27. package/docs/retold-keyword-index.json +19 -0
  28. package/docs/topics.md +83 -0
  29. package/html/codejar-bundle.js +16 -0
  30. package/html/codemirror-bundle.js +29982 -0
  31. package/html/edit.html +25 -0
  32. package/html/index.html +25 -0
  33. package/html/preview.html +19 -0
  34. package/package.json +70 -0
  35. package/server.js +43 -0
  36. package/source/Pict-Application-ContentEditor-Configuration.json +15 -0
  37. package/source/Pict-Application-ContentEditor.js +1361 -0
  38. package/source/Pict-Application-ContentReader-Configuration.json +15 -0
  39. package/source/Pict-Application-ContentReader.js +91 -0
  40. package/source/Pict-ContentSystem-Bundle.js +21 -0
  41. package/source/cli/ContentSystem-CLI-Program.js +15 -0
  42. package/source/cli/ContentSystem-CLI-Run.js +3 -0
  43. package/source/cli/ContentSystem-Server-Setup.js +405 -0
  44. package/source/cli/commands/ContentSystem-Command-Serve.js +104 -0
  45. package/source/providers/Pict-Provider-ContentEditor.js +198 -0
  46. package/source/views/PictView-Editor-CodeEditor.js +271 -0
  47. package/source/views/PictView-Editor-Layout.js +1194 -0
  48. package/source/views/PictView-Editor-MarkdownEditor.js +115 -0
  49. package/source/views/PictView-Editor-MarkdownReference.js +801 -0
  50. package/source/views/PictView-Editor-SettingsPanel.js +563 -0
  51. package/source/views/PictView-Editor-TopBar.js +366 -0
  52. package/source/views/PictView-Editor-Topics.js +1025 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "Name": "Retold Content Reader",
3
+ "Hash": "ContentReader",
4
+
5
+ "MainViewportViewIdentifier": "Docuserve-Layout",
6
+
7
+ "AutoSolveAfterInitialize": true,
8
+ "AutoRenderMainViewportViewAfterInitialize": false,
9
+ "AutoRenderViewsAfterInitialize": false,
10
+
11
+ "pict_configuration":
12
+ {
13
+ "Product": "ContentReader-Pict-Application"
14
+ }
15
+ }
@@ -0,0 +1,91 @@
1
+ const libDocuserveApplication = require('pict-docuserve');
2
+
3
+ /**
4
+ * Content Reader Application
5
+ *
6
+ * Extends pict-docuserve to serve standalone markdown content.
7
+ * Overrides DocsBaseURL to point at the /content/ static route
8
+ * so all markdown is fetched from the server's content directory.
9
+ */
10
+ class ContentReaderApplication extends libDocuserveApplication
11
+ {
12
+ constructor(pFable, pOptions, pServiceHash)
13
+ {
14
+ super(pFable, pOptions, pServiceHash);
15
+ }
16
+
17
+ onAfterInitializeAsync(fCallback)
18
+ {
19
+ // Point docuserve at the /content/ path served by Orator
20
+ this.pict.AppData.Docuserve =
21
+ {
22
+ CatalogLoaded: false,
23
+ Catalog: null,
24
+ CoverLoaded: false,
25
+ Cover: null,
26
+ SidebarLoaded: false,
27
+ SidebarGroups: [],
28
+ TopBarLoaded: false,
29
+ TopBar: null,
30
+ ErrorPageLoaded: false,
31
+ ErrorPageHTML: null,
32
+ KeywordIndexLoaded: false,
33
+ KeywordDocumentCount: 0,
34
+ CurrentGroup: '',
35
+ CurrentModule: '',
36
+ CurrentPath: '',
37
+ SidebarVisible: true,
38
+ // Serve content from the /content/ static route
39
+ DocsBaseURL: '/content/',
40
+ // No catalog in standalone mode
41
+ CatalogURL: '/content/retold-catalog.json'
42
+ };
43
+
44
+ // Load the catalog (will gracefully fall back to standalone mode)
45
+ let tmpDocProvider = this.pict.providers['Docuserve-Documentation'];
46
+ tmpDocProvider.loadCatalog(() =>
47
+ {
48
+ // Set page title from cover or topbar
49
+ let tmpDocuserve = this.pict.AppData.Docuserve;
50
+ if (tmpDocuserve.CoverLoaded && tmpDocuserve.Cover && tmpDocuserve.Cover.Title)
51
+ {
52
+ document.title = tmpDocuserve.Cover.Title.replace(/<[^>]*>/g, '');
53
+ }
54
+ else if (tmpDocuserve.TopBarLoaded && tmpDocuserve.TopBar && tmpDocuserve.TopBar.Brand)
55
+ {
56
+ document.title = tmpDocuserve.TopBar.Brand.replace(/<[^>]*>/g, '');
57
+ }
58
+
59
+ // Inject an "Edit" link into the topbar (shows in ExternalLinks area, top-right)
60
+ if (tmpDocuserve.TopBar)
61
+ {
62
+ if (!Array.isArray(tmpDocuserve.TopBar.ExternalLinks))
63
+ {
64
+ tmpDocuserve.TopBar.ExternalLinks = [];
65
+ }
66
+ tmpDocuserve.TopBar.ExternalLinks.push({ Text: 'Edit', Href: '/' });
67
+ }
68
+ else
69
+ {
70
+ tmpDocuserve.TopBarLoaded = true;
71
+ tmpDocuserve.TopBar =
72
+ {
73
+ Brand: 'Content System',
74
+ NavLinks: [],
75
+ ExternalLinks: [{ Text: 'Edit', Href: '/' }]
76
+ };
77
+ }
78
+
79
+ // Render the layout shell
80
+ this.pict.views['Docuserve-Layout'].render();
81
+
82
+ // Call the base PictApplication callback (skip docuserve's onAfterInitializeAsync
83
+ // since we already did the catalog loading ourselves)
84
+ return fCallback();
85
+ });
86
+ }
87
+ }
88
+
89
+ module.exports = ContentReaderApplication;
90
+
91
+ module.exports.default_configuration = require('./Pict-Application-ContentReader-Configuration.json');
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Combined browser bundle for Retold Content System.
3
+ *
4
+ * Exports both the reader and editor applications as window globals
5
+ * so each HTML page can bootstrap the appropriate one.
6
+ *
7
+ * In index.html: Pict.safeLoadPictApplication(PictContentReader, 2)
8
+ * In edit.html: Pict.safeLoadPictApplication(PictContentEditor, 2)
9
+ */
10
+ module.exports =
11
+ {
12
+ PictContentReader: require('./Pict-Application-ContentReader.js'),
13
+ PictContentEditor: require('./Pict-Application-ContentEditor.js')
14
+ };
15
+
16
+ // Also expose on window for direct access
17
+ if (typeof (window) !== 'undefined')
18
+ {
19
+ window.PictContentReader = module.exports.PictContentReader;
20
+ window.PictContentEditor = module.exports.PictContentEditor;
21
+ }
@@ -0,0 +1,15 @@
1
+ const libCLIProgram = require('pict-service-commandlineutility');
2
+
3
+ let _PictCLIProgram = new libCLIProgram(
4
+ {
5
+ Product: 'retold-content-system',
6
+ Version: require('../../package.json').version,
7
+
8
+ Command: 'retold-content-system',
9
+ Description: 'Serve and edit markdown content folders with reader and editor apps powered by Pict.'
10
+ },
11
+ [
12
+ require('./commands/ContentSystem-Command-Serve.js')
13
+ ]);
14
+
15
+ module.exports = _PictCLIProgram;
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ const libContentSystemProgram = require('./ContentSystem-CLI-Program.js');
3
+ libContentSystemProgram.run();
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Retold Content System -- Shared Orator Server Setup
3
+ *
4
+ * This module encapsulates all server initialization logic so it can be
5
+ * used by both the standalone server.js entry point and the CLI serve command.
6
+ *
7
+ * @param {object} pOptions
8
+ * @param {string} pOptions.ContentPath - Absolute path to the markdown content folder
9
+ * @param {string} pOptions.UploadPath - Absolute path to the uploads folder
10
+ * @param {string} pOptions.DistPath - Absolute path to the built dist folder
11
+ * @param {number} pOptions.Port - HTTP port to listen on
12
+ * @param {Function} fCallback - Callback(pError, { Fable, Orator, Port })
13
+ */
14
+
15
+ const libFs = require('fs');
16
+ const libPath = require('path');
17
+
18
+ const libFable = require('fable');
19
+ const libOrator = require('orator');
20
+ const libOratorServiceServerRestify = require('orator-serviceserver-restify');
21
+ const libFileBrowserService = require('pict-section-filebrowser').FileBrowserService;
22
+
23
+ /**
24
+ * Sanitize a file path -- prevent directory traversal attacks.
25
+ *
26
+ * @param {string} pPath - The raw path from the client
27
+ * @returns {string|null} A safe relative path, or null if invalid
28
+ */
29
+ function sanitizePath(pPath)
30
+ {
31
+ if (!pPath || typeof (pPath) !== 'string')
32
+ {
33
+ return null;
34
+ }
35
+
36
+ // Decode URI components
37
+ let tmpPath = decodeURIComponent(pPath);
38
+
39
+ // Remove leading slashes
40
+ tmpPath = tmpPath.replace(/^\/+/, '');
41
+
42
+ // Block directory traversal
43
+ if (tmpPath.includes('..'))
44
+ {
45
+ return null;
46
+ }
47
+
48
+ // Block absolute paths
49
+ if (libPath.isAbsolute(tmpPath))
50
+ {
51
+ return null;
52
+ }
53
+
54
+ // Remove dangerous characters
55
+ tmpPath = tmpPath.replace(/[<>"|?*]/g, '_');
56
+
57
+ return tmpPath || null;
58
+ }
59
+
60
+ /**
61
+ * Sanitize a filename -- strip path separators and dangerous characters.
62
+ *
63
+ * @param {string} pName - The raw filename from the client
64
+ * @returns {string} A safe filename
65
+ */
66
+ function sanitizeFilename(pName)
67
+ {
68
+ if (!pName || typeof (pName) !== 'string')
69
+ {
70
+ return 'upload';
71
+ }
72
+ let tmpName = libPath.basename(pName);
73
+ tmpName = tmpName.replace(/[\/\\:*?"<>|]/g, '_');
74
+ if (tmpName.length > 200)
75
+ {
76
+ tmpName = tmpName.substring(0, 200);
77
+ }
78
+ return tmpName || 'upload';
79
+ }
80
+
81
+ /**
82
+ * Set up and start the Retold Content System Orator server.
83
+ */
84
+ function setupContentSystemServer(pOptions, fCallback)
85
+ {
86
+ let tmpContentPath = pOptions.ContentPath;
87
+ let tmpUploadPath = pOptions.UploadPath;
88
+ let tmpDistFolder = pOptions.DistPath;
89
+ let tmpPort = pOptions.Port;
90
+
91
+ let tmpSettings =
92
+ {
93
+ Product: 'Retold-Content-System',
94
+ ProductVersion: require('../../package.json').version,
95
+ APIServerPort: tmpPort,
96
+ ContentPath: tmpContentPath,
97
+ UploadPath: tmpUploadPath
98
+ };
99
+
100
+ let tmpFable = new libFable(tmpSettings);
101
+
102
+ // Ensure the uploads directory exists
103
+ if (!libFs.existsSync(tmpUploadPath))
104
+ {
105
+ libFs.mkdirSync(tmpUploadPath, { recursive: true });
106
+ }
107
+
108
+ // Ensure the content directory exists
109
+ if (!libFs.existsSync(tmpContentPath))
110
+ {
111
+ libFs.mkdirSync(tmpContentPath, { recursive: true });
112
+ }
113
+
114
+ tmpFable.serviceManager.addServiceType('OratorServiceServer', libOratorServiceServerRestify);
115
+ tmpFable.serviceManager.instantiateServiceProvider('OratorServiceServer');
116
+ tmpFable.serviceManager.addServiceType('Orator', libOrator);
117
+ let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator');
118
+
119
+ // Set up the FileBrowserService for the content directory
120
+ let tmpFileBrowser = new libFileBrowserService(tmpFable,
121
+ {
122
+ BasePath: tmpContentPath,
123
+ APIRoutePrefix: '/api/filebrowser',
124
+ ServeWebApp: false,
125
+ IncludeHiddenFiles: false
126
+ });
127
+
128
+ tmpOrator.initialize(
129
+ function ()
130
+ {
131
+ let tmpServiceServer = tmpOrator.serviceServer;
132
+
133
+ // Enable body parsing for POST/PUT requests
134
+ tmpServiceServer.server.use(tmpServiceServer.bodyParser());
135
+
136
+ // Connect the file browser API routes
137
+ tmpFileBrowser.connectRoutes();
138
+
139
+ // --- PUT /api/filebrowser/settings ---
140
+ // Toggle file browser options at runtime (e.g. hidden files)
141
+ tmpServiceServer.put('/api/filebrowser/settings',
142
+ (pRequest, pResponse, fNext) =>
143
+ {
144
+ try
145
+ {
146
+ if (pRequest.body && typeof (pRequest.body.IncludeHiddenFiles) === 'boolean')
147
+ {
148
+ tmpFileBrowser.options.IncludeHiddenFiles = pRequest.body.IncludeHiddenFiles;
149
+ }
150
+ pResponse.send({ Success: true });
151
+ }
152
+ catch (pError)
153
+ {
154
+ pResponse.send(500, { Error: pError.message });
155
+ }
156
+ return fNext();
157
+ });
158
+
159
+ // --- GET /api/content/read/* ---
160
+ // Read the raw markdown content of a file
161
+ tmpServiceServer.get('/api/content/read/*',
162
+ (pRequest, pResponse, fNext) =>
163
+ {
164
+ try
165
+ {
166
+ let tmpFilePath = sanitizePath(pRequest.params['*']);
167
+
168
+ if (!tmpFilePath)
169
+ {
170
+ pResponse.send(400, { Success: false, Error: 'Invalid file path.' });
171
+ return fNext();
172
+ }
173
+
174
+ let tmpFullPath = libPath.join(tmpContentPath, tmpFilePath);
175
+
176
+ // Ensure the resolved path is within the content directory
177
+ let tmpRealContent = libFs.realpathSync(tmpContentPath);
178
+ if (!tmpFullPath.startsWith(tmpRealContent))
179
+ {
180
+ pResponse.send(403, { Success: false, Error: 'Access denied.' });
181
+ return fNext();
182
+ }
183
+
184
+ if (!libFs.existsSync(tmpFullPath))
185
+ {
186
+ pResponse.send(404, { Success: false, Error: 'File not found.' });
187
+ return fNext();
188
+ }
189
+
190
+ let tmpContent = libFs.readFileSync(tmpFullPath, 'utf8');
191
+ pResponse.send({ Success: true, Path: tmpFilePath, Content: tmpContent });
192
+ }
193
+ catch (pError)
194
+ {
195
+ pResponse.send(500, { Success: false, Error: pError.message });
196
+ }
197
+ return fNext();
198
+ });
199
+
200
+ // --- PUT /api/content/save/* ---
201
+ // Save markdown content to a file (create or overwrite)
202
+ tmpServiceServer.put('/api/content/save/*',
203
+ (pRequest, pResponse, fNext) =>
204
+ {
205
+ try
206
+ {
207
+ let tmpFilePath = sanitizePath(pRequest.params['*']);
208
+
209
+ if (!tmpFilePath)
210
+ {
211
+ pResponse.send(400, { Success: false, Error: 'Invalid file path.' });
212
+ return fNext();
213
+ }
214
+
215
+ let tmpFullPath = libPath.join(tmpContentPath, tmpFilePath);
216
+
217
+ // Ensure the resolved path target is within the content directory
218
+ let tmpRealContent = libFs.realpathSync(tmpContentPath);
219
+ let tmpTargetDir = libPath.dirname(tmpFullPath);
220
+ // The file may not exist yet, but the directory should be within content
221
+ if (!tmpTargetDir.startsWith(tmpRealContent))
222
+ {
223
+ pResponse.send(403, { Success: false, Error: 'Access denied.' });
224
+ return fNext();
225
+ }
226
+
227
+ let tmpContent = '';
228
+ if (pRequest.body && typeof (pRequest.body) === 'object' && pRequest.body.Content !== undefined)
229
+ {
230
+ tmpContent = pRequest.body.Content;
231
+ }
232
+ else if (typeof (pRequest.body) === 'string')
233
+ {
234
+ tmpContent = pRequest.body;
235
+ }
236
+
237
+ // Ensure the target directory exists
238
+ let tmpDir = libPath.dirname(tmpFullPath);
239
+ if (!libFs.existsSync(tmpDir))
240
+ {
241
+ libFs.mkdirSync(tmpDir, { recursive: true });
242
+ }
243
+
244
+ libFs.writeFileSync(tmpFullPath, tmpContent, 'utf8');
245
+
246
+ tmpFable.log.info(`Content saved: ${tmpFilePath} (${tmpContent.length} bytes)`);
247
+ pResponse.send({ Success: true, Path: tmpFilePath, Size: tmpContent.length });
248
+ }
249
+ catch (pError)
250
+ {
251
+ tmpFable.log.error(`Content save failed: ${pError.message}`);
252
+ pResponse.send(500, { Success: false, Error: pError.message });
253
+ }
254
+ return fNext();
255
+ });
256
+
257
+ // --- POST /api/content/upload-image ---
258
+ // Upload an image file into the content folder the user is browsing.
259
+ // The client sends the target folder via the x-upload-path header.
260
+ tmpServiceServer.post('/api/content/upload-image',
261
+ (pRequest, pResponse, fNext) =>
262
+ {
263
+ try
264
+ {
265
+ let tmpBody = pRequest.body;
266
+
267
+ if (!tmpBody)
268
+ {
269
+ pResponse.send(400, { Success: false, Error: 'No image data received.' });
270
+ return fNext();
271
+ }
272
+
273
+ let tmpOriginalName = sanitizeFilename(pRequest.headers['x-filename']);
274
+ let tmpContentType = pRequest.headers['content-type'] || 'application/octet-stream';
275
+
276
+ // Determine file extension from content-type if needed
277
+ let tmpExt = libPath.extname(tmpOriginalName);
278
+ if (!tmpExt)
279
+ {
280
+ let tmpMimeMap =
281
+ {
282
+ 'image/png': '.png',
283
+ 'image/jpeg': '.jpg',
284
+ 'image/gif': '.gif',
285
+ 'image/webp': '.webp',
286
+ 'image/svg+xml': '.svg',
287
+ 'image/bmp': '.bmp'
288
+ };
289
+ tmpExt = tmpMimeMap[tmpContentType] || '.bin';
290
+ tmpOriginalName += tmpExt;
291
+ }
292
+
293
+ // Determine the target folder: use the x-upload-path header
294
+ // (the folder the user is currently browsing), falling back
295
+ // to the content root if none is provided.
296
+ let tmpUploadFolder = tmpContentPath;
297
+ let tmpRelativeFolder = '';
298
+ let tmpRawUploadPath = pRequest.headers['x-upload-path'];
299
+ if (tmpRawUploadPath)
300
+ {
301
+ let tmpSafePath = sanitizePath(tmpRawUploadPath);
302
+ if (tmpSafePath)
303
+ {
304
+ tmpRelativeFolder = tmpSafePath;
305
+ tmpUploadFolder = libPath.join(tmpContentPath, tmpSafePath);
306
+
307
+ // Ensure the resolved path is within the content directory
308
+ let tmpRealContent = libFs.realpathSync(tmpContentPath);
309
+ if (!libPath.resolve(tmpUploadFolder).startsWith(tmpRealContent))
310
+ {
311
+ pResponse.send(403, { Success: false, Error: 'Access denied.' });
312
+ return fNext();
313
+ }
314
+ }
315
+ }
316
+
317
+ // Ensure the target directory exists
318
+ if (!libFs.existsSync(tmpUploadFolder))
319
+ {
320
+ libFs.mkdirSync(tmpUploadFolder, { recursive: true });
321
+ }
322
+
323
+ let tmpUniqueFilename = `${Date.now()}-${tmpOriginalName}`;
324
+ let tmpFilePath = libPath.join(tmpUploadFolder, tmpUniqueFilename);
325
+
326
+ let tmpBuffer = Buffer.isBuffer(tmpBody) ? tmpBody : Buffer.from(tmpBody);
327
+ libFs.writeFileSync(tmpFilePath, tmpBuffer);
328
+
329
+ // Build the URL: serve through the /content/ static route
330
+ let tmpRelativePath = tmpRelativeFolder
331
+ ? (tmpRelativeFolder + '/' + tmpUniqueFilename)
332
+ : tmpUniqueFilename;
333
+ let tmpURL = `/content/${tmpRelativePath}`;
334
+ tmpFable.log.info(`Image uploaded: ${tmpURL} (${tmpBuffer.length} bytes)`);
335
+
336
+ pResponse.send(
337
+ {
338
+ Success: true,
339
+ URL: tmpURL,
340
+ RelativePath: tmpRelativePath,
341
+ Filename: tmpUniqueFilename,
342
+ Size: tmpBuffer.length
343
+ });
344
+ }
345
+ catch (pError)
346
+ {
347
+ tmpFable.log.error(`Image upload failed: ${pError.message}`);
348
+ pResponse.send(500, { Success: false, Error: pError.message });
349
+ }
350
+ return fNext();
351
+ });
352
+
353
+ // --- GET /api/content/uploads ---
354
+ // List uploaded images
355
+ tmpServiceServer.get('/api/content/uploads',
356
+ (pRequest, pResponse, fNext) =>
357
+ {
358
+ try
359
+ {
360
+ let tmpFiles = libFs.readdirSync(tmpUploadPath);
361
+ let tmpFileList = tmpFiles.map(
362
+ (pFilename) =>
363
+ {
364
+ let tmpStat = libFs.statSync(libPath.join(tmpUploadPath, pFilename));
365
+ return (
366
+ {
367
+ Filename: pFilename,
368
+ URL: `/uploads/${pFilename}`,
369
+ Size: tmpStat.size,
370
+ Modified: tmpStat.mtime
371
+ });
372
+ });
373
+ pResponse.send({ Success: true, Files: tmpFileList });
374
+ }
375
+ catch (pError)
376
+ {
377
+ pResponse.send(500, { Success: false, Error: pError.message });
378
+ }
379
+ return fNext();
380
+ });
381
+
382
+ // Serve uploaded images at /uploads/
383
+ tmpOrator.addStaticRoute(`${tmpUploadPath}/`, 'index.html', '/uploads/*', '/uploads/');
384
+
385
+ // Serve content markdown files at /content/ (for the reader)
386
+ tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
387
+
388
+ // Serve the built application from dist/ (main static route)
389
+ tmpOrator.addStaticRoute(`${tmpDistFolder}/`, 'index.html');
390
+
391
+ // Start the server
392
+ tmpOrator.startService(
393
+ function ()
394
+ {
395
+ return fCallback(null,
396
+ {
397
+ Fable: tmpFable,
398
+ Orator: tmpOrator,
399
+ Port: tmpPort
400
+ });
401
+ });
402
+ });
403
+ }
404
+
405
+ module.exports = setupContentSystemServer;
@@ -0,0 +1,104 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+
3
+ const libFs = require('fs');
4
+ const libPath = require('path');
5
+
6
+ class ContentSystemCommandServe extends libCommandLineCommand
7
+ {
8
+ constructor(pFable, pManifest, pServiceHash)
9
+ {
10
+ super(pFable, pManifest, pServiceHash);
11
+
12
+ this.options.CommandKeyword = 'serve';
13
+ this.options.Description = 'Start the content system server for a content folder.';
14
+
15
+ this.options.CommandArguments.push(
16
+ { Name: '[content-path]', Description: 'Path to the markdown content folder (defaults to current directory).' });
17
+
18
+ this.options.CommandOptions.push(
19
+ { Name: '-p, --port [port]', Description: 'Port to serve on (defaults to random 7000-7999).', Default: '0' });
20
+
21
+ this.addCommand();
22
+ }
23
+
24
+ onRunAsync(fCallback)
25
+ {
26
+ let tmpContentPath = libPath.resolve(this.ArgumentString || process.cwd());
27
+
28
+ // If no explicit content-path was given and the resolved directory has a
29
+ // content/ subfolder, use that instead. This way running the CLI from
30
+ // a project root that has a content/ folder does the right thing.
31
+ if (!this.ArgumentString)
32
+ {
33
+ let tmpContentSubfolder = libPath.join(tmpContentPath, 'content');
34
+ if (libFs.existsSync(tmpContentSubfolder) && libFs.statSync(tmpContentSubfolder).isDirectory())
35
+ {
36
+ tmpContentPath = tmpContentSubfolder;
37
+ }
38
+ }
39
+
40
+ let tmpDistPath = libPath.resolve(__dirname, '..', '..', '..', 'dist');
41
+ let tmpUploadPath = libPath.join(tmpContentPath, 'uploads');
42
+ let tmpPortOption = parseInt(this.CommandOptions.port, 10);
43
+ let tmpPort = (tmpPortOption > 0) ? tmpPortOption : (7000 + Math.floor(Math.random() * 1000));
44
+
45
+ // Validate dist path (packaged with the module)
46
+ if (!libFs.existsSync(tmpDistPath))
47
+ {
48
+ this.log.error(`Built assets not found at ${tmpDistPath}. Run 'npm run build-all' in the retold-content-system package first.`);
49
+ return fCallback(new Error('dist folder not found'));
50
+ }
51
+
52
+ // Ensure content directory exists
53
+ if (!libFs.existsSync(tmpContentPath))
54
+ {
55
+ libFs.mkdirSync(tmpContentPath, { recursive: true });
56
+ this.log.info(`Created content directory: ${tmpContentPath}`);
57
+ }
58
+
59
+ // Ensure uploads directory exists
60
+ if (!libFs.existsSync(tmpUploadPath))
61
+ {
62
+ libFs.mkdirSync(tmpUploadPath, { recursive: true });
63
+ }
64
+
65
+ let tmpSelf = this;
66
+ let tmpSetupServer = require('../ContentSystem-Server-Setup.js');
67
+
68
+ tmpSetupServer(
69
+ {
70
+ ContentPath: tmpContentPath,
71
+ UploadPath: tmpUploadPath,
72
+ DistPath: tmpDistPath,
73
+ Port: tmpPort
74
+ },
75
+ function (pError, pServerInfo)
76
+ {
77
+ if (pError)
78
+ {
79
+ tmpSelf.log.error(`Failed to start server: ${pError.message}`);
80
+ return fCallback(pError);
81
+ }
82
+
83
+ tmpSelf.log.info('');
84
+ tmpSelf.log.info('==========================================================');
85
+ tmpSelf.log.info(` Retold Content System running on http://localhost:${pServerInfo.Port}`);
86
+ tmpSelf.log.info('==========================================================');
87
+ tmpSelf.log.info(` Content: ${tmpContentPath}`);
88
+ tmpSelf.log.info(` Uploads: ${tmpUploadPath}`);
89
+ tmpSelf.log.info(` Assets: ${tmpDistPath}`);
90
+ tmpSelf.log.info(` Reader: http://localhost:${pServerInfo.Port}/`);
91
+ tmpSelf.log.info(` Editor: http://localhost:${pServerInfo.Port}/edit.html`);
92
+ tmpSelf.log.info('==========================================================');
93
+ tmpSelf.log.info('');
94
+ tmpSelf.log.info(' Press Ctrl+C to stop.');
95
+ tmpSelf.log.info('');
96
+
97
+ // Intentionally do NOT call fCallback() here.
98
+ // The server should keep running -- the Orator listener
99
+ // keeps the event loop alive.
100
+ });
101
+ }
102
+ }
103
+
104
+ module.exports = ContentSystemCommandServe;