@gpdoc/cli 1.8.0 → 1.8.1

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.
@@ -3,6 +3,7 @@ import { createServer } from 'node:http';
3
3
  import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  const {
6
+ createManagedDocument,
6
7
  readDocument,
7
8
  serializeManagedMarkdown,
8
9
  validateDocument,
@@ -102,11 +103,44 @@ function contentForSave(snapshot, content) {
102
103
  return content;
103
104
  }
104
105
 
106
+ function isPlainObject(value) {
107
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
108
+ && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
109
+ }
110
+
111
+ // @spec CLI-040, CLI-042, CLI-043, CLI-044
112
+ function metadataForSave(snapshot, metadata) {
113
+ if (!isPlainObject(metadata)) {
114
+ throw new EditorPreviewError('INVALID_INPUT', 'Front matter metadata must be an object.');
115
+ }
116
+ const currentMetadata = snapshot.document.managed
117
+ ? snapshot.document.metadata
118
+ : createManagedDocument(snapshot.document.title, snapshot.document.body);
119
+ const nextMetadata = {
120
+ ...currentMetadata,
121
+ ...metadata,
122
+ content: { document: snapshot.document.body },
123
+ };
124
+ return serializeManagedMarkdown(nextMetadata, snapshot.document.body);
125
+ }
126
+
127
+ function frontMatterFor(snapshot) {
128
+ const metadata = snapshot.document.managed
129
+ ? snapshot.document.metadata
130
+ : createManagedDocument(snapshot.document.title, snapshot.document.body);
131
+ return {
132
+ managed: snapshot.document.managed,
133
+ metadata,
134
+ };
135
+ }
136
+
105
137
  // @spec CLI-032
106
138
  export async function startGPEditorPreview({ filePath, editorHtml }) {
107
139
  if (!editorHtml) throw new EditorPreviewError('EDITOR_PREVIEW_UNAVAILABLE', 'GPEditor preview assets are unavailable. Reinstall @gpdoc/cli.');
108
140
  const target = path.resolve(filePath);
109
- const localEditorHtml = editorHtml.replace(/<\/head>/i, '<style>.share-action{display:none!important}</style></head>');
141
+ const localEditorHtml = editorHtml
142
+ .replace(/<html([^>]*)>/i, '<html$1 data-gpdoc-cli-editor>')
143
+ .replace(/<\/head>/i, '<style>.share-action{display:none!important}</style></head>');
110
144
  await readSnapshot(target);
111
145
  const token = crypto.randomBytes(32).toString('base64url');
112
146
  let origin = '';
@@ -116,6 +150,7 @@ export async function startGPEditorPreview({ filePath, editorHtml }) {
116
150
  const url = new URL(request.url || '/', origin || 'http://127.0.0.1');
117
151
  const editorPath = `/editor/${token}`;
118
152
  const apiPath = `/api/editor/${token}`;
153
+ const frontMatterPath = `${apiPath}/front-matter`;
119
154
  if (request.method === 'GET' && url.pathname === editorPath) {
120
155
  response.writeHead(200, {
121
156
  'Content-Type': 'text/html; charset=utf-8',
@@ -127,7 +162,7 @@ export async function startGPEditorPreview({ filePath, editorHtml }) {
127
162
  response.end(localEditorHtml);
128
163
  return;
129
164
  }
130
- if (url.pathname !== apiPath && url.pathname !== `${apiPath}/save`) {
165
+ if (url.pathname !== apiPath && url.pathname !== `${apiPath}/save` && url.pathname !== frontMatterPath) {
131
166
  sendJson(response, 404, { error: { code: 'ROUTE_NOT_FOUND', message: 'The editor route was not found.' } });
132
167
  return;
133
168
  }
@@ -140,7 +175,12 @@ export async function startGPEditorPreview({ filePath, editorHtml }) {
140
175
  sendJson(response, 200, { editor });
141
176
  return;
142
177
  }
143
- if (request.method !== 'POST' || url.pathname !== `${apiPath}/save`) {
178
+ if (request.method === 'GET' && url.pathname === frontMatterPath) {
179
+ const snapshot = await readSnapshot(target);
180
+ sendJson(response, 200, { frontMatter: frontMatterFor(snapshot) });
181
+ return;
182
+ }
183
+ if (request.method !== 'POST' || (url.pathname !== `${apiPath}/save` && url.pathname !== frontMatterPath)) {
144
184
  sendJson(response, 405, { error: { code: 'METHOD_NOT_ALLOWED', message: 'The editor operation does not support this method.' } });
145
185
  return;
146
186
  }
@@ -149,12 +189,16 @@ export async function startGPEditorPreview({ filePath, editorHtml }) {
149
189
  return;
150
190
  }
151
191
  const input = await readJson(request);
152
- if (typeof input.content !== 'string') throw new EditorPreviewError('INVALID_INPUT', 'Editor content must be text.');
153
192
  const snapshot = await readSnapshot(target);
154
193
  if (String(input.expectedRevision || '') !== snapshot.editor.revision) {
155
194
  throw new EditorPreviewError('REVISION_CONFLICT', 'The local file changed. Reload the editor before saving.');
156
195
  }
157
- const nextSource = contentForSave(snapshot, input.content);
196
+ const nextSource = url.pathname === frontMatterPath
197
+ ? metadataForSave(snapshot, input.metadata)
198
+ : (() => {
199
+ if (typeof input.content !== 'string') throw new EditorPreviewError('INVALID_INPUT', 'Editor content must be text.');
200
+ return contentForSave(snapshot, input.content);
201
+ })();
158
202
  const prospective = snapshotFor(target, nextSource);
159
203
  validateDocument(prospective.document);
160
204
  await atomicWrite(target, nextSource);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpdoc/cli",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "type": "module",
5
5
  "description": "GPDoc command-line file conversion and validation tools",
6
6
  "repository": "https://github.com/repetere/gpdoc.git",