notes-to-strapi-export-article-ai 1.0.119 → 3.0.2

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
@@ -0,0 +1,265 @@
1
+ import { App, Modal, Notice, Setting } from 'obsidian'
2
+ import { AnalyzedContent } from '../types'
3
+ import { FrontmatterGenerator } from '../services/frontmatter'
4
+ import StrapiExporterPlugin from '../main'
5
+
6
+ export class PreviewModal extends Modal {
7
+ private content: AnalyzedContent
8
+ private plugin: StrapiExporterPlugin
9
+ private onConfirm: () => void
10
+ private onCancel: () => void
11
+ private frontmatterGenerator: FrontmatterGenerator
12
+
13
+ constructor(
14
+ app: App,
15
+ content: AnalyzedContent,
16
+ plugin: StrapiExporterPlugin,
17
+ onConfirm: () => void,
18
+ onCancel?: () => void
19
+ ) {
20
+ super(app)
21
+ this.plugin = plugin
22
+ this.content = content
23
+ this.onConfirm = onConfirm
24
+ this.onCancel = onCancel || (() => {})
25
+
26
+ // Initialize with plugin instance
27
+ this.frontmatterGenerator = new FrontmatterGenerator(this.plugin)
28
+ }
29
+
30
+ onOpen() {
31
+ const { contentEl } = this
32
+
33
+ try {
34
+ this.createHeader(contentEl)
35
+ this.createGenerateButton(contentEl)
36
+ this.createPreviewContainer(contentEl)
37
+ this.createButtons(contentEl)
38
+ this.addStyles()
39
+ } catch (error) {
40
+ this.showError('Failed to render preview' + error.message)
41
+ }
42
+ }
43
+
44
+ private createHeader(container: HTMLElement) {
45
+ container.createEl('h2', {
46
+ text: 'Content Preview',
47
+ cls: 'preview-modal-title',
48
+ })
49
+
50
+ container.createEl('p', {
51
+ text: 'Review or generate frontmatter before exporting to Strapi.',
52
+ cls: 'preview-modal-description',
53
+ })
54
+ }
55
+
56
+ private createGenerateButton(container: HTMLElement) {
57
+ new Setting(container)
58
+ .setName('Generate Metadata')
59
+ .setDesc('Use AI to generate frontmatter metadata for your content')
60
+ .addButton(button =>
61
+ button
62
+ .setButtonText('Generate')
63
+ .setCta()
64
+ .onClick(async () => {
65
+ try {
66
+ await this.generateFrontmatter()
67
+ } catch (error) {
68
+ new Notice(`Failed to generate metadata: ${error.message}`)
69
+ }
70
+ })
71
+ )
72
+ }
73
+
74
+ private async generateFrontmatter() {
75
+ new Notice('Generating frontmatter...')
76
+
77
+ // Get the active file
78
+ const file = this.app.workspace.getActiveFile()
79
+ if (!file) {
80
+ throw new Error('No active file')
81
+ }
82
+
83
+ // Generate frontmatter
84
+ const updatedContent =
85
+ await this.frontmatterGenerator.updateContentFrontmatter(file, this.app)
86
+
87
+ // Update content object
88
+ this.content.content = updatedContent
89
+
90
+ // Update preview
91
+ this.updatePreview()
92
+ await this.app.vault.modify(file, updatedContent)
93
+
94
+ new Notice('Frontmatter generated successfully!')
95
+ }
96
+
97
+ private createPreviewContainer(container: HTMLElement) {
98
+ const previewContainer = container.createDiv('preview-container')
99
+ this.createContentSections(previewContainer)
100
+ }
101
+
102
+ private updatePreview() {
103
+ const previewContainer = this.contentEl.querySelector('.preview-container')
104
+ if (previewContainer) {
105
+ previewContainer.empty()
106
+ this.createContentSections(previewContainer)
107
+ }
108
+ }
109
+
110
+ private createContentSections(container: Element) {
111
+ // Main content section
112
+ if (this.content.content) {
113
+ const contentSection = this.createSection(
114
+ container.createDiv(),
115
+ 'Main Content',
116
+ this.content.content,
117
+ 'content-section'
118
+ )
119
+ contentSection.addClass('main-content')
120
+ }
121
+ }
122
+
123
+ createSection(
124
+ container: HTMLElement,
125
+ title: string,
126
+ content: any,
127
+ className: string
128
+ ): HTMLElement {
129
+ const section = container.createDiv(className)
130
+ section.createEl('h3', { text: title })
131
+
132
+ const previewEl = section.createEl('pre')
133
+ previewEl.setText(
134
+ typeof content === 'string' ? content : JSON.stringify(content, null, 2)
135
+ )
136
+
137
+ return section
138
+ }
139
+
140
+ private createButtons(container: HTMLElement) {
141
+ const buttonContainer = container.createDiv('button-container')
142
+
143
+ // Confirm button
144
+ new Setting(buttonContainer).addButton(button => {
145
+ button
146
+ .setButtonText('Confirm & Export')
147
+ .setCta()
148
+ .onClick(() => {
149
+ this.close()
150
+ this.onConfirm()
151
+ })
152
+ })
153
+
154
+ // Cancel button
155
+ new Setting(buttonContainer).addButton(button => {
156
+ button.setButtonText('Cancel').onClick(() => {
157
+ this.close()
158
+ this.onCancel()
159
+ })
160
+ })
161
+ }
162
+
163
+ private addStyles() {
164
+ document.body.addClass('preview-modal-open')
165
+
166
+ const styles = `
167
+ .preview-modal-title {
168
+ margin-bottom: 1em;
169
+ padding-bottom: 0.5em;
170
+ border-bottom: 1px solid var(--background-modifier-border);
171
+ }
172
+
173
+ .preview-modal-description {
174
+ margin-bottom: 1.5em;
175
+ color: var(--text-muted);
176
+ }
177
+
178
+ .preview-container {
179
+ max-height: 60vh;
180
+ overflow-y: auto;
181
+ margin-bottom: 1.5em;
182
+ padding: 1em;
183
+ border: 1px solid var(--background-modifier-border);
184
+ border-radius: 4px;
185
+ }
186
+
187
+ .content-section,
188
+ .metadata-section {
189
+ margin-bottom: 1.5em;
190
+ }
191
+
192
+ .content-section h3,
193
+ .metadata-section h3 {
194
+ margin-bottom: 0.5em;
195
+ color: var(--text-normal);
196
+ }
197
+
198
+ .content-section pre,
199
+ .metadata-section pre {
200
+ padding: 1em;
201
+ background-color: var(--background-primary-alt);
202
+ border-radius: 4px;
203
+ overflow-x: auto;
204
+ }
205
+
206
+ .button-container {
207
+ display: flex;
208
+ justify-content: flex-end;
209
+ gap: 1em;
210
+ margin-top: 1em;
211
+ }
212
+
213
+ .button-container .setting-item {
214
+ align-items: end;
215
+ border: none;
216
+ }
217
+ `
218
+
219
+ document.head.createEl('style', {
220
+ attr: { type: 'text/css' },
221
+ text: styles,
222
+ })
223
+ }
224
+
225
+ private showError(message: string) {
226
+ const { contentEl } = this
227
+ contentEl.empty()
228
+
229
+ contentEl.createEl('h2', {
230
+ text: 'Error',
231
+ cls: 'preview-modal-error-title',
232
+ })
233
+
234
+ contentEl.createEl('p', {
235
+ text: message,
236
+ cls: 'preview-modal-error-message',
237
+ })
238
+ }
239
+
240
+ onClose() {
241
+ const { contentEl } = this
242
+ contentEl.empty()
243
+ document.body.removeClass('preview-modal-open')
244
+ }
245
+ }
246
+
247
+ export function showPreviewToUser(
248
+ app: App,
249
+ content: AnalyzedContent,
250
+ plugin: StrapiExporterPlugin
251
+ ): Promise<boolean> {
252
+ return new Promise(resolve => {
253
+ new PreviewModal(
254
+ app,
255
+ content,
256
+ plugin,
257
+ () => {
258
+ resolve(true)
259
+ },
260
+ () => {
261
+ resolve(false)
262
+ }
263
+ ).open()
264
+ })
265
+ }
@@ -0,0 +1,122 @@
1
+ import { App, TFile } from 'obsidian'
2
+ import { StrapiExporterSettings, AnalyzedContent } from '../types'
3
+ import { uploadImageToStrapi } from './strapi-uploader'
4
+
5
+ interface ImageMatch {
6
+ fullMatch: string
7
+ altText: string
8
+ imagePath: string
9
+ }
10
+
11
+ /**
12
+ * Process images in content fields and upload them to Strapi
13
+ */
14
+ export async function processImages(
15
+ content: AnalyzedContent,
16
+ app: App,
17
+ settings: StrapiExporterSettings
18
+ ): Promise<AnalyzedContent> {
19
+ const processedContent = { ...content }
20
+
21
+ for (const [key, value] of Object.entries(processedContent)) {
22
+ if (typeof value === 'string') {
23
+ processedContent[key] = await processImageLinks(value, app, settings)
24
+ }
25
+ }
26
+
27
+ return processedContent
28
+ }
29
+
30
+ /**
31
+ * Process image links in content string
32
+ */
33
+ async function processImageLinks(
34
+ content: string,
35
+ app: App,
36
+ settings: StrapiExporterSettings
37
+ ): Promise<string> {
38
+ const imageMatches = extractImageMatches(content)
39
+
40
+ let processedContent = content
41
+
42
+ for (const match of imageMatches) {
43
+ const replacedContent = await processImageMatch(
44
+ match,
45
+ processedContent,
46
+ app,
47
+ settings
48
+ )
49
+
50
+ if (replacedContent !== processedContent) {
51
+ processedContent = replacedContent
52
+ }
53
+ }
54
+
55
+ return processedContent
56
+ }
57
+
58
+ /**
59
+ * Extract image matches from content
60
+ */
61
+ function extractImageMatches(content: string): ImageMatch[] {
62
+ const matches: ImageMatch[] = []
63
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
64
+ let match
65
+
66
+ while ((match = imageRegex.exec(content)) !== null) {
67
+ matches.push({
68
+ fullMatch: match[0],
69
+ altText: match[1],
70
+ imagePath: match[2],
71
+ })
72
+ }
73
+
74
+ return matches
75
+ }
76
+
77
+ /**
78
+ * Process individual image match
79
+ */
80
+ async function processImageMatch(
81
+ match: ImageMatch,
82
+ content: string,
83
+ app: App,
84
+ settings: StrapiExporterSettings
85
+ ): Promise<string> {
86
+ if (isExternalUrl(match.imagePath)) {
87
+ return content
88
+ }
89
+
90
+ const file = app.vault.getAbstractFileByPath(match.imagePath)
91
+ if (!(file instanceof TFile)) {
92
+ return content
93
+ }
94
+
95
+ const uploadedImage = await uploadImageToStrapi(
96
+ file,
97
+ file.name,
98
+ settings,
99
+ app,
100
+ {
101
+ alternativeText: match.altText || file.basename,
102
+ caption: match.altText || file.basename,
103
+ }
104
+ )
105
+
106
+ if (uploadedImage?.url) {
107
+ const newContent = content.replace(
108
+ match.fullMatch,
109
+ `![${match.altText}](${uploadedImage.url})`
110
+ )
111
+ return newContent
112
+ }
113
+
114
+ return content
115
+ }
116
+
117
+ /**
118
+ * Check if URL is external
119
+ */
120
+ function isExternalUrl(url: string): boolean {
121
+ return url.startsWith('http://') || url.startsWith('https://')
122
+ }
@@ -1,137 +1,138 @@
1
- import { Notice } from 'obsidian'
2
- import { StrapiExporterSettings } from '../types/settings'
3
- import { ImageBlob, ImageDescription } from '../types/image'
1
+ import { App, Notice, TAbstractFile, TFile } from 'obsidian'
2
+ import { StrapiExporterSettings, ImageDescription } from '../types'
3
+
4
+ interface UploadResponse {
5
+ url: string
6
+ id: number
7
+ name: string
8
+ alternativeText?: string
9
+ caption?: string
10
+ }
4
11
 
