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,198 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ /**
4
+ * Content Editor Provider
5
+ *
6
+ * Handles communication with the server's REST API for content
7
+ * management: reading/saving markdown and uploading images.
8
+ * File listing is handled by pict-section-filebrowser via the FileBrowserService API.
9
+ */
10
+ class ContentEditorProvider extends libPictProvider
11
+ {
12
+ constructor(pFable, pOptions, pServiceHash)
13
+ {
14
+ super(pFable, pOptions, pServiceHash);
15
+ }
16
+
17
+ /**
18
+ * Load the raw markdown content of a file.
19
+ *
20
+ * @param {string} pFilePath - The relative file path
21
+ * @param {Function} fCallback - Callback receiving (error, contentString)
22
+ */
23
+ loadFile(pFilePath, fCallback)
24
+ {
25
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
26
+
27
+ if (!pFilePath)
28
+ {
29
+ return tmpCallback('No file path specified', '');
30
+ }
31
+
32
+ fetch('/api/content/read/' + encodeURIComponent(pFilePath))
33
+ .then((pResponse) =>
34
+ {
35
+ if (!pResponse.ok)
36
+ {
37
+ return tmpCallback('File not found: ' + pResponse.status, '');
38
+ }
39
+ return pResponse.json();
40
+ })
41
+ .then((pData) =>
42
+ {
43
+ if (pData && pData.Success)
44
+ {
45
+ return tmpCallback(null, pData.Content || '');
46
+ }
47
+ return tmpCallback(pData ? pData.Error : 'Unknown error', '');
48
+ })
49
+ .catch((pError) =>
50
+ {
51
+ this.log.warn(`ContentEditor: Error loading file [${pFilePath}]: ${pError}`);
52
+ return tmpCallback(pError.message, '');
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Save markdown content to a file.
58
+ *
59
+ * @param {string} pFilePath - The relative file path
60
+ * @param {string} pContent - The markdown content to save
61
+ * @param {Function} fCallback - Callback receiving (error)
62
+ */
63
+ saveFile(pFilePath, pContent, fCallback)
64
+ {
65
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
66
+
67
+ if (!pFilePath)
68
+ {
69
+ return tmpCallback('No file path specified');
70
+ }
71
+
72
+ fetch('/api/content/save/' + encodeURIComponent(pFilePath),
73
+ {
74
+ method: 'PUT',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ Content: pContent })
77
+ })
78
+ .then((pResponse) =>
79
+ {
80
+ if (!pResponse.ok)
81
+ {
82
+ return tmpCallback('Save failed: ' + pResponse.status);
83
+ }
84
+ return pResponse.json();
85
+ })
86
+ .then((pData) =>
87
+ {
88
+ if (pData && pData.Success)
89
+ {
90
+ return tmpCallback(null);
91
+ }
92
+ return tmpCallback(pData ? pData.Error : 'Unknown error');
93
+ })
94
+ .catch((pError) =>
95
+ {
96
+ this.log.warn(`ContentEditor: Error saving file [${pFilePath}]: ${pError}`);
97
+ return tmpCallback(pError.message);
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Upload an image file to the server.
103
+ *
104
+ * The image is saved into the content folder the user is currently
105
+ * browsing, determined from the current file path or browse location.
106
+ *
107
+ * @param {File} pFile - The image file to upload
108
+ * @param {Function} fCallback - Callback receiving (error, url)
109
+ */
110
+ uploadImage(pFile, fCallback)
111
+ {
112
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
113
+
114
+ // Determine the target folder from the currently open file
115
+ let tmpUploadPath = '';
116
+ let tmpCurrentFile = this.pict.AppData.ContentEditor.CurrentFile;
117
+ if (tmpCurrentFile)
118
+ {
119
+ let tmpLastSlash = tmpCurrentFile.lastIndexOf('/');
120
+ if (tmpLastSlash > 0)
121
+ {
122
+ tmpUploadPath = tmpCurrentFile.substring(0, tmpLastSlash);
123
+ }
124
+ }
125
+ else if (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation)
126
+ {
127
+ tmpUploadPath = this.pict.AppData.PictFileBrowser.CurrentLocation;
128
+ }
129
+
130
+ let tmpHeaders =
131
+ {
132
+ 'Content-Type': pFile.type,
133
+ 'x-filename': pFile.name
134
+ };
135
+
136
+ if (tmpUploadPath)
137
+ {
138
+ tmpHeaders['x-upload-path'] = tmpUploadPath;
139
+ }
140
+
141
+ fetch('/api/content/upload-image',
142
+ {
143
+ method: 'POST',
144
+ body: pFile,
145
+ headers: tmpHeaders
146
+ })
147
+ .then((pResponse) => pResponse.json())
148
+ .then((pData) =>
149
+ {
150
+ if (pData && pData.Success && pData.URL)
151
+ {
152
+ return tmpCallback(null, pData.URL);
153
+ }
154
+ return tmpCallback(pData ? pData.Error : 'Upload failed');
155
+ })
156
+ .catch((pError) =>
157
+ {
158
+ this.log.warn(`ContentEditor: Image upload failed: ${pError}`);
159
+ return tmpCallback(pError.message);
160
+ });
161
+ }
162
+
163
+ /**
164
+ * List uploaded images.
165
+ *
166
+ * @param {Function} fCallback - Callback receiving (error, filesArray)
167
+ */
168
+ listUploads(fCallback)
169
+ {
170
+ let tmpCallback = (typeof (fCallback) === 'function') ? fCallback : () => {};
171
+
172
+ fetch('/api/content/uploads')
173
+ .then((pResponse) => pResponse.json())
174
+ .then((pData) =>
175
+ {
176
+ if (pData && pData.Success)
177
+ {
178
+ return tmpCallback(null, pData.Files || []);
179
+ }
180
+ return tmpCallback(pData ? pData.Error : 'Unknown error', []);
181
+ })
182
+ .catch((pError) =>
183
+ {
184
+ this.log.warn(`ContentEditor: Error listing uploads: ${pError}`);
185
+ return tmpCallback(pError.message, []);
186
+ });
187
+ }
188
+ }
189
+
190
+ module.exports = ContentEditorProvider;
191
+
192
+ module.exports.default_configuration =
193
+ {
194
+ ProviderIdentifier: "ContentEditor-Provider",
195
+
196
+ AutoInitialize: true,
197
+ AutoInitializeOrdinal: 0
198
+ };
@@ -0,0 +1,271 @@
1
+ const libPictSectionCode = require('pict-section-code');
2
+
3
+ /**
4
+ * Map of file extensions to highlight.js language identifiers.
5
+ *
6
+ * highlight.js supports 190+ languages. This map covers the most common
7
+ * file extensions. For unlisted extensions, highlight.js auto-detection
8
+ * is used as a fallback.
9
+ */
10
+ const _ExtensionLanguageMap =
11
+ {
12
+ 'js': 'javascript',
13
+ 'mjs': 'javascript',
14
+ 'cjs': 'javascript',
15
+ 'jsx': 'javascript',
16
+ 'ts': 'typescript',
17
+ 'tsx': 'typescript',
18
+ 'json': 'json',
19
+ 'html': 'xml',
20
+ 'htm': 'xml',
21
+ 'xml': 'xml',
22
+ 'svg': 'xml',
23
+ 'css': 'css',
24
+ 'scss': 'scss',
25
+ 'sass': 'scss',
26
+ 'less': 'less',
27
+ 'sql': 'sql',
28
+ 'py': 'python',
29
+ 'rb': 'ruby',
30
+ 'java': 'java',
31
+ 'kt': 'kotlin',
32
+ 'kts': 'kotlin',
33
+ 'go': 'go',
34
+ 'rs': 'rust',
35
+ 'c': 'c',
36
+ 'h': 'c',
37
+ 'cpp': 'cpp',
38
+ 'cc': 'cpp',
39
+ 'hpp': 'cpp',
40
+ 'cs': 'csharp',
41
+ 'swift': 'swift',
42
+ 'php': 'php',
43
+ 'sh': 'bash',
44
+ 'bash': 'bash',
45
+ 'zsh': 'bash',
46
+ 'fish': 'bash',
47
+ 'yml': 'yaml',
48
+ 'yaml': 'yaml',
49
+ 'toml': 'ini',
50
+ 'ini': 'ini',
51
+ 'cfg': 'ini',
52
+ 'conf': 'ini',
53
+ 'dockerfile': 'dockerfile',
54
+ 'docker': 'dockerfile',
55
+ 'makefile': 'makefile',
56
+ 'mk': 'makefile',
57
+ 'md': 'markdown',
58
+ 'markdown': 'markdown',
59
+ 'lua': 'lua',
60
+ 'r': 'r',
61
+ 'pl': 'perl',
62
+ 'pm': 'perl',
63
+ 'ex': 'elixir',
64
+ 'exs': 'elixir',
65
+ 'erl': 'erlang',
66
+ 'hrl': 'erlang',
67
+ 'hs': 'haskell',
68
+ 'clj': 'clojure',
69
+ 'scala': 'scala',
70
+ 'dart': 'dart',
71
+ 'groovy': 'groovy',
72
+ 'gradle': 'groovy',
73
+ 'tf': 'terraform',
74
+ 'vim': 'vim',
75
+ 'diff': 'diff',
76
+ 'patch': 'diff',
77
+ 'log': 'accesslog',
78
+ 'txt': 'plaintext'
79
+ };
80
+
81
+ const _ViewConfiguration =
82
+ {
83
+ ViewIdentifier: "ContentEditor-CodeEditor",
84
+
85
+ DefaultRenderable: "CodeEditor-Wrap",
86
+ DefaultDestinationAddress: "#ContentEditor-Editor-Container",
87
+
88
+ TargetElementAddress: "#ContentEditor-Editor-Container",
89
+ CodeDataAddress: "AppData.ContentEditor.CodeContent",
90
+
91
+ ReadOnly: false,
92
+ LineNumbers: true,
93
+ Language: "javascript",
94
+ Tab: "\t",
95
+
96
+ AutoRender: false,
97
+
98
+ Renderables:
99
+ [
100
+ {
101
+ RenderableHash: "CodeEditor-Wrap",
102
+ TemplateHash: "CodeEditor-Container",
103
+ DestinationAddress: "#ContentEditor-Editor-Container"
104
+ }
105
+ ]
106
+ };
107
+
108
+ /**
109
+ * Content Editor Code Editor View
110
+ *
111
+ * Extends pict-section-code to integrate with the content system's
112
+ * server-side save and auto-dirty detection. Uses highlight.js for
113
+ * syntax highlighting across 190+ languages.
114
+ */
115
+ class ContentEditorCodeEditorView extends libPictSectionCode
116
+ {
117
+ constructor(pFable, pOptions, pServiceHash)
118
+ {
119
+ super(pFable, pOptions, pServiceHash);
120
+
121
+ // When true, the next onCodeChange call will be suppressed
122
+ // (used to prevent the initial marshalToView from marking dirty)
123
+ this._suppressNextDirty = false;
124
+ }
125
+
126
+ /**
127
+ * Override the initial render to connect CodeJar and highlight.js
128
+ * before the editor is created.
129
+ */
130
+ onAfterInitialRender()
131
+ {
132
+ // Connect CodeJar from the global bundle
133
+ if (typeof (window) !== 'undefined' && window.CodeJarModules && window.CodeJarModules.CodeJar)
134
+ {
135
+ this.connectCodeJarPrototype(window.CodeJarModules.CodeJar);
136
+ }
137
+
138
+ // Set up highlight.js-based highlighting
139
+ if (typeof (window) !== 'undefined' && window.CodeJarModules && window.CodeJarModules.hljs)
140
+ {
141
+ let tmpHljs = window.CodeJarModules.hljs;
142
+ let tmpLanguage = this._language;
143
+
144
+ this._highlightFunction = function (pElement)
145
+ {
146
+ // Remove any previous hljs state
147
+ pElement.removeAttribute('data-highlighted');
148
+ delete pElement.dataset.highlighted;
149
+
150
+ // Set the language class for hljs
151
+ pElement.className = pElement.className.replace(/\bhljs\b/g, '').replace(/\blanguage-\S+/g, '').trim();
152
+ pElement.classList.add('hljs');
153
+ if (tmpLanguage && tmpLanguage !== 'plaintext')
154
+ {
155
+ pElement.classList.add('language-' + tmpLanguage);
156
+ }
157
+
158
+ tmpHljs.highlightElement(pElement);
159
+ };
160
+ }
161
+
162
+ // Now call the parent which creates the CodeJar instance
163
+ return super.onAfterInitialRender();
164
+ }
165
+
166
+ /**
167
+ * Set the language for the editor and update highlight.js integration.
168
+ *
169
+ * @param {string} pLanguage - The highlight.js language identifier
170
+ */
171
+ setLanguage(pLanguage)
172
+ {
173
+ this._language = pLanguage;
174
+
175
+ // Rebuild the highlight function with the new language
176
+ if (typeof (window) !== 'undefined' && window.CodeJarModules && window.CodeJarModules.hljs)
177
+ {
178
+ let tmpHljs = window.CodeJarModules.hljs;
179
+ let tmpLanguage = pLanguage;
180
+
181
+ this._highlightFunction = function (pElement)
182
+ {
183
+ pElement.removeAttribute('data-highlighted');
184
+ delete pElement.dataset.highlighted;
185
+
186
+ pElement.className = pElement.className.replace(/\bhljs\b/g, '').replace(/\blanguage-\S+/g, '').trim();
187
+ pElement.classList.add('hljs');
188
+ if (tmpLanguage && tmpLanguage !== 'plaintext')
189
+ {
190
+ pElement.classList.add('language-' + tmpLanguage);
191
+ }
192
+
193
+ tmpHljs.highlightElement(pElement);
194
+ };
195
+ }
196
+
197
+ if (this._editorElement)
198
+ {
199
+ this._editorElement.className = 'pict-code-editor language-' + pLanguage;
200
+ if (!this.options.LineNumbers)
201
+ {
202
+ this._editorElement.className += ' pict-code-no-line-numbers';
203
+ }
204
+ }
205
+
206
+ if (this.codeJar)
207
+ {
208
+ // Re-create the editor with the new highlight function
209
+ let tmpCode = this.codeJar.toString();
210
+ this.codeJar.destroy();
211
+ this.codeJar = this._codeJarPrototype(this._editorElement, this._highlightFunction,
212
+ {
213
+ tab: this.options.Tab,
214
+ catchTab: this.options.CatchTab,
215
+ addClosing: this.options.AddClosing
216
+ });
217
+ this.codeJar.updateCode(tmpCode);
218
+ this.codeJar.onUpdate((pCode) =>
219
+ {
220
+ this._updateLineNumbers();
221
+ this.onCodeChange(pCode);
222
+ });
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handle content changes — mark document as dirty.
228
+ *
229
+ * @param {string} pCode - The new code content
230
+ */
231
+ onCodeChange(pCode)
232
+ {
233
+ // Write back to data address
234
+ super.onCodeChange(pCode);
235
+
236
+ // Suppress the dirty signal from the initial marshalToView call
237
+ if (this._suppressNextDirty)
238
+ {
239
+ this._suppressNextDirty = false;
240
+ return;
241
+ }
242
+
243
+ // Mark the document as dirty and update stats
244
+ if (this.pict.PictApplication)
245
+ {
246
+ this.pict.PictApplication.markDirty();
247
+ this.pict.PictApplication.updateStats();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Get the highlight.js language for a file extension.
253
+ *
254
+ * @param {string} pExtension - The file extension (without dot)
255
+ * @returns {string} The highlight.js language identifier
256
+ */
257
+ static getLanguageForExtension(pExtension)
258
+ {
259
+ if (!pExtension)
260
+ {
261
+ return 'plaintext';
262
+ }
263
+ let tmpExt = pExtension.toLowerCase();
264
+ return _ExtensionLanguageMap[tmpExt] || 'plaintext';
265
+ }
266
+ }
267
+
268
+ module.exports = ContentEditorCodeEditorView;
269
+
270
+ module.exports.default_configuration = _ViewConfiguration;
271
+ module.exports.ExtensionLanguageMap = _ExtensionLanguageMap;