@rmdes/indiekit-endpoint-micropub 1.0.0-beta.25
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/README.md +76 -0
- package/assets/icon.svg +4 -0
- package/index.js +33 -0
- package/lib/config.js +82 -0
- package/lib/controllers/action.js +141 -0
- package/lib/controllers/query.js +117 -0
- package/lib/jf2.js +318 -0
- package/lib/markdown.js +56 -0
- package/lib/media.js +45 -0
- package/lib/mf2.js +80 -0
- package/lib/post-content.js +147 -0
- package/lib/post-data.js +285 -0
- package/lib/post-type-count.js +49 -0
- package/lib/post-type-discovery.js +68 -0
- package/lib/reserved-properties.js +10 -0
- package/lib/scope.js +36 -0
- package/lib/update.js +129 -0
- package/lib/utils.js +121 -0
- package/package.json +54 -0
package/lib/jf2.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { excerpt, getDate, md5, slugify } from "@indiekit/util";
|
|
2
|
+
import {
|
|
3
|
+
fetchReferences,
|
|
4
|
+
mf2tojf2,
|
|
5
|
+
mf2tojf2referenced,
|
|
6
|
+
} from "@paulrobertlloyd/mf2tojf2";
|
|
7
|
+
|
|
8
|
+
import { markdownToHtml, htmlToMarkdown } from "./markdown.js";
|
|
9
|
+
import { reservedProperties } from "./reserved-properties.js";
|
|
10
|
+
import { decodeQueryParameter, relativeMediaPath, toArray } from "./utils.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create JF2 object from form-encoded request
|
|
14
|
+
* @param {object} body - Form-encoded request body
|
|
15
|
+
* @param {boolean} [requestReferences] - Request data for any referenced URLs
|
|
16
|
+
* @returns {Promise<object>} Micropub action
|
|
17
|
+
*/
|
|
18
|
+
export const formEncodedToJf2 = async (body, requestReferences) => {
|
|
19
|
+
const jf2 = {
|
|
20
|
+
type: body.h || "entry",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (const key in body) {
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) {
|
|
25
|
+
// Delete reserved properties
|
|
26
|
+
const isReservedProperty = reservedProperties.includes(key);
|
|
27
|
+
if (isReservedProperty) {
|
|
28
|
+
delete body[key];
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add decoded string value to JF2 object
|
|
33
|
+
jf2[key] = decodeQueryParameter(body[key]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (requestReferences) {
|
|
38
|
+
return fetchReferences(jf2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return jf2;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert mf2 to JF2
|
|
46
|
+
* @param {object} body - Form-encoded request body
|
|
47
|
+
* @param {boolean} [requestReferences] - Request data for any referenced URLs
|
|
48
|
+
* @returns {Promise<object>} Micropub action
|
|
49
|
+
*/
|
|
50
|
+
export const mf2ToJf2 = async (body, requestReferences) => {
|
|
51
|
+
const mf2 = {
|
|
52
|
+
items: [body],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (requestReferences) {
|
|
56
|
+
return mf2tojf2referenced(mf2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return mf2tojf2(mf2);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalise JF2 properties
|
|
64
|
+
* @param {object} publication - Publication configuration
|
|
65
|
+
* @param {object} properties - JF2 properties
|
|
66
|
+
* @param {string} timeZone - Application time zone
|
|
67
|
+
* @returns {object} Normalised JF2 properties
|
|
68
|
+
*/
|
|
69
|
+
export const normaliseProperties = (publication, properties, timeZone) => {
|
|
70
|
+
const { channels, me, slugSeparator } = publication;
|
|
71
|
+
|
|
72
|
+
properties.published = getDate(timeZone, properties.published);
|
|
73
|
+
|
|
74
|
+
if (properties.name) {
|
|
75
|
+
properties.name = properties.name.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (properties.content) {
|
|
79
|
+
properties.content = getContentProperty(properties);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (properties.location) {
|
|
83
|
+
properties.location = getLocationProperty(properties);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (properties.audio) {
|
|
87
|
+
properties.audio = getAudioProperty(properties, me);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (properties.photo) {
|
|
91
|
+
properties.photo = getPhotoProperty(properties, me);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (properties.video) {
|
|
95
|
+
properties.video = getVideoProperty(properties, me);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
properties.slug = getSlugProperty(properties, slugSeparator);
|
|
99
|
+
|
|
100
|
+
const publicationHasChannels = channels && Object.keys(channels).length > 0;
|
|
101
|
+
if (publicationHasChannels) {
|
|
102
|
+
properties.channel = getChannelProperty(properties, channels);
|
|
103
|
+
delete properties["mp-channel"];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (properties["mp-syndicate-to"]) {
|
|
107
|
+
properties["mp-syndicate-to"] = toArray(properties["mp-syndicate-to"]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (properties.syndication) {
|
|
111
|
+
properties.syndication = toArray(properties.syndication);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return properties;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get audio property
|
|
119
|
+
* @param {object} properties - JF2 properties
|
|
120
|
+
* @param {object} me - Publication URL
|
|
121
|
+
* @returns {Array} `audio` property
|
|
122
|
+
*/
|
|
123
|
+
export const getAudioProperty = (properties, me) => {
|
|
124
|
+
let { audio } = properties;
|
|
125
|
+
audio = Array.isArray(audio) ? audio : [audio];
|
|
126
|
+
|
|
127
|
+
return audio.map((item) => ({
|
|
128
|
+
url: relativeMediaPath(item.url || item, me),
|
|
129
|
+
}));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get channel property.
|
|
134
|
+
*
|
|
135
|
+
* If a publication has configured channels, but no channel has been selected,
|
|
136
|
+
* the default channel is used.
|
|
137
|
+
*
|
|
138
|
+
* If `mp-channel` provides a UID that does not appear in the publication’s
|
|
139
|
+
* channels, the default channel is used.
|
|
140
|
+
*
|
|
141
|
+
* The first item in a publication’s configured channels is considered the
|
|
142
|
+
* default channel.
|
|
143
|
+
* @param {object} properties - JF2 properties
|
|
144
|
+
* @param {object} channels - Publication channels
|
|
145
|
+
* @returns {Array} `mp-channel` property
|
|
146
|
+
* @see {@link https://github.com/indieweb/micropub-extensions/issues/40}
|
|
147
|
+
*/
|
|
148
|
+
export const getChannelProperty = (properties, channels) => {
|
|
149
|
+
channels = Object.keys(channels);
|
|
150
|
+
const mpChannel = properties["mp-channel"];
|
|
151
|
+
const providedChannels = Array.isArray(mpChannel) ? mpChannel : [mpChannel];
|
|
152
|
+
const selectedChannels = new Set();
|
|
153
|
+
|
|
154
|
+
// Only select channels that have been configured
|
|
155
|
+
for (const uid of providedChannels) {
|
|
156
|
+
if (channels.includes(uid)) {
|
|
157
|
+
selectedChannels.add(uid);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If no channels provided, use default channel UID
|
|
162
|
+
if (selectedChannels.size === 0) {
|
|
163
|
+
const defaultChannel = channels[0];
|
|
164
|
+
selectedChannels.add(defaultChannel);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return toArray([...selectedChannels]);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get content property.
|
|
172
|
+
*
|
|
173
|
+
* JF2 allows for the provision of both plaintext and HTML representations.
|
|
174
|
+
* Use existing values, or add HTML representation if only plaintext provided.
|
|
175
|
+
* @param {object} properties - JF2 properties
|
|
176
|
+
* @returns {object} `content` property
|
|
177
|
+
* @see {@link https://www.w3.org/TR/jf2/#html-content}
|
|
178
|
+
*/
|
|
179
|
+
export const getContentProperty = (properties) => {
|
|
180
|
+
const { content } = properties;
|
|
181
|
+
let { html, text } = content;
|
|
182
|
+
|
|
183
|
+
// Return existing text and HTML representations, unamended
|
|
184
|
+
if (html && text) {
|
|
185
|
+
return { html, text };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If HTML representation only, add text representation
|
|
189
|
+
if (html && !text) {
|
|
190
|
+
return { html, text: htmlToMarkdown(html) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If text representation only, add HTML representation
|
|
194
|
+
if (!html && text) {
|
|
195
|
+
return { html: markdownToHtml(text), text };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If content is a string, add `html` and move plaintext to `text.
|
|
199
|
+
if (typeof content === "string") {
|
|
200
|
+
text = content;
|
|
201
|
+
html = markdownToHtml(content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { html, text };
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get location property, parsing a Geo URI if provided
|
|
209
|
+
* @param {object|string} properties - JF2 properties
|
|
210
|
+
* @returns {object} `location` property
|
|
211
|
+
*/
|
|
212
|
+
export const getLocationProperty = (properties) => {
|
|
213
|
+
let { location } = properties;
|
|
214
|
+
|
|
215
|
+
if (typeof location === "string" && location.startsWith("geo:")) {
|
|
216
|
+
const geoUriRegexp =
|
|
217
|
+
/geo:(?<latitude>[\d+.?-]*),(?<longitude>[\d+.?-]*)(?:,(?<altitude>[\d+.?-]*))?/;
|
|
218
|
+
const { latitude, longitude, altitude } =
|
|
219
|
+
location.match(geoUriRegexp).groups;
|
|
220
|
+
|
|
221
|
+
location = {
|
|
222
|
+
type: "geo",
|
|
223
|
+
latitude,
|
|
224
|
+
longitude,
|
|
225
|
+
...(altitude ? { altitude } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return location;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get photo property (adding text alternatives where provided)
|
|
234
|
+
* @param {object} properties - JF2 properties
|
|
235
|
+
* @param {object} me - Publication URL
|
|
236
|
+
* @returns {Array} `photo` property
|
|
237
|
+
*/
|
|
238
|
+
export const getPhotoProperty = (properties, me) => {
|
|
239
|
+
let { photo } = properties;
|
|
240
|
+
photo = Array.isArray(photo) ? photo : [photo];
|
|
241
|
+
|
|
242
|
+
let photoAlt = properties["mp-photo-alt"];
|
|
243
|
+
if (photoAlt) {
|
|
244
|
+
photoAlt = Array.isArray(photoAlt) ? photoAlt : [photoAlt];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const property = photo.map((item, index) => ({
|
|
248
|
+
url: relativeMediaPath(item.url || item, me),
|
|
249
|
+
...(item.alt && { alt: item.alt.trim() }),
|
|
250
|
+
...(photoAlt && { alt: photoAlt[index].trim() }),
|
|
251
|
+
}));
|
|
252
|
+
delete properties["mp-photo-alt"];
|
|
253
|
+
return property;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get video property
|
|
258
|
+
* @param {object} properties - JF2 properties
|
|
259
|
+
* @param {object} me - Publication URL
|
|
260
|
+
* @returns {Array} `video` property
|
|
261
|
+
*/
|
|
262
|
+
export const getVideoProperty = (properties, me) => {
|
|
263
|
+
let { video } = properties;
|
|
264
|
+
video = Array.isArray(video) ? video : [video];
|
|
265
|
+
|
|
266
|
+
return video.map((item) => ({
|
|
267
|
+
url: relativeMediaPath(item.url || item, me),
|
|
268
|
+
}));
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get slug
|
|
273
|
+
* @param {object} properties - JF2 properties
|
|
274
|
+
* @param {string} separator - Slug separator
|
|
275
|
+
* @returns {string} Array containing slug value
|
|
276
|
+
*/
|
|
277
|
+
export const getSlugProperty = (properties, separator) => {
|
|
278
|
+
const suggested = properties["mp-slug"];
|
|
279
|
+
const { name, published } = properties;
|
|
280
|
+
|
|
281
|
+
let string;
|
|
282
|
+
if (suggested) {
|
|
283
|
+
string = suggested;
|
|
284
|
+
} else if (name) {
|
|
285
|
+
string = excerpt(name, 5);
|
|
286
|
+
} else {
|
|
287
|
+
string = md5(published).slice(0, 5);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return slugify(string, { separator });
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get `mp-syndicate-to` property
|
|
295
|
+
* @param {object} properties - JF2 properties
|
|
296
|
+
* @param {Array} syndicationTargets - Configured syndication targets
|
|
297
|
+
* @returns {Array|undefined} Resolved syndication targets
|
|
298
|
+
*/
|
|
299
|
+
export const getSyndicateToProperty = (properties, syndicationTargets) => {
|
|
300
|
+
const property = [];
|
|
301
|
+
|
|
302
|
+
if (!syndicationTargets || syndicationTargets.length === 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const target of syndicationTargets) {
|
|
307
|
+
const { uid } = target.info;
|
|
308
|
+
const syndicateTo = properties["mp-syndicate-to"];
|
|
309
|
+
|
|
310
|
+
if (syndicateTo?.includes(uid)) {
|
|
311
|
+
property.push(uid);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (property.length > 0) {
|
|
316
|
+
return property;
|
|
317
|
+
}
|
|
318
|
+
};
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import markdownIt from "markdown-it";
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert Markdown to HTML
|
|
6
|
+
* @param {string} string - Markdown
|
|
7
|
+
* @returns {string} HTML
|
|
8
|
+
*/
|
|
9
|
+
export const markdownToHtml = (string) => {
|
|
10
|
+
const options = {
|
|
11
|
+
html: true,
|
|
12
|
+
breaks: true,
|
|
13
|
+
typographer: true,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const parser = markdownIt(options);
|
|
17
|
+
|
|
18
|
+
const html = parser.render(string).trim();
|
|
19
|
+
|
|
20
|
+
return html;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert HTML to Markdown
|
|
25
|
+
* @param {string} string - String (may be HTML or Markdown)
|
|
26
|
+
* @returns {string} Markdown
|
|
27
|
+
*/
|
|
28
|
+
export const htmlToMarkdown = (string) => {
|
|
29
|
+
// Normalise text as HTML before converting to Markdown
|
|
30
|
+
string = markdownToHtml(string);
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
codeBlockStyle: "fenced",
|
|
34
|
+
emDelimiter: "*",
|
|
35
|
+
headingStyle: "atx",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const turndownService = new TurndownService(options);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Disable escaping of Markdown characters
|
|
42
|
+
* @param {string} string - String
|
|
43
|
+
* @returns {string} String
|
|
44
|
+
* @see {@link https://github.com/mixmark-io/turndown#escaping-markdown-characters}
|
|
45
|
+
*/
|
|
46
|
+
turndownService.escape = (string) => string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List of inline elements to keep in Markdown
|
|
50
|
+
*/
|
|
51
|
+
turndownService.keep(["cite", "del", "ins"]);
|
|
52
|
+
|
|
53
|
+
const markdown = turndownService.turndown(string);
|
|
54
|
+
|
|
55
|
+
return markdown;
|
|
56
|
+
};
|
package/lib/media.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload attached file(s) via media endpoint
|
|
3
|
+
* @param {string} mediaEndpoint - Media endpoint URL
|
|
4
|
+
* @param {string} token - Bearer token
|
|
5
|
+
* @param {object} properties - JF2 properties
|
|
6
|
+
* @param {object} files - Files to upload
|
|
7
|
+
* @returns {Promise<object>} Uploaded file locations
|
|
8
|
+
*/
|
|
9
|
+
export const uploadMedia = async (mediaEndpoint, token, properties, files) => {
|
|
10
|
+
for await (let [mediaProperty, media] of Object.entries(files)) {
|
|
11
|
+
// Media property may contain one or many media files
|
|
12
|
+
media = Array.isArray(media) ? media : [media];
|
|
13
|
+
|
|
14
|
+
for await (const file of media) {
|
|
15
|
+
const { data, name } = file;
|
|
16
|
+
|
|
17
|
+
// Create multipart/form-data
|
|
18
|
+
const formData = new FormData();
|
|
19
|
+
formData.append("file", new Blob([data]), name);
|
|
20
|
+
|
|
21
|
+
// Upload file via media endpoint
|
|
22
|
+
const response = await fetch(mediaEndpoint, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
authorization: `Bearer ${token}`,
|
|
26
|
+
},
|
|
27
|
+
body: formData,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
/** @type {object} */
|
|
32
|
+
const body = await response.json();
|
|
33
|
+
|
|
34
|
+
const message = body.error_description || response.statusText;
|
|
35
|
+
throw new Error(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Update respective media property with location of upload
|
|
39
|
+
properties[mediaProperty] = properties[mediaProperty] || [];
|
|
40
|
+
properties[mediaProperty].push(response.headers.get("location"));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return properties;
|
|
45
|
+
};
|
package/lib/mf2.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return mf2 properties of a post
|
|
3
|
+
* @param {object} mf2 - mf2 object
|
|
4
|
+
* @param {Array<string>|string} requestedProperties - mf2 properties to select
|
|
5
|
+
* @returns {Promise<object>} mf2 with requested properties
|
|
6
|
+
*/
|
|
7
|
+
export const getMf2Properties = (mf2, requestedProperties) => {
|
|
8
|
+
if (!requestedProperties) {
|
|
9
|
+
return mf2;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const item = mf2.items ? mf2.items[0] : mf2;
|
|
13
|
+
const { properties } = item;
|
|
14
|
+
|
|
15
|
+
// Return requested properties
|
|
16
|
+
if (requestedProperties) {
|
|
17
|
+
requestedProperties = Array.isArray(requestedProperties)
|
|
18
|
+
? requestedProperties
|
|
19
|
+
: [requestedProperties];
|
|
20
|
+
|
|
21
|
+
const selectedProperties = {};
|
|
22
|
+
|
|
23
|
+
for (const key of requestedProperties) {
|
|
24
|
+
if (properties[key]) {
|
|
25
|
+
selectedProperties[key] = properties[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
item.properties = selectedProperties;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return properties
|
|
33
|
+
delete item.type;
|
|
34
|
+
return item;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert JF2 post data to mf2
|
|
39
|
+
* @param {object} postData - Post data
|
|
40
|
+
* @param {boolean} [includeObjectId] - Include ObjectID from post data
|
|
41
|
+
* @returns {object} mf2
|
|
42
|
+
*/
|
|
43
|
+
export const jf2ToMf2 = (postData, includeObjectId = true) => {
|
|
44
|
+
const { properties, _id } = postData;
|
|
45
|
+
|
|
46
|
+
const mf2 = {
|
|
47
|
+
type: [`h-${properties.type}`],
|
|
48
|
+
properties: {
|
|
49
|
+
...(includeObjectId && _id && { uid: [_id] }),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
delete properties.type;
|
|
54
|
+
|
|
55
|
+
// Move values to property object
|
|
56
|
+
for (const key in properties) {
|
|
57
|
+
// Convert nested vocabulary to mf2 (i.e. h-card, h-geo, h-adr)
|
|
58
|
+
if (Object.prototype.hasOwnProperty.call(properties[key], "type")) {
|
|
59
|
+
mf2.properties[key] = [jf2ToMf2({ properties: properties[key] }, false)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Convert values to arrays (i.e. 'a' => ['a'])
|
|
63
|
+
else if (Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
64
|
+
const value = properties[key];
|
|
65
|
+
mf2.properties[key] = Array.isArray(value) ? value : [value];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update key for plaintext content
|
|
70
|
+
if (
|
|
71
|
+
mf2.properties.content &&
|
|
72
|
+
mf2.properties.content[0] &&
|
|
73
|
+
mf2.properties.content[0].text
|
|
74
|
+
) {
|
|
75
|
+
mf2.properties.content[0].value = properties.content.text;
|
|
76
|
+
delete mf2.properties.content[0].text;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return mf2;
|
|
80
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import makeDebug from "debug";
|
|
2
|
+
|
|
3
|
+
import { getPostTemplateProperties } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const debug = makeDebug("indiekit:endpoint-micropub:post-content");
|
|
6
|
+
|
|
7
|
+
export const postContent = {
|
|
8
|
+
/**
|
|
9
|
+
* Create post
|
|
10
|
+
* @param {object} publication - Publication configuration
|
|
11
|
+
* @param {object} postData - Post data
|
|
12
|
+
* @returns {Promise<object>} Response data
|
|
13
|
+
*/
|
|
14
|
+
async create(publication, postData) {
|
|
15
|
+
debug(`Create %O`, { postData });
|
|
16
|
+
|
|
17
|
+
const { postTemplate, store, storeMessageTemplate } = publication;
|
|
18
|
+
const { path, properties } = postData;
|
|
19
|
+
const metaData = {
|
|
20
|
+
action: "create",
|
|
21
|
+
result: "created",
|
|
22
|
+
fileType: "post",
|
|
23
|
+
postType: properties["post-type"],
|
|
24
|
+
};
|
|
25
|
+
const templateProperties = getPostTemplateProperties(properties);
|
|
26
|
+
const content = await postTemplate(templateProperties);
|
|
27
|
+
const message = storeMessageTemplate(metaData);
|
|
28
|
+
|
|
29
|
+
await store.createFile(path, content, { message });
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
location: properties.url,
|
|
33
|
+
status: 202,
|
|
34
|
+
json: {
|
|
35
|
+
success: "create_pending",
|
|
36
|
+
success_description: `Post will be created at ${properties.url}`,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Update post
|
|
43
|
+
* @param {object} publication - Publication configuration
|
|
44
|
+
* @param {object} postData - Post data
|
|
45
|
+
* @param {string} url - Files attached to request
|
|
46
|
+
* @returns {Promise<object>} Response data
|
|
47
|
+
*/
|
|
48
|
+
async update(publication, postData, url) {
|
|
49
|
+
debug(`Update ${url} %O`, { postData });
|
|
50
|
+
|
|
51
|
+
const { postTemplate, store, storeMessageTemplate } = publication;
|
|
52
|
+
const { _originalPath, path, properties } = postData;
|
|
53
|
+
const metaData = {
|
|
54
|
+
action: "update",
|
|
55
|
+
result: "updated",
|
|
56
|
+
fileType: "post",
|
|
57
|
+
postType: properties["post-type"],
|
|
58
|
+
};
|
|
59
|
+
const templateProperties = getPostTemplateProperties(properties);
|
|
60
|
+
const content = await postTemplate(templateProperties);
|
|
61
|
+
const message = storeMessageTemplate(metaData);
|
|
62
|
+
const hasUpdatedUrl = url !== properties.url;
|
|
63
|
+
|
|
64
|
+
_originalPath === path
|
|
65
|
+
? await store.updateFile(path, content, { message })
|
|
66
|
+
: await store.updateFile(_originalPath, content, {
|
|
67
|
+
message,
|
|
68
|
+
newPath: path,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
delete postData._originalPath;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
location: properties.url,
|
|
75
|
+
status: hasUpdatedUrl ? 201 : 200,
|
|
76
|
+
json: {
|
|
77
|
+
success: "update",
|
|
78
|
+
success_description: hasUpdatedUrl
|
|
79
|
+
? `Post updated and moved to ${properties.url}`
|
|
80
|
+
: `Post updated at ${url}`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete post
|
|
87
|
+
* @param {object} publication - Publication configuration
|
|
88
|
+
* @param {object} postData - Post data
|
|
89
|
+
* @returns {Promise<object>} Response data
|
|
90
|
+
*/
|
|
91
|
+
async delete(publication, postData) {
|
|
92
|
+
debug(`Delete %O`, { postData });
|
|
93
|
+
|
|
94
|
+
const { store, storeMessageTemplate } = publication;
|
|
95
|
+
const { path, properties } = postData;
|
|
96
|
+
const metaData = {
|
|
97
|
+
action: "delete",
|
|
98
|
+
result: "deleted",
|
|
99
|
+
fileType: "post",
|
|
100
|
+
postType: properties["post-type"],
|
|
101
|
+
};
|
|
102
|
+
const message = storeMessageTemplate(metaData);
|
|
103
|
+
|
|
104
|
+
await store.deleteFile(path, { message });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
status: 200,
|
|
108
|
+
json: {
|
|
109
|
+
success: "delete",
|
|
110
|
+
success_description: `Post deleted from ${properties.url}`,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Undelete post
|
|
117
|
+
* @param {object} publication - Publication configuration
|
|
118
|
+
* @param {object} postData - Post data
|
|
119
|
+
* @returns {Promise<object>} Response data
|
|
120
|
+
*/
|
|
121
|
+
async undelete(publication, postData) {
|
|
122
|
+
debug(`Undelete %O`, { postData });
|
|
123
|
+
|
|
124
|
+
const { postTemplate, store, storeMessageTemplate } = publication;
|
|
125
|
+
const { path, properties } = postData;
|
|
126
|
+
const metaData = {
|
|
127
|
+
action: "undelete",
|
|
128
|
+
result: "undeleted",
|
|
129
|
+
fileType: "post",
|
|
130
|
+
postType: properties["post-type"],
|
|
131
|
+
};
|
|
132
|
+
const templateProperties = getPostTemplateProperties(properties);
|
|
133
|
+
const content = await postTemplate(templateProperties);
|
|
134
|
+
const message = storeMessageTemplate(metaData);
|
|
135
|
+
|
|
136
|
+
await store.createFile(path, content, { message });
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
location: properties.url,
|
|
140
|
+
status: 200,
|
|
141
|
+
json: {
|
|
142
|
+
success: "delete_undelete",
|
|
143
|
+
success_description: `Post restored to ${properties.url}`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|