retold-content-system 1.0.4 → 1.0.6
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/docs/1772209863522-Editor-Overview.png +0 -0
- package/docs/editor-guide.md +2 -0
- package/docs/getting-started.md +7 -0
- package/package.json +2 -2
- package/server.js +0 -2
- package/source/Pict-Application-ContentEditor.js +183 -8
- package/source/cli/ContentSystem-Server-Setup.js +53 -43
- package/source/cli/commands/ContentSystem-Command-Serve.js +0 -12
- package/source/providers/Pict-Provider-ContentEditor.js +0 -25
- package/source/views/PictView-Editor-Layout.js +216 -5
- package/source/views/PictView-Editor-SettingsPanel.js +57 -0
- package/source/views/PictView-Editor-TopBar.js +93 -0
- package/web-application/retold-content-system.compatible.js +217 -223
- package/web-application/retold-content-system.compatible.js.map +1 -1
- package/web-application/retold-content-system.compatible.min.js +5 -5
- package/web-application/retold-content-system.compatible.min.js.map +1 -1
- package/web-application/retold-content-system.js +153 -159
- package/web-application/retold-content-system.js.map +1 -1
- package/web-application/retold-content-system.min.js +13 -13
- package/web-application/retold-content-system.min.js.map +1 -1
|
Binary file
|
package/docs/editor-guide.md
CHANGED
|
@@ -4,6 +4,8 @@ The content editor is a browser-based application for editing markdown files and
|
|
|
4
4
|
|
|
5
5
|
## Layout
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
The editor has three main areas:
|
|
8
10
|
|
|
9
11
|
- **Top Bar** -- Shows the application name, current file name, save status, word/character stats, and action buttons (Save, Close).
|
package/docs/getting-started.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Retold Content System is a local markdown editor and documentation viewer. Install it once with npm and point it at any folder of markdown files.
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
7
8
|
Install globally from npm:
|
|
@@ -12,6 +13,8 @@ npm install -g retold-content-system
|
|
|
12
13
|
|
|
13
14
|
This gives you two equivalent CLI commands: `retold-content-system` and `rcs`.
|
|
14
15
|
|
|
16
|
+

