notes-to-strapi-export-article-ai 1.0.119 → 3.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 (46) hide show
  1. package/.eslintrc +30 -22
  2. package/README.md +98 -143
  3. package/images/img.png +0 -0
  4. package/images/img_1.png +0 -0
  5. package/images/img_10.png +0 -0
  6. package/images/img_11.png +0 -0
  7. package/images/img_12.png +0 -0
  8. package/images/img_13.png +0 -0
  9. package/images/img_2.png +0 -0
  10. package/images/img_3.png +0 -0
  11. package/images/img_4.png +0 -0
  12. package/images/img_5.png +0 -0
  13. package/images/img_6.png +0 -0
  14. package/images/img_7.png +0 -0
  15. package/images/img_8.png +0 -0
  16. package/images/img_9.png +0 -0
  17. package/manifest.json +2 -2
  18. package/package.json +29 -26
  19. package/src/components/APIKeys.ts +219 -0
  20. package/src/components/Configuration.ts +663 -0
  21. package/src/components/Dashboard.ts +184 -0
  22. package/src/components/ImageSelectionModal.ts +58 -0
  23. package/src/components/Routes.ts +279 -0
  24. package/src/constants.ts +22 -61
  25. package/src/main.ts +177 -34
  26. package/src/services/configuration-generator.ts +172 -0
  27. package/src/services/field-analyzer.ts +84 -0
  28. package/src/services/frontmatter.ts +329 -0
  29. package/src/services/strapi-export.ts +436 -0
  30. package/src/settings/UnifiedSettingsTab.ts +206 -0
  31. package/src/types/image.ts +27 -16
  32. package/src/types/index.ts +3 -0
  33. package/src/types/route.ts +51 -0
  34. package/src/types/settings.ts +22 -23
  35. package/src/utils/analyse-file.ts +94 -0
  36. package/src/utils/debounce.ts +34 -0
  37. package/src/utils/image-processor.ts +124 -400
  38. package/src/utils/preview-modal.ts +265 -0
  39. package/src/utils/process-file.ts +122 -0
  40. package/src/utils/strapi-uploader.ts +120 -119
  41. package/src/settings.ts +0 -404
  42. package/src/types/article.ts +0 -8
  43. package/src/utils/openai-generator.ts +0 -139
  44. package/src/utils/validators.ts +0 -8
  45. package/version-bump.mjs +0 -14
  46. package/versions.json +0 -119
@@ -1,442 +1,166 @@
1
- import { App, MarkdownView, Notice, TFile, TFolder } from 'obsidian'
2
- import { OpenAI } from 'openai'
3
- import { StrapiExporterSettings } from '../types/settings'
1
+ import { App, TFile } from 'obsidian'
4
2
  import {
5
- uploadGalleryImagesToStrapi,
6
- uploadImagesToStrapi,
7
- } from './strapi-uploader'
8
- import { generateArticleContent, getImageDescription } from './openai-generator'
9
- import { ImageBlob } from '../types/image'
3
+ ImageDescription,
4
+ ImageProcessingResult,
5
+ StrapiExporterSettings,
6
+ } from '../types'
7
+ import { uploadImageToStrapi } from './strapi-uploader'
10
8
 
11
9
  /**
12
- * Process the markdown content
13
- * @param app
14
- * @param settings
15
- * @param useAdditionalCallAPI
10
+ * Process images in markdown content and handle uploads to Strapi
16
11
  */
17
- export async function processMarkdownContent(
12
+ export async function processImages(
18
13
  app: App,
19
14
  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
- }
15
+ content: string
16
+ ): Promise<ImageProcessingResult> {
17
+ try {
18
+ const imageRefs = extractImageReferences(content)
27
19
 
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'
20
+ const processedImages = await processImageReferences(
21
+ app,
22
+ settings,
23
+ imageRefs
35
24
  )
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
25
+ const updatedContent = updateImageReferences(content, processedImages)
26
+ return {
27
+ content: updatedContent,
28
+ processedImages,
57
29
  }
30
+ } catch (error) {
31
+ throw new Error(`Image processing failed: ${error.message}`)
32
+ }
33
+ }
58
34
 