5
12
  /**
6
- * Upload images to Strapi
7
- * @param imageDescriptions
8
- * @param settings
9
- * @param app
10
- * @param imageFolderPath
13
+ * Upload single image to Strapi
11
14
  */
12
- export async function uploadImagesToStrapi(
13
- imageDescriptions: ImageDescription[],
15
+ export async function uploadImageToStrapi(
16
+ imageData: string | TFile,
17
+ fileName: string,
14
18
  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)
19
+ app: App,
20
+ additionalMetadata?: {
21
+ alternativeText?: string
22
+ caption?: string
23
+ }
24
+ ): Promise<ImageDescription | null> {
25
+ // Validate settings
26
+ validateStrapiSettings(settings)
27
+
28
+ // Get file
29
+ const file = await getFileFromImageData(imageData, fileName, app)
30
+ if (!file) {
31
+ return null
32
+ }
33
+
34
+ // Prepare form data
35
+ const formData = await prepareFormData(file, fileName, additionalMetadata)
36
+
37
+ // Upload to Strapi
38
+ const uploadResult = await performStrapiUpload(formData, settings)
39
+
40
+ if (uploadResult) {
41
+ return createImageDescription(uploadResult, fileName, additionalMetadata)
42
+ }
43
+
44
+ return null
45
+ }
46
+ // Helper functions
47
+
48
+ async function getFileFromImageData(
49
+ imageData: string | TFile,
50
+ fileName: string,
51
+ app: App
52
+ ): Promise<TFile | null> {
53
+ let file: TAbstractFile | null = null
54
+ if (typeof imageData === 'string') {
55
+ file = app.vault.getAbstractFileByPath(imageData)
56
+ } else if (imageData instanceof TFile) {
57
+ file = imageData
58
+ }
59
+
60
+ if (!(file instanceof TFile)) {
61
+ new Notice(`Failed to find file: ${fileName}`)
62
+ return null
63
+ }
64
+
65
+ return file
66
+ }
67
+
68
+ async function prepareFormData(
69
+ file: TFile,
70
+ fileName: string,
71
+ metadata?: { alternativeText?: string; caption?: string }
72
+ ): Promise<FormData> {
73
+ const formData = new FormData()
74
+ const arrayBuffer = await file.vault.readBinary(file)
75
+ const blob = new Blob([arrayBuffer], { type: `image/${file.extension}` })
76
+ formData.append('files', blob, fileName)
77
+
78
+ if (metadata) {
24
79
  formData.append(
25
80
  'fileInfo',
26
81
  JSON.stringify({
27
- name: imageDescription.description.name,
28
- alternativeText: imageDescription.description.alternativeText,
29
- caption: imageDescription.description.caption,
82
+ name: fileName,
83
+ alternativeText: metadata.alternativeText,
84
+ caption: metadata.caption,
30
85
  })
31
86
  )
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
- const errorData = await response.json()
51
- new Notice(
52
- `Failed to upload image: ${imageDescription.name}. Error: ${errorData.error.message}`
53
- )
54
- }
55
- } catch (error) {
56
- new Notice(
57
- `Error uploading image: ${imageDescription.name}. Error: ${error.message}`
58
- )
59
- }
60
87
  }