|
|
17
|
+
|
|
15
18
|
## Quick Start
|
|
16
19
|
|
|
17
20
|
Create a folder with some markdown files and serve it:
|
|
@@ -33,6 +36,7 @@ The server starts on a random port between 7000 and 7999 and prints the URLs for
|
|
|
33
36
|
|
|
34
37
|
Open the **Reader** URL for a pict-docuserve documentation viewer, or the **Editor** URL for the full editing environment.
|
|
35
38
|
|
|
39
|
+
|
|
36
40
|
## Choosing a Port
|
|
37
41
|
|
|
38
42
|
Pass `-p` to pin the server to a specific port:
|
|
@@ -41,6 +45,7 @@ Pass `-p` to pin the server to a specific port:
|
|
|
41
45
|
rcs serve -p 8080
|
|
42
46
|
```
|
|
43
47
|
|
|
48
|
+
|
|
44
49
|
## Pointing at a Different Folder
|
|
45
50
|
|
|
46
51
|
Provide a content path as the first argument:
|
|
@@ -51,6 +56,7 @@ rcs serve ~/projects/my-wiki
|
|
|
51
56
|
|
|
52
57
|
If the target directory has a `content/` subfolder, the server uses that automatically. This means running `rcs serve` from a project root that has a `content/` directory does the right thing without extra arguments.
|
|
53
58
|
|
|
59
|
+
|
|
54
60
|
## What Gets Served
|
|
55
61
|
|
|
56
62
|
The content system sets up three static routes and several API endpoints:
|
|
@@ -66,6 +72,7 @@ The content system sets up three static routes and several API endpoints:
|
|
|
66
72
|
| `/api/content/save/*` | File content save API |
|
|
67
73
|
| `/api/content/upload-image` | Image upload endpoint |
|
|
68
74
|
|
|
75
|
+
|
|
69
76
|
## Next Steps
|
|
70
77
|
|
|
71
78
|
- Read the [Editor Guide](editor-guide.md) for a walkthrough of the editing UI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retold-content-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Retold Content System - Markdown content viewer and editor",
|
|
5
5
|
"main": "source/Pict-ContentSystem-Bundle.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"pict-section-code": "^1.0.3",
|
|
44
44
|
"pict-section-content": "^0.0.8",
|
|
45
45
|
"pict-section-filebrowser": "^0.0.2",
|
|
46
|
-
"pict-section-markdowneditor": "^1.0.
|
|
46
|
+
"pict-section-markdowneditor": "^1.0.3",
|
|
47
47
|
"pict-service-commandlineutility": "^1.0.19",
|
|
48
48
|
"pict-view": "^1.0.67"
|
|
49
49
|
},
|
package/server.js
CHANGED
|
@@ -21,7 +21,6 @@ let tmpPort = parseInt(process.env.PORT, 10) || 8086;
|
|
|
21
21
|
libSetupServer(
|
|
22
22
|
{
|
|
23
23
|
ContentPath: libPath.join(__dirname, 'content'),
|
|
24
|
-
UploadPath: libPath.join(__dirname, 'uploads'),
|
|
25
24
|
DistPath: libPath.join(__dirname, 'web-application'),
|
|
26
25
|
Port: tmpPort
|
|
27
26
|
},
|
|
@@ -36,7 +35,6 @@ libSetupServer(
|
|
|
36
35
|
pServerInfo.Fable.log.info(` Retold Content System running on http://localhost:${pServerInfo.Port}`);
|
|
37
36
|
pServerInfo.Fable.log.info('==========================================================');
|
|
38
37
|
pServerInfo.Fable.log.info(` Content path: ${libPath.join(__dirname, 'content')}`);
|
|
39
|
-
pServerInfo.Fable.log.info(` Upload path: ${libPath.join(__dirname, 'uploads')}`);
|
|
40
38
|
pServerInfo.Fable.log.info(` Reader: http://localhost:${pServerInfo.Port}/`);
|
|
41
39
|
pServerInfo.Fable.log.info(` Editor: http://localhost:${pServerInfo.Port}/edit.html`);
|
|
42
40
|
pServerInfo.Fable.log.info('==========================================================');
|
|
@@ -46,9 +46,45 @@ class ContentEditorApplication extends libPictApplication
|
|
|
46
46
|
tmpFileBrowserConfig.DefaultState.Layout = 'list-only';
|
|
47
47
|
this.pict.addView('Pict-FileBrowser', tmpFileBrowserConfig, libPictSectionFileBrowser);
|
|
48
48
|
|
|
49
|
-
// Register the list detail sub-view for the file list pane
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Register the list detail sub-view for the file list pane.
|
|
50
|
+
// Override templates to:
|
|
51
|
+
// - Add a "create folder" button to the breadcrumb bar
|
|
52
|
+
// - Add a hover-visible "+" insert button on each file row
|
|
53
|
+
let tmpListDetailConfig = JSON.parse(JSON.stringify(
|
|
54
|
+
libPictSectionFileBrowser.PictViewListDetail.default_configuration));
|
|
55
|
+
for (let i = 0; i < tmpListDetailConfig.Templates.length; i++)
|
|
56
|
+
{
|
|
57
|
+
if (tmpListDetailConfig.Templates[i].Hash === 'FileBrowser-ListDetail-Container-Template')
|
|
58
|
+
{
|
|
59
|
+
tmpListDetailConfig.Templates[i].Template = /*html*/`
|
|
60
|
+
<div class="pict-fb-detail" id="Pict-FileBrowser-DetailList">
|
|
61
|
+
<div class="pict-fb-breadcrumb-bar">
|
|
62
|
+
<div class="pict-fb-breadcrumb" id="Pict-FileBrowser-Breadcrumb"></div>
|
|
63
|
+
<button class="pict-fb-breadcrumb-addfolder" onclick="pict.PictApplication.promptNewFolder()" title="New folder">+</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="pict-fb-detail-header">
|
|
66
|
+
<div class="pict-fb-detail-header-cell pict-fb-detail-col-name" onclick="pict.views['{~D:Record.ViewHash~}'].sortBy('Name')">Name</div>
|
|
67
|
+
<div class="pict-fb-detail-header-cell pict-fb-detail-col-size" onclick="pict.views['{~D:Record.ViewHash~}'].sortBy('Size')">Size</div>
|
|
68
|
+
<div class="pict-fb-detail-header-cell pict-fb-detail-col-modified" onclick="pict.views['{~D:Record.ViewHash~}'].sortBy('Modified')">Modified</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div id="Pict-FileBrowser-DetailRows"></div>
|
|
71
|
+
</div>
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
if (tmpListDetailConfig.Templates[i].Hash === 'FileBrowser-ListDetail-Row-Template')
|
|
75
|
+
{
|
|
76
|
+
tmpListDetailConfig.Templates[i].Template = /*html*/`
|
|
77
|
+
<div class="pict-fb-detail-row{~D:Record.SelectedClass~}" data-index="{~D:Record.Index~}" data-name="{~D:Record.Name~}" onclick="{~D:Record.ClickHandler~}" ondblclick="{~D:Record.DblClickHandler~}">
|
|
78
|
+
<span class="pict-fb-detail-icon">{~D:Record.Icon~}</span>
|
|
79
|
+
<span class="pict-fb-detail-name">{~D:Record.Name~}</span>
|
|
80
|
+
<span class="pict-fb-detail-size">{~D:Record.SizeFormatted~}</span>
|
|
81
|
+
<span class="pict-fb-detail-modified">{~D:Record.ModifiedFormatted~}</span>
|
|
82
|
+
<button class="pict-fb-insert-btn" onclick="event.stopPropagation(); pict.PictApplication.insertFileReference(this.parentElement.getAttribute('data-name'))" title="Insert into editor">+</button>
|
|
83
|
+
</div>
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.pict.addView('Pict-FileBrowser-ListDetail', tmpListDetailConfig,
|
|
52
88
|
libPictSectionFileBrowser.PictViewListDetail);
|
|
53
89
|
}
|
|
54
90
|
|
|
@@ -81,7 +117,7 @@ class ContentEditorApplication extends libPictApplication
|
|
|
81
117
|
// Settings
|
|
82
118
|
AutoSegmentMarkdown: false,
|
|
83
119
|
AutoSegmentDepth: 1,
|
|
84
|
-
AutoContentPreview:
|
|
120
|
+
AutoContentPreview: true,
|
|
85
121
|
MarkdownEditingControls: true,
|
|
86
122
|
MarkdownWordWrap: true,
|
|
87
123
|
CodeWordWrap: false,
|
|
@@ -689,13 +725,32 @@ class ContentEditorApplication extends libPictApplication
|
|
|
689
725
|
let tmpEditorView = tmpSelf.pict.views['ContentEditor-MarkdownEditor'];
|
|
690
726
|
if (tmpEditorView)
|
|
691
727
|
{
|
|
728
|
+
// Set the image base URL so relative image references
|
|
729
|
+
// resolve through the /content/ static route.
|
|
730
|
+
let tmpImageBase = '/content/';
|
|
731
|
+
let tmpLastSlash = pFilePath.lastIndexOf('/');
|
|
732
|
+
if (tmpLastSlash > 0)
|
|
733
|
+
{
|
|
734
|
+
tmpImageBase = '/content/' + pFilePath.substring(0, tmpLastSlash) + '/';
|
|
735
|
+
}
|
|
736
|
+
tmpEditorView.options.ImageBaseURL = tmpImageBase;
|
|
737
|
+
|
|
692
738
|
tmpEditorView.render();
|
|
693
739
|
tmpEditorView.marshalToView();
|
|
694
740
|
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
}
|
|
699
754
|
|
|
700
755
|
// Apply the Editing Controls setting (line numbers
|
|
701
756
|
// and right sidebar) via the library's toggleControls.
|
|
@@ -1007,6 +1062,126 @@ class ContentEditorApplication extends libPictApplication
|
|
|
1007
1062
|
}
|
|
1008
1063
|
}
|
|
1009
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* Prompt the user for a folder name and create it in the current
|
|
1067
|
+
* browse location via the server API.
|
|
1068
|
+
*/
|
|
1069
|
+
promptNewFolder()
|
|
1070
|
+
{
|
|
1071
|
+
let tmpFolderName = prompt('Enter a name for the new folder:');
|
|
1072
|
+
if (!tmpFolderName || !tmpFolderName.trim())
|
|
1073
|
+
{
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
tmpFolderName = tmpFolderName.trim();
|
|
1077
|
+
|
|
1078
|
+
// Build the full relative path inside the current browse location
|
|
1079
|
+
let tmpCurrentLocation = '';
|
|
1080
|
+
if (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation)
|
|
1081
|
+
{
|
|
1082
|
+
tmpCurrentLocation = this.pict.AppData.PictFileBrowser.CurrentLocation;
|
|
1083
|
+
}
|
|
1084
|
+
let tmpPath = tmpCurrentLocation
|
|
1085
|
+
? (tmpCurrentLocation + '/' + tmpFolderName)
|
|
1086
|
+
: tmpFolderName;
|
|
1087
|
+
|
|
1088
|
+
let tmpSelf = this;
|
|
1089
|
+
|
|
1090
|
+
fetch('/api/content/mkdir',
|
|
1091
|
+
{
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1094
|
+
body: JSON.stringify({ Path: tmpPath })
|
|
1095
|
+
})
|
|
1096
|
+
.then((pResponse) => pResponse.json())
|
|
1097
|
+
.then((pData) =>
|
|
1098
|
+
{
|
|
1099
|
+
if (pData && pData.Success)
|
|
1100
|
+
{
|
|
1101
|
+
tmpSelf.log.info(`Folder created: ${tmpPath}`);
|
|
1102
|
+
// Refresh the file list to show the new folder
|
|
1103
|
+
tmpSelf.loadFileList();
|
|
1104
|
+
}
|
|
1105
|
+
else
|
|
1106
|
+
{
|
|
1107
|
+
alert('Could not create folder: ' + (pData ? pData.Error : 'Unknown error'));
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
.catch((pError) =>
|
|
1111
|
+
{
|
|
1112
|
+
alert('Error creating folder: ' + pError.message);
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Insert a file reference into the active markdown editor segment.
|
|
1118
|
+
*
|
|
1119
|
+
* Called from the "+" button on file browser rows. For image files
|
|
1120
|
+
* this inserts markdown image syntax; for other files a markdown link.
|
|
1121
|
+
*
|
|
1122
|
+
* @param {string} pFilename - The filename to insert
|
|
1123
|
+
*/
|
|
1124
|
+
insertFileReference(pFilename)
|
|
1125
|
+
{
|
|
1126
|
+
if (!pFilename)
|
|
1127
|
+
{
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
let tmpEditorView = this.pict.views['ContentEditor-MarkdownEditor'];
|
|
1132
|
+
if (!tmpEditorView || this.pict.AppData.ContentEditor.ActiveEditor !== 'markdown')
|
|
1133
|
+
{
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Determine the active segment (the one with focus, or the last one)
|
|
1138
|
+
let tmpSegmentIndex = tmpEditorView._activeSegmentIndex;
|
|
1139
|
+
if (tmpSegmentIndex < 0)
|
|
1140
|
+
{
|
|
1141
|
+
// Fall back to the first segment
|
|
1142
|
+
let tmpIndices = tmpEditorView._getOrderedSegmentIndices();
|
|
1143
|
+
if (tmpIndices.length > 0)
|
|
1144
|
+
{
|
|
1145
|
+
tmpSegmentIndex = tmpIndices[0];
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (tmpSegmentIndex < 0)
|
|
1149
|
+
{
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Build alt text from the filename (strip extension and timestamp prefix)
|
|
1154
|
+
let tmpAltText = pFilename.replace(/\.[^.]+$/, '');
|
|
1155
|
+
tmpAltText = tmpAltText.replace(/^\d{10,}-/, '');
|
|
1156
|
+
tmpAltText = tmpAltText.replace(/[-_]+/g, ' ').trim() || 'image';
|
|
1157
|
+
|
|
1158
|
+
// Check if this is an image file
|
|
1159
|
+
let tmpExt = pFilename.substring(pFilename.lastIndexOf('.')).toLowerCase();
|
|
1160
|
+
let tmpImageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.avif', '.apng', '.ico', '.tiff', '.tif', '.jfif'];
|
|
1161
|
+
|
|
1162
|
+
if (tmpImageExts.indexOf(tmpExt) >= 0)
|
|
1163
|
+
{
|
|
1164
|
+
// Insert markdown image syntax — relative filename resolved by ImageBaseURL
|
|
1165
|
+
tmpEditorView._insertImageMarkdown(tmpSegmentIndex, pFilename, tmpAltText);
|
|
1166
|
+
}
|
|
1167
|
+
else
|
|
1168
|
+
{
|
|
1169
|
+
// Insert a markdown link for non-image files
|
|
1170
|
+
let tmpEditor = tmpEditorView._segmentEditors[tmpSegmentIndex];
|
|
1171
|
+
if (tmpEditor)
|
|
1172
|
+
{
|
|
1173
|
+
let tmpInsert = '[' + tmpAltText + '](' + pFilename + ')';
|
|
1174
|
+
let tmpCursorPos = tmpEditor.state.selection.main.head;
|
|
1175
|
+
tmpEditor.dispatch(
|
|
1176
|
+
{
|
|
1177
|
+
changes: { from: tmpCursorPos, insert: tmpInsert },
|
|
1178
|
+
selection: { anchor: tmpCursorPos + tmpInsert.length }
|
|
1179
|
+
});
|
|
1180
|
+
tmpEditor.focus();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1010
1185
|
/**
|
|
1011
1186
|
* Handle F4 / Cmd+Shift+T: context-aware topic creation.
|
|
1012
1187
|
*
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @param {object} pOptions
|
|
8
8
|
* @param {string} pOptions.ContentPath - Absolute path to the markdown content folder
|
|
9
|
-
* @param {string} pOptions.UploadPath - Absolute path to the uploads folder
|
|
10
9
|
* @param {string} pOptions.DistPath - Absolute path to the built web-application folder
|
|
11
10
|
* @param {number} pOptions.Port - HTTP port to listen on
|
|
12
11
|
* @param {Function} fCallback - Callback(pError, { Fable, Orator, Port })
|
|
@@ -84,7 +83,6 @@ function sanitizeFilename(pName)
|
|
|
84
83
|
function setupContentSystemServer(pOptions, fCallback)
|
|
85
84
|
{
|
|
86
85
|
let tmpContentPath = pOptions.ContentPath;
|
|
87
|
-
let tmpUploadPath = pOptions.UploadPath;
|
|
88
86
|
let tmpDistFolder = pOptions.DistPath;
|
|
89
87
|
let tmpPort = pOptions.Port;
|
|
90
88
|
|
|
@@ -93,18 +91,11 @@ function setupContentSystemServer(pOptions, fCallback)
|
|
|
93
91
|
Product: 'Retold-Content-System',
|
|
94
92
|
ProductVersion: require('../../package.json').version,
|
|
95
93
|
APIServerPort: tmpPort,
|
|
96
|
-
ContentPath: tmpContentPath
|
|
97
|
-
UploadPath: tmpUploadPath
|
|
94
|
+
ContentPath: tmpContentPath
|
|
98
95
|
};
|
|
99
96
|
|
|
100
97
|
let tmpFable = new libFable(tmpSettings);
|
|
101
98
|
|
|
102
|
-
// Ensure the uploads directory exists
|
|
103
|
-
if (!libFs.existsSync(tmpUploadPath))
|
|
104
|
-
{
|
|
105
|
-
libFs.mkdirSync(tmpUploadPath, { recursive: true });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
99
|
// Ensure the content directory exists
|
|
109
100
|
if (!libFs.existsSync(tmpContentPath))
|
|
110
101
|
{
|
|
@@ -156,6 +147,57 @@ function setupContentSystemServer(pOptions, fCallback)
|
|
|
156
147
|
return fNext();
|
|
157
148
|
});
|
|
158
149
|
|
|
150
|
+
// --- POST /api/content/mkdir ---
|
|
151
|
+
// Create a new folder inside the content directory.
|
|
152
|
+
// Body: { Path: "relative/path/to/new-folder" }
|
|
153
|
+
tmpServiceServer.post('/api/content/mkdir',
|
|
154
|
+
(pRequest, pResponse, fNext) =>
|
|
155
|
+
{
|
|
156
|
+
try
|
|
157
|
+
{
|
|
158
|
+
let tmpRawPath = (pRequest.body && pRequest.body.Path) ? pRequest.body.Path : null;
|
|
159
|
+
let tmpSafePath = sanitizePath(tmpRawPath);
|
|
160
|
+
|
|
161
|
+
if (!tmpSafePath)
|
|
162
|
+
{
|
|
163
|
+
pResponse.send(400, { Success: false, Error: 'Invalid folder path.' });
|
|
164
|
+
return fNext();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let tmpFullPath = libPath.join(tmpContentPath, tmpSafePath);
|
|
168
|
+
|
|
169
|
+
// Ensure the resolved path is within the content directory
|
|
170
|
+
let tmpRealContent = libFs.realpathSync(tmpContentPath);
|
|
171
|
+
let tmpTargetParent = libPath.dirname(tmpFullPath);
|
|
172
|
+
if (libFs.existsSync(tmpTargetParent))
|
|
173
|
+
{
|
|
174
|
+
tmpTargetParent = libFs.realpathSync(tmpTargetParent);
|
|
175
|
+
}
|
|
176
|
+
if (!tmpTargetParent.startsWith(tmpRealContent))
|
|
177
|
+
{
|
|
178
|
+
pResponse.send(403, { Success: false, Error: 'Access denied.' });
|
|
179
|
+
return fNext();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (libFs.existsSync(tmpFullPath))
|
|
183
|
+
{
|
|
184
|
+
pResponse.send(409, { Success: false, Error: 'Folder already exists.' });
|
|
185
|
+
return fNext();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
libFs.mkdirSync(tmpFullPath, { recursive: true });
|
|
189
|
+
|
|
190
|
+
tmpFable.log.info(`Folder created: ${tmpSafePath}`);
|
|
191
|
+
pResponse.send({ Success: true, Path: tmpSafePath });
|
|
192
|
+
}
|
|
193
|
+
catch (pError)
|
|
194
|
+
{
|
|
195
|
+
tmpFable.log.error(`Folder creation failed: ${pError.message}`);
|
|
196
|
+
pResponse.send(500, { Success: false, Error: pError.message });
|
|
197
|
+
}
|
|
198
|
+
return fNext();
|
|
199
|
+
});
|
|
200
|
+
|
|
159
201
|
// --- GET /api/content/read/* ---
|
|
160
202
|
// Read the raw markdown content of a file
|
|
161
203
|
tmpServiceServer.get('/api/content/read/*',
|
|
@@ -350,39 +392,7 @@ function setupContentSystemServer(pOptions, fCallback)
|
|
|
350
392
|
return fNext();
|
|
351
393
|
});
|
|
352
394
|
|
|
353
|
-
//
|
|
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)
|
|
395
|
+
// Serve content files (markdown, images, etc.) at /content/
|
|
386
396
|
tmpOrator.addStaticRoute(`${tmpContentPath}/`, 'index.html', '/content/*', '/content/');
|
|
387
397
|
|
|
388
398
|
// Serve the built application from dist/ (main static route)
|
|
@@ -38,7 +38,6 @@ class ContentSystemCommandServe extends libCommandLineCommand
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
let tmpDistPath = libPath.resolve(__dirname, '..', '..', '..', 'web-application');
|
|
41
|
-
let tmpUploadPath = libPath.join(tmpContentPath, 'uploads');
|
|
42
41
|
let tmpPortOption = parseInt(this.CommandOptions.port, 10);
|
|
43
42
|
let tmpPort = (tmpPortOption > 0) ? tmpPortOption : (7000 + Math.floor(Math.random() * 1000));
|
|
44
43
|
|
|
@@ -56,19 +55,12 @@ class ContentSystemCommandServe extends libCommandLineCommand
|
|
|
56
55
|
this.log.info(`Created content directory: ${tmpContentPath}`);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// Ensure uploads directory exists
|
|
60
|
-
if (!libFs.existsSync(tmpUploadPath))
|
|
61
|
-
{
|
|
62
|
-
libFs.mkdirSync(tmpUploadPath, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
58
|
let tmpSelf = this;
|
|
66
59
|
let tmpSetupServer = require('../ContentSystem-Server-Setup.js');
|
|
67
60
|
|
|
68
61
|
tmpSetupServer(
|
|
69
62
|
{
|
|
70
63
|
ContentPath: tmpContentPath,
|
|
71
|
-
UploadPath: tmpUploadPath,
|
|
72
64
|
DistPath: tmpDistPath,
|
|
73
65
|
Port: tmpPort
|
|
74
66
|
},
|
|
@@ -85,10 +77,6 @@ class ContentSystemCommandServe extends libCommandLineCommand
|
|
|
85
77
|
tmpSelf.log.info(` Retold Content System running on http://localhost:${pServerInfo.Port}`);
|
|
86
78
|
tmpSelf.log.info('==========================================================');
|
|
87
79
|
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
80
|
tmpSelf.log.info('==========================================================');
|
|
93
81
|
tmpSelf.log.info('');
|
|
94
82
|
tmpSelf.log.info(' Press Ctrl+C to stop.');
|
|
@@ -160,31 +160,6 @@ class ContentEditorProvider extends libPictProvider
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
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
163
|
}
|
|
189
164
|
|
|
190
165
|
module.exports = ContentEditorProvider;
|