59
- if (!settings.additionalUrl) {
60
- new Notice(
61
- 'Please configure the additional call api URL in the plugin settings'
62
- )
63
- return
64
- }
35
+ interface ImageReference {
36
+ fullMatch: string
37
+ path: string
38
+ altText: string
39
+ type: 'wikilink' | 'markdown'
40
+ }
65
41
 
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
- }
42
+ /**
43
+ * Extract image references from content
44
+ */
45
+ function extractImageReferences(content: string): ImageReference[] {
46
+ const references: ImageReference[] = []
77
47
 
78
- if (!settings.jsonTemplateDescription) {
79
- new Notice(
80
- 'Please configure JSON template description in the plugin settings'
81
- )
82
- return
48
+ try {
49
+ // Wiki-style image links (![[image.png]])
50
+ const wikiLinkRegex = /!\[\[([^\]]+\.(png|jpe?g|gif|svg|bmp|webp))\]\]/gi
51
+ let match
52
+
53
+ while ((match = wikiLinkRegex.exec(content)) !== null) {
54
+ references.push({
55
+ fullMatch: match[0],
56
+ path: match[1],
57
+ altText: '',
58
+ type: 'wikilink',
59
+ })
83
60
  }
84
61
 
85
- if (!settings.strapiArticleCreateUrl) {
86
- new Notice(
87
- 'Please configure Strapi article create URL in the plugin settings'
88
- )
89
- return
62
+ // Markdown-style image links (![alt](path.png))
63
+ const markdownLinkRegex =
64
+ /!\[([^\]]*)\]\(([^)]+\.(png|jpe?g|gif|svg|bmp|webp))\)/gi
65
+ while ((match = markdownLinkRegex.exec(content)) !== null) {
66
+ references.push({
67
+ fullMatch: match[0],
68
+ path: match[2],
69
+ altText: match[1],
70
+ type: 'markdown',
71
+ })
90
72
  }
91
73
 
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
74
+ return references
75
+ } catch (error) {
76
+ throw new Error(`Failed to extract image references: ${error.message}`)
117
77
  }
78
+ }
118
79
 
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,
80
+ /**
81
+ * Process image references and upload to Strapi
82
+ */
83
+ async function processImageReferences(
84
+ app: App,
85
+ settings: StrapiExporterSettings,
86
+ references: ImageReference[]
87
+ ): Promise<ImageDescription[]> {
88
+ const processedImages: ImageDescription[] = []
89
+
90
+ for (const ref of references) {
91
+ try {
92
+ if (isExternalUrl(ref.path)) {
93
+ processedImages.push({
94
+ url: ref.path,
95
+ name: ref.path,
96
+ path: ref.path,
97
+ description: {
98
+ name: ref.path,
99
+ alternativeText: ref.altText,
100
+ caption: ref.altText,
101
+ },
102
+ })
103
+ continue
142
104
  }
143
- }
144
- }
145
105
 
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
- }
106
+ const file = app.vault.getAbstractFileByPath(ref.path)
107
+ if (!(file instanceof TFile)) {
108
+ continue
109
+ }
156
110
 
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 }],
111
+ const uploadResult = await uploadImageToStrapi(
112
+ file,
113
+ file.name,
164
114
  settings,
165
115
  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,
116
+ {
117
+ alternativeText: ref.altText || file.basename,
118
+ caption: ref.altText || file.basename,
204
119
  }
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
- }
120
+ )
256
121
 
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),
122
+ if (uploadResult) {
123
+ processedImages.push(uploadResult)
270
124
  }
271
- )
272
-
273
- if (response.ok) {
274
- new Notice(
275
- 'Check your API content now, the article is created & uploaded! 🎉'
276
- )
277
- } else {
278
- const errorData = await response.json()
279
- new Notice(
280
- `Failed to create article in Strapi. Error: ${errorData.error.message}`
281
- )
125
+ } catch (error) {
126
+ throw new Error(`Failed to process image ${ref.path}: ${error.message}`)
282
127
  }
283
- } catch (error) {
284
- new Notice(`Error creating article in Strapi. Error: ${error.message}`)
285
128
  }
286
- }
287
-
288
- /**
289
- * Extract image paths from the markdown content
290
- * @param content
291
- */
292
- export function extractImagePaths(content: string): string[] {
293
- const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
294
- const imagePaths: string[] = []
295
- let match
296
-
297
- while ((match = imageRegex.exec(content)) !== null) {
298
- imagePaths.push(match[1])
299
- }
300
-
301
- return imagePaths
302
- }
303
-
304
- /**
305
- * Check if the markdown content has unexported images
306
- * @param content
307
- */
308
- export function hasUnexportedImages(content: string): boolean {
309
- const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
310
- return imageRegex.test(content)
311
- }
312
129
 
313
- /**
314
- * Get the image blobs from the image paths
315
- * @param app
316
- * @param imagePaths
317
- */
318
- export async function getImageBlobs(
319
- app: App,
320
- imagePaths: string[]
321
- ): Promise<{ path: string; blob: Blob; name: string }[]> {
322
- const files = app.vault.getAllLoadedFiles()
323
- const fileNames = files.map(file => file.name)
324
- const imageFiles = imagePaths.filter(path => fileNames.includes(path))
325
- return await Promise.all(
326
- imageFiles.map(async path => {
327
- const file = files.find(file => file.name === path)
328
- if (file instanceof TFile) {
329
- const blob = await app.vault.readBinary(file)
330
- return {
331
- name: path,
332
- blob: new Blob([blob], { type: 'image/png' }),
333
- path: file.path,
334
- }
335
- }
336
- return {
337
- name: '',
338
- blob: new Blob(),
339
- path: '',
340
- }
341
- })
342
- )
130
+ return processedImages
343
131
  }
