notes-to-strapi-export-article-ai 1.0.11 → 1.0.13

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.
@@ -0,0 +1,426 @@
1
+ import { App, MarkdownView, Notice, TFile, TFolder } from 'obsidian'
2
+ import { OpenAI } from 'openai'
3
+ import { StrapiExporterSettings } from '../types/settings'
4
+ import {
5
+ uploadGalleryImagesToStrapi,
6
+ uploadImagesToStrapi,
7
+ } from './strapi-uploader'
8
+ import { generateArticleContent, getImageDescription } from './openai-generator'
9
+ import { ImageBlob } from '../types/image'
10
+
11
+ /**
12
+ * Process the markdown content
13
+ * @param app
14
+ * @param settings
15
+ * @param useAdditionalCallAPI
16
+ */
17
+ export async function processMarkdownContent(
18
+ app: App,
19
+ settings: StrapiExporterSettings,
20
+ useAdditionalCallAPI = false
21
+ ) {
22
+ const activeView = app.workspace.getActiveViewOfType(MarkdownView)
23
+ if (!activeView) {
24
+ new Notice('No active Markdown view')
25
+ return
26
+ }
27
+
28
+ /** ****************************************************************************
29
+ * Check if all the settings are configured
30
+ * *****************************************************************************
31
+ */
32
+ if (!settings.strapiUrl || !settings.strapiApiToken) {
33
+ new Notice(
34
+ 'Please configure Strapi URL and API token in the plugin settings'
35
+ )
36
+ return
37
+ }
38
+
39
+ if (!settings.openaiApiKey) {
40
+ new Notice('Please configure OpenAI API key in the plugin settings')
41
+ return
42
+ }
43
+
44
+ if (useAdditionalCallAPI) {
45
+ if (!settings.additionalJsonTemplate) {
46
+ new Notice(
47
+ 'Please configure the additional call api JSON template in the plugin settings'
48
+ )
49
+ return
50
+ }
51
+
52
+ if (!settings.additionalJsonTemplateDescription) {
53
+ new Notice(
54
+ 'Please configure the additional call api JSON template description in the plugin settings'
55
+ )
56
+ return
57
+ }
58
+
59
+ if (!settings.additionalUrl) {
60
+ new Notice(
61
+ 'Please configure the additional call api URL in the plugin settings'
62
+ )
63
+ return
64
+ }
65
+
66
+ if (!settings.additionalContentAttributeName) {
67
+ new Notice(
68
+ 'Please configure the additional call api content attribute name in the plugin settings'
69
+ )
70
+ return
71
+ }
72
+ } else {
73
+ if (!settings.jsonTemplate) {
74
+ new Notice('Please configure JSON template in the plugin settings')
75
+ return
76
+ }
77
+
78
+ if (!settings.jsonTemplateDescription) {
79
+ new Notice(
80
+ 'Please configure JSON template description in the plugin settings'
81
+ )
82
+ return
83
+ }
84
+
85
+ if (!settings.strapiArticleCreateUrl) {
86
+ new Notice(
87
+ 'Please configure Strapi article create URL in the plugin settings'
88
+ )
89
+ return
90
+ }
91
+
92
+ if (!settings.strapiContentAttributeName) {
93
+ new Notice(
94
+ 'Please configure Strapi content attribute name in the plugin settings'
95
+ )
96
+ return
97
+ }
98
+ }
99
+
100
+ new Notice('All settings are ok, processing Markdown content...')
101
+
102
+ // Initialize OpenAI API
103
+ const openai = new OpenAI({
104
+ apiKey: settings.openaiApiKey,
105
+ dangerouslyAllowBrowser: true,
106
+ })
107
+
108
+ /** ****************************************************************************
109
+ * Process the markdown content
110
+ * *****************************************************************************
111
+ */
112
+ const file = activeView.file
113
+ let content = ''
114
+ if (!file) {
115
+ new Notice('No file found in active view...')
116
+ return
117
+ }
118
+
119
+ /** ****************************************************************************
120
+ * Check if the content has any images to process
121
+ * *****************************************************************************
122
+ */
123
+ let imageBlob: ImageBlob | null = null
124
+ let galleryUploadedImageIds: number[] = []
125
+ const articleFolderPath = file.parent?.path
126
+ const imageFolderPath = `${articleFolderPath}/image`
127
+ const galleryFolderPath = `${articleFolderPath}/gallery`
128
+ const imageMetadataFile = `${articleFolderPath}/image/metadata.json`
129
+ const galleryMetadataFile = `${articleFolderPath}/gallery/metadata.json`
130
+
131
+ // Check if metadata files exist
132
+ if (await app.vault.adapter.exists(imageMetadataFile)) {
133
+ const imageMetadata = JSON.parse(
134
+ await app.vault.adapter.read(imageMetadataFile)
135
+ )
136
+ if (Object.keys(imageMetadata).length > 0) {
137
+ imageBlob = {
138
+ path: imageMetadata[Object.keys(imageMetadata)[0]].data.url,
139
+ blob: new Blob(),
140
+ name: Object.keys(imageMetadata)[0],
141
+ id: imageMetadata[Object.keys(imageMetadata)[0]].id,
142
+ }
143
+ }
144
+ }
145
+
146
+ if (await app.vault.adapter.exists(galleryMetadataFile)) {
147
+ const galleryMetadata = JSON.parse(
148
+ await app.vault.adapter.read(galleryMetadataFile)
149
+ )
150
+ if (Object.keys(galleryMetadata).length > 0) {
151
+ galleryUploadedImageIds = Object.values(galleryMetadata).map(
152
+ (item: any) => item.data.id
153
+ )
154
+ }
155
+ }
156
+
157
+ // If metadata doesn't exist, get image blobs and upload to Strapi
158
+ if (!imageBlob) {
159
+ imageBlob = await getImageBlob(app, imageFolderPath)
160
+ if (imageBlob) {
161
+ const imageDescription = await getImageDescription(imageBlob.blob, openai)
162
+ const uploadedImage: any = await uploadImagesToStrapi(
163
+ [{ ...imageBlob, description: imageDescription }],
164
+ settings,
165
+ app,
166
+ imageFolderPath
167
+ )
168
+ imageBlob.path = uploadedImage[imageBlob.name].url
169
+ imageBlob.id = uploadedImage[imageBlob.name].id
170
+ }
171
+ }
172
+
173
+ if (galleryUploadedImageIds.length === 0) {
174
+ const galleryImageBlobs = await getGalleryImageBlobs(app, galleryFolderPath)
175
+ galleryUploadedImageIds = await uploadGalleryImagesToStrapi(
176
+ galleryImageBlobs,
177
+ settings,
178
+ app,
179
+ galleryFolderPath
180
+ )
181
+ }
182
+
183
+ content = await app.vault.read(file)
184
+
185
+ const flag = hasUnexportedImages(content)
186
+
187
+ /** ****************************************************************************
188
+ * Process the images
189
+ * *****************************************************************************
190
+ */
191
+ if (flag) {
192
+ const imagePaths = extractImagePaths(content)
193
+ const imageBlobs = await getImageBlobs(app, imagePaths)
194
+
195
+ new Notice('Getting image descriptions...')
196
+ const imageDescriptions = await Promise.all(
197
+ imageBlobs.map(async imageBlob => {
198
+ const description = await getImageDescription(imageBlob.blob, openai)
199
+ return {
200
+ blob: imageBlob.blob,
201
+ name: imageBlob.name,
202
+ path: imageBlob.path,
203
+ description,
204
+ }
205
+ })
206
+ )
207
+
208
+ new Notice('Uploading images to Strapi...')
209
+ const uploadedImages = await uploadImagesToStrapi(
210
+ imageDescriptions,
211
+ settings
212
+ )
213
+
214
+ new Notice('Replacing image paths...')
215
+ content = replaceImagePaths(content, uploadedImages)
216
+ await app.vault.modify(file, content)
217
+ new Notice('Images uploaded and links updated successfully!')
218
+ } else {
219
+ new Notice(
220
+ 'No local images found in the content... Skip the image processing...'
221
+ )
222
+ }
223
+
224
+ /** ****************************************************************************
225
+ * Generate article content
226
+ * *****************************************************************************
227
+ */
228
+ new Notice('Generating article content...')
229
+ const articleContent = await generateArticleContent(
230
+ content,
231
+ openai,
232
+ settings,
233
+ useAdditionalCallAPI
234
+ )
235
+
236
+ /** ****************************************************************************
237
+ * Add the content, image, and gallery to the article content based on the settings
238
+ * *****************************************************************************
239
+ */
240
+ const imageFullPathProperty = useAdditionalCallAPI
241
+ ? settings.additionalImageFullPathProperty
242
+ : settings.mainImageFullPathProperty
243
+ const galleryFullPathProperty = useAdditionalCallAPI
244
+ ? settings.additionalGalleryFullPathProperty
245
+ : settings.mainGalleryFullPathProperty
246
+
247
+ articleContent.data = {
248
+ ...articleContent.data,
249
+ ...(imageBlob &&
250
+ imageFullPathProperty && { [imageFullPathProperty]: imageBlob.id }),
251
+ ...(galleryUploadedImageIds.length > 0 &&
252
+ galleryFullPathProperty && {
253
+ [galleryFullPathProperty]: galleryUploadedImageIds,
254
+ }),
255
+ }
256
+
257
+ new Notice('Article content generated successfully!')
258
+ try {
259
+ const response = await fetch(
260
+ useAdditionalCallAPI
261
+ ? settings.additionalUrl
262
+ : settings.strapiArticleCreateUrl,
263
+ {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ Authorization: `Bearer ${settings.strapiApiToken}`,
268
+ },
269
+ body: JSON.stringify(articleContent),
270
+ }
271
+ )
272
+
273
+ if (response.ok) {
274
+ new Notice(
275
+ 'Check your API content now, the article is created & uploaded! 🎉'
276
+ )
277
+ } else {
278
+ new Notice('Failed to create article in Strapi.')
279
+ }
280
+ } catch (error) {
281
+ new Notice('Error creating article in Strapi.')
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Extract image paths from the markdown content
287
+ * @param content
288
+ */
289
+ export function extractImagePaths(content: string): string[] {
290
+ const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
291
+ const imagePaths: string[] = []
292
+ let match
293
+
294
+ while ((match = imageRegex.exec(content)) !== null) {
295
+ imagePaths.push(match[1])
296
+ }
297
+
298
+ return imagePaths
299
+ }
300
+
301
+ /**
302
+ * Check if the markdown content has unexported images
303
+ * @param content
304
+ */
305
+ export function hasUnexportedImages(content: string): boolean {
306
+ const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
307
+ return imageRegex.test(content)
308
+ }
309
+
310
+ /**
311
+ * Get the image blobs from the image paths
312
+ * @param app
313
+ * @param imagePaths
314
+ */
315
+ export async function getImageBlobs(
316
+ app: App,
317
+ imagePaths: string[]
318
+ ): Promise<{ path: string; blob: Blob; name: string }[]> {
319
+ const files = app.vault.getAllLoadedFiles()
320
+ const fileNames = files.map(file => file.name)
321
+ const imageFiles = imagePaths.filter(path => fileNames.includes(path))
322
+ return await Promise.all(
323
+ imageFiles.map(async path => {
324
+ const file = files.find(file => file.name === path)
325
+ if (file instanceof TFile) {
326
+ const blob = await app.vault.readBinary(file)
327
+ return {
328
+ name: path,
329
+ blob: new Blob([blob], { type: 'image/png' }),
330
+ path: file.path,
331
+ }
332
+ }
333
+ return {
334
+ name: '',
335
+ blob: new Blob(),
336
+ path: '',
337
+ }
338
+ })
339
+ )
340
+ }
341
+
342
+ /**
343
+ * Get the image blob from the image path
344
+ * @param app
345
+ * @param imageFolderPath
346
+ */
347
+ export async function getImageBlob(
348
+ app: App,
349
+ imageFolderPath: string
350
+ ): Promise<{ path: string; blob: Blob; name: string } | null> {
351
+ const folder = app.vault.getAbstractFileByPath(imageFolderPath)
352
+ if (folder instanceof TFolder) {
353
+ const files = folder.children.filter(
354
+ file =>
355
+ file instanceof TFile &&
356
+ file.extension.match(/^(jpg|jpeg|png|gif|bmp|webp)$/i)
357
+ )
358
+ if (files.length > 0) {
359
+ const file = files[0] as TFile
360
+ const blob = await app.vault.readBinary(file)
361
+ return {
362
+ name: file.name,
363
+ blob: new Blob([blob], { type: 'image/png' }),
364
+ path: file.path,
365
+ }
366
+ }
367
+ } else {
368
+ new Notice(
369
+ 'Image folder not found. Please create an "image" folder next to your article file.'
370
+ )
371
+ }
372
+ return null
373
+ }
374
+
375
+ /**
376
+ * Get the gallery image blobs from the folder path
377
+ * @param app
378
+ * @param galleryFolderPath
379
+ */
380
+ export async function getGalleryImageBlobs(
381
+ app: App,
382
+ galleryFolderPath: string
383
+ ): Promise<{ path: string; blob: Blob; name: string }[]> {
384
+ const folder = app.vault.getAbstractFileByPath(galleryFolderPath)
385
+ if (folder instanceof TFolder) {
386
+ const files = folder.children.filter(
387
+ file =>
388
+ file instanceof TFile &&
389
+ file.extension.match(/^(jpg|jpeg|png|gif|bmp|webp)$/i)
390
+ )
391
+ return Promise.all(
392
+ files.map(async file => {
393
+ const blob = await app.vault.readBinary(file as TFile)
394
+ return {
395
+ name: file.name,
396
+ blob: new Blob([blob], { type: 'image/png' }),
397
+ path: file.path,
398
+ }
399
+ })
400
+ )
401
+ } else {
402
+ new Notice(
403
+ 'Gallery folder not found. Please create a "gallery" folder next to your article file.'
404
+ )
405
+ }
406
+ return []
407
+ }
408
+
409
+ /**
410
+ * Replace the image paths in the content with the uploaded images
411
+ * @param content
412
+ * @param uploadedImages
413
+ */
414
+ export function replaceImagePaths(
415
+ content: string,
416
+ uploadedImages: { [key: string]: { url: string; data: any } }
417
+ ): string {
418
+ for (const [localPath, imageData] of Object.entries(uploadedImages)) {
419
+ const markdownImageRegex = new RegExp(`!\\[\\[${localPath}\\]\\]`, 'g')
420
+ content = content.replace(
421
+ markdownImageRegex,
422
+ `![${imageData.data.alternativeText}](${imageData.url})`
423
+ )
424
+ }
425
+ return content
426
+ }
@@ -0,0 +1,139 @@
1
+ import { OpenAI } from 'openai'
2
+ import { StrapiExporterSettings } from '../types/settings'
3
+ import { ArticleContent } from '../types/article'
4
+ import { Notice } from 'obsidian'
5
+
6
+ /**
7
+ * Generate article content using OpenAI
8
+ * @param content
9
+ * @param openai
10
+ * @param settings
11
+ * @param useAdditionalCallAPI
12
+ */
13
+ export async function generateArticleContent(
14
+ content: string,
15
+ openai: OpenAI,
16
+ settings: StrapiExporterSettings,
17
+ useAdditionalCallAPI = false
18
+ ): Promise<ArticleContent> {
19
+ let jsonTemplate: any
20
+ let jsonTemplateDescription: any
21
+ let contentAttributeName: string
22
+
23
+ if (useAdditionalCallAPI) {
24
+ jsonTemplate = JSON.parse(settings.additionalJsonTemplate)
25
+ jsonTemplateDescription = JSON.parse(
26
+ settings.additionalJsonTemplateDescription
27
+ )
28
+ contentAttributeName = settings.additionalContentAttributeName
29
+ } else {
30
+ jsonTemplate = JSON.parse(settings.jsonTemplate)
31
+ jsonTemplateDescription = JSON.parse(settings.jsonTemplateDescription)
32
+ contentAttributeName = settings.strapiContentAttributeName
33
+ }
34
+
35
+ const articlePrompt = `You are an SEO expert. Generate an article based on the following template and field descriptions:
36
+
37
+ Template:
38
+ ${JSON.stringify(jsonTemplate, null, 2)}
39
+
40
+ Field Descriptions:
41
+ ${JSON.stringify(jsonTemplateDescription, null, 2)}
42
+
43
+ The main content of the article should be based on the following text and all the keywords around the domain of the text:
44
+ ----- CONTENT -----
45
+ ${content.substring(0, 500)}
46
+ ----- END CONTENT -----
47
+
48
+ Please provide the generated article content as a JSON object following the given template structure.
49
+
50
+ ${settings.additionalPrompt ? `Additional Prompt: ${settings.additionalPrompt}` : ''}`
51
+
52
+ const completion = await openai.chat.completions.create({
53
+ model: 'gpt-3.5-turbo-0125',
54
+ messages: [
55
+ {
56
+ role: 'user',
57
+ content: articlePrompt,
58
+ },
59
+ ],
60
+ max_tokens: 2000,
61
+ n: 1,
62
+ stop: null,
63
+ })
64
+
65
+ let articleContent = JSON.parse(completion.choices[0].message.content ?? '{}')
66
+ articleContent = {
67
+ data: {
68
+ ...articleContent.data,
69
+ [contentAttributeName]: content,
70
+ },
71
+ }
72
+
73
+ return articleContent
74
+ }
75
+
76
+ /**
77
+ * Get the description of the image using OpenAI
78
+ * @param imageBlob
79
+ * @param openai
80
+ */
81
+ export const getImageDescription = async (imageBlob: Blob, openai: OpenAI) => {
82
+ // Get the image description using the OpenAI API (using gpt 4 vision preview model)
83
+ // @ts-ignore
84
+ const response = await openai.chat.completions.create({
85
+ model: 'gpt-4-vision-preview',
86
+ messages: [
87
+ {
88
+ role: 'user',
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: `What's in this image? make it simple, i just want the context and an idea(think about alt text)`,
93
+ },
94
+ {
95
+ type: 'image_url',
96
+ // Encode imageBlob as base64
97
+ // @ts-ignore
98
+ image_url: `data:image/png;base64,${btoa(
99
+ new Uint8Array(await imageBlob.arrayBuffer()).reduce(
100
+ (data, byte) => data + String.fromCharCode(byte),
101
+ ''
102
+ )
103
+ )}`,
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ })
109
+
110
+ new Notice(response.choices[0].message.content ?? 'no response content...')
111
+ new Notice(
112
+ `prompt_tokens: ${response.usage?.prompt_tokens} // completion_tokens: ${response.usage?.completion_tokens} // total_tokens: ${response.usage?.total_tokens}`
113
+ )
114
+
115
+ // gpt-3.5-turbo-0125
116
+ // Generate alt text, caption, and title for the image, based on the description of the image
117
+ const completion = await openai.chat.completions.create({
118
+ model: 'gpt-3.5-turbo-0125',
119
+ messages: [
120
+ {
121
+ role: 'user',
122
+ content: `You are an SEO expert and you are writing alt text, caption, and title for this image. The description of the image is: ${response.choices[0].message.content}.
123
+ Give me a title (name) for this image, an SEO-friendly alternative text, and a caption for this image.
124
+ Generate this information and respond with a JSON object using the following fields: name, alternativeText, caption.
125
+ Use this JSON template: {"name": "string", "alternativeText": "string", "caption": "string"}.`,
126
+ },
127
+ ],
128
+ max_tokens: 750,
129
+ n: 1,
130
+ stop: null,
131
+ })
132
+
133
+ new Notice(completion.choices[0].message.content ?? 'no response content...')
134
+ new Notice(
135
+ `prompt_tokens: ${completion.usage?.prompt_tokens} // completion_tokens: ${completion.usage?.completion_tokens} // total_tokens: ${completion.usage?.total_tokens}`
136
+ )
137
+
138
+ return JSON.parse(completion.choices[0].message.content?.trim() || '{}')
139
+ }
@@ -0,0 +1,127 @@
1
+ import { Notice } from 'obsidian'
2
+ import { StrapiExporterSettings } from '../types/settings'
3
+ import { ImageBlob, ImageDescription } from '../types/image'
4
+
5
+ /**
6
+ * Upload images to Strapi
7
+ * @param imageDescriptions
8
+ * @param settings
9
+ * @param app
10
+ * @param imageFolderPath
11
+ */
12
+ export async function uploadImagesToStrapi(
13
+ imageDescriptions: ImageDescription[],
14
+ settings: StrapiExporterSettings,
15
+ app: any = null,
16
+ imageFolderPath: string = ''
17
+ ): Promise<{ [key: string]: { url: string; data: any } }> {
18
+ const uploadedImages: { [key: string]: { url: string; data: any; id: any } } =
19
+ {}
20
+
21
+ for (const imageDescription of imageDescriptions) {
22
+ const formData = new FormData()
23
+ formData.append('files', imageDescription.blob, imageDescription.name)
24
+ formData.append(
25
+ 'fileInfo',
26
+ JSON.stringify({
27
+ name: imageDescription.description.name,
28
+ alternativeText: imageDescription.description.alternativeText,
29
+ caption: imageDescription.description.caption,
30
+ })
31
+ )
32
+
33
+ try {
34
+ const response = await fetch(`${settings.strapiUrl}/api/upload`, {
35
+ method: 'POST',
36
+ headers: {
37
+ Authorization: `Bearer ${settings.strapiApiToken}`,
38
+ },
39
+ body: formData,
40
+ })
41
+
42
+ if (response.ok) {
43
+ const data = await response.json()
44
+ uploadedImages[imageDescription.name] = {
45
+ url: data[0].url,
46
+ data: data[0],
47
+ id: data[0].id,
48
+ }
49
+ } else {
50
+ new Notice(`Failed to upload image: ${imageDescription.name}`)
51
+ }
52
+ } catch (error) {
53
+ new Notice(`Error uploading image: ${imageDescription.name}`)
54
+ }
55
+ }
56
+
57
+ if (imageFolderPath && app) {
58
+ // Save metadata to a file only if there are uploaded images
59
+ if (Object.keys(uploadedImages).length > 0) {
60
+ const metadataFile = `${imageFolderPath}/metadata.json`
61
+ await app.vault.adapter.write(
62
+ metadataFile,
63
+ JSON.stringify(uploadedImages)
64
+ )
65
+ }
66
+ }
67
+ return uploadedImages
68
+ }
69
+
70
+ /**
71
+ * Upload gallery images to Strapi
72
+ * @param imageBlobs
73
+ * @param settings
74
+ * @param app
75
+ * @param galleryFolderPath
76
+ */
77
+ export async function uploadGalleryImagesToStrapi(
78
+ imageBlobs: ImageBlob[],
79
+ settings: StrapiExporterSettings,
80
+ app: any = null,
81
+ galleryFolderPath: string = ''
82
+ ): Promise<number[]> {
83
+ const uploadedImageIds: number[] = []
84
+ const uploadedImages: { [key: string]: { url: string; data: any; id: any } } =
85
+ {}
86
+
87
+ for (const imageBlob of imageBlobs) {
88
+ const formData = new FormData()
89
+ formData.append('files', imageBlob.blob, imageBlob.name)
90
+
91
+ try {
92
+ const response = await fetch(`${settings.strapiUrl}/api/upload`, {
93
+ method: 'POST',
94
+ headers: {
95
+ Authorization: `Bearer ${settings.strapiApiToken}`,
96
+ },
97
+ body: formData,
98
+ })
99
+
100
+ if (response.ok) {
101
+ const data = await response.json()
102
+ uploadedImages[imageBlob.name] = {
103
+ url: data[0].url,
104
+ id: data[0].id,
105
+ data: data[0],
106
+ }
107
+ } else {
108
+ new Notice(`Failed to upload gallery image: ${imageBlob.name}`)
109
+ }
110
+ } catch (error) {
111
+ new Notice(`Error uploading gallery image: ${imageBlob.name}`)
112
+ }
113
+ }
114
+
115
+ if (galleryFolderPath && app) {
116
+ // Save metadata to a file only if there are uploaded images
117
+ if (Object.keys(uploadedImages).length > 0) {
118
+ const metadataFile = `${galleryFolderPath}/metadata.json`
119
+ await app.vault.adapter.write(
120
+ metadataFile,
121
+ JSON.stringify(uploadedImages)
122
+ )
123
+ }
124
+ }
125
+
126
+ return uploadedImageIds
127
+ }
@@ -0,0 +1,8 @@
1
+ export const validateJsonTemplate = (jsonString: string): boolean => {
2
+ try {
3
+ JSON.parse(jsonString)
4
+ return true
5
+ } catch (error) {
6
+ return false
7
+ }
8
+ }
package/versions.json CHANGED
@@ -7,5 +7,7 @@
7
7
  "1.0.8": "1.5.0",
8
8
  "1.0.9": "1.5.0",
9
9
  "1.0.10": "1.5.0",
10
- "1.0.11": "1.5.0"
10
+ "1.0.11": "1.5.0",
11
+ "1.0.12": "1.5.0",
12
+ "1.0.13": "1.5.0"
11
13
  }