61
88
 
62
- if (imageFolderPath && app) {
63
- // Save metadata to a file only if there are uploaded images
64
- if (Object.keys(uploadedImages).length > 0) {
65
- const metadataFile = `${imageFolderPath}/metadata.json`
66
- await app.vault.adapter.write(
67
- metadataFile,
68
- JSON.stringify(uploadedImages)
69
- )
70
- }
71
- }
72
- return uploadedImages
89
+ return formData
73
90
  }
74
91
 
75
- /**
76
- * Upload gallery images to Strapi
77
- * @param imageBlobs
78
- * @param settings
79
- * @param app
80
- * @param galleryFolderPath
81
- */
82
- export async function uploadGalleryImagesToStrapi(
83
- imageBlobs: ImageBlob[],
84
- settings: StrapiExporterSettings,
85
- app: any = null,
86
- galleryFolderPath: string = ''
87
- ): Promise<number[]> {
88
- const uploadedImageIds: number[] = []
89
- const uploadedImages: { [key: string]: { url: string; data: any; id: any } } =
90
- {}
91
-
92
- for (const imageBlob of imageBlobs) {
93
- const formData = new FormData()
94
- formData.append('files', imageBlob.blob, imageBlob.name)
95
-
96
- try {
97
- const response = await fetch(`${settings.strapiUrl}/api/upload`, {
98
- method: 'POST',
99
- headers: {
100
- Authorization: `Bearer ${settings.strapiApiToken}`,
101
- },
102
- body: formData,
103
- })
92
+ async function performStrapiUpload(
93
+ formData: FormData,
94
+ settings: StrapiExporterSettings
95
+ ): Promise<UploadResponse | null> {
96
+ const response = await fetch(`${settings.strapiUrl}/api/upload`, {
97
+ method: 'POST',
98
+ headers: {
99
+ Authorization: `Bearer ${settings.strapiApiToken}`,
100
+ },
101
+ body: formData,
102
+ })
104
103
 
105
- if (response.ok) {
106
- const data = await response.json()
107
- uploadedImages[imageBlob.name] = {
108
- url: data[0].url,
109
- id: data[0].id,
110
- data: data[0],
111
- }
112
- } else {
113
- const errorData = await response.json()
114
- new Notice(
115
- `Failed to upload gallery image: ${imageBlob.name}. Error: ${errorData.error.message}`
116
- )
117
- }
118
- } catch (error) {
119
- new Notice(
120
- `Error uploading gallery image: ${imageBlob.name}. Error: ${error.message}`
121
- )
122
- }
104
+ if (!response.ok) {
105
+ const errorData = await response.json()
106
+ throw new Error(errorData.error.message)
123
107
  }
124
108
 
125
- if (galleryFolderPath && app) {
126
- // Save metadata to a file only if there are uploaded images
127
- if (Object.keys(uploadedImages).length > 0) {
128
- const metadataFile = `${galleryFolderPath}/metadata.json`
129
- await app.vault.adapter.write(
130
- metadataFile,
131
- JSON.stringify(uploadedImages)
132
- )
133
- }
134
- }
109
+ const data = await response.json()
110
+ return data[0]
111
+ }
135
112
 
136
- return uploadedImageIds
113
+ function validateStrapiSettings(settings: StrapiExporterSettings): void {
114
+ if (!settings.strapiUrl) {
115
+ throw new Error('Strapi URL is not configured')
116
+ }
117
+ if (!settings.strapiApiToken) {
118
+ throw new Error('Strapi API token is not configured')
119
+ }
120
+ }
121
+ function createImageDescription(
122
+ uploadResult: UploadResponse,
123
+ fileName: string,
124
+ metadata?: { alternativeText?: string; caption?: string }
125
+ ): ImageDescription {
126
+ return {
127
+ url: uploadResult.url,
128
+ name: fileName,
129
+ path: uploadResult.url,
130
+ id: uploadResult.id,
131
+ description: {
132
+ name: uploadResult.name,
133
+ alternativeText:
134
+ uploadResult.alternativeText || metadata?.alternativeText || '',
135
+ caption: uploadResult.caption || metadata?.caption || '',
136
+ },
137
+ }
137
138
  }