344
132
 
345
133
  /**
346
- * Get the image blob from the image path
347
- * @param app
348
- * @param imageFolderPath
134
+ * Update image references in content with uploaded URLs
349
135
  */
350
- export async function getImageBlob(
351
- app: App,
352
- imageFolderPath: string
353
- ): Promise<{ path: string; blob: Blob; name: string } | null> {
354
- const folder = app.vault.getAbstractFileByPath(imageFolderPath)
355
- if (folder instanceof TFolder) {
356
- const files = folder.children.filter(
357
- file =>
358
- file instanceof TFile &&
359
- file.extension.match(/^(jpg|jpeg|png|gif|bmp|webp)$/i)
360
- )
361
- if (files.length > 0) {
362
- // check the TFILE type, and not cast it to TFILE
363
- if (!(files[0] instanceof TFile)) {
364
- return null
365
- }
366
- const file = files[0]
367
-
368
- const blob = await app.vault.readBinary(file)
369
- return {
370
- name: file.name,
371
- blob: new Blob([blob], { type: 'image/png' }),
372
- path: file.path,
136
+ function updateImageReferences(
137
+ content: string,
138
+ processedImages: ImageDescription[]
139
+ ): string {
140
+ try {
141
+ let updatedContent = content
142
+ for (const image of processedImages) {
143
+ if (image.url) {
144
+ const imageRegex = new RegExp(
145
+ `!\\[([^\\]]*)\\]\\(${image.path}\\)|!\\[\\[${image.path}\\]\\]`,
146
+ 'g'
147
+ )
148
+ updatedContent = updatedContent.replace(
149
+ imageRegex,
150
+ `![${image.description?.alternativeText || ''}](${image.url})`
151
+ )
373
152
  }
374
153
  }
375
- } else {
376
- new Notice(
377
- 'Image folder not found. Please create an "image" folder next to your article file.'
378
- )
379
- }
380
- return null
381
- }
382
154
 
383
- /**
384
- * Get the gallery image blobs from the folder path
385
- * @param app
386
- * @param galleryFolderPath
387
- */
388
- export async function getGalleryImageBlobs(
389
- app: App,
390
- galleryFolderPath: string
391
- ): Promise<{ path: string; blob: Blob; name: string }[]> {
392
- const folder = app.vault.getAbstractFileByPath(galleryFolderPath)
393
- if (folder instanceof TFolder) {
394
- const files = folder.children.filter(
395
- file =>
396
- file instanceof TFile &&
397
- file.extension.match(/^(jpg|jpeg|png|gif|bmp|webp)$/i)
398
- )
399
- return Promise.all(
400
- files.map(async file => {
401
- // check the TFILE type, and not cast it to TFILE
402
- if (!(file instanceof TFile)) {
403
- return {
404
- name: '',
405
- blob: new Blob(),
406
- path: '',
407
- }
408
- }
409
- const blob = await app.vault.readBinary(file)
410
- return {
411
- name: file.name,
412
- blob: new Blob([blob], { type: 'image/png' }),
413
- path: file.path,
414
- }
415
- })
416
- )
417
- } else {
418
- new Notice(
419
- 'Gallery folder not found. Please create a "gallery" folder next to your article file.'
420
- )
155
+ return updatedContent
156
+ } catch (error) {
157
+ throw new Error(`Failed to update image references: ${error.message}`)
421
158
  }
422
- return []
423
159
  }
424
160
 
425
161
  /**
426
- * Replace the image paths in the content with the uploaded images
427
- * @param content
428
- * @param uploadedImages
162
+ * Check if a path is an external URL
429
163
  */
430
- export function replaceImagePaths(
431
- content: string,
432
- uploadedImages: { [key: string]: { url: string; data: any } }
433
- ): string {
434
- for (const [localPath, imageData] of Object.entries(uploadedImages)) {
435
- const markdownImageRegex = new RegExp(`!\\[\\[${localPath}\\]\\]`, 'g')
436
- content = content.replace(
437
- markdownImageRegex,
438
- `![${imageData.data.alternativeText}](${imageData.url})`
439
- )
440
- }
441
- return content
164
+ function isExternalUrl(path: string): boolean {
165
+ return path.startsWith('http://') || path.startsWith('https://')
442
166
  }