notes-to-strapi-export-article-ai 3.0.2 → 3.0.5
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 +1 -0
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/src/services/frontmatter.ts +78 -13
- package/src/services/strapi-export.ts +248 -10
- package/src/utils/process-file.ts +116 -68
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://github.com/sponsors/CinquinAndy)
|
|
6
6
|
|
|
7
7
|
Strapi Exporter is a game-changing Obsidian plugin that streamlines your content creation process by seamlessly exporting your notes to Strapi CMS. With its AI-powered image handling and SEO optimization features, you can take your content to the next level with just a few clicks.
|
|
8
|
+
! This plugin was totally refactored, since the 3.0.0 version ! Restart your configuration !
|
|
8
9
|
|
|
9
10
|
## ✨ Key Features
|
|
10
11
|
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "notes-to-strapi-export-article-ai",
|
|
3
3
|
"name": "Strapi Exporter AI",
|
|
4
|
-
"version": "3.0.
|
|
4
|
+
"version": "3.0.5",
|
|
5
5
|
"minAppVersion": "1.7.0",
|
|
6
6
|
"description": "Effortlessly export your notes to Strapi CMS with AI-powered handling and SEO optimization.",
|
|
7
7
|
"author": "Cinquin Andy",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notes-to-strapi-export-article-ai",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "Effortlessly export your Obsidian notes to Strapi CMS with AI-powered image handling and SEO optimization. Replace all the images in your notes by uploaded images in Strapi, and add SEO metadata to uploaded images.",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@ai-sdk/openai": "0.0.72",
|
|
26
26
|
"ai": "3.4.33",
|
|
27
27
|
"js-yaml": "4.1.0",
|
|
28
|
-
"openai": "4.
|
|
28
|
+
"openai": "4.72.0",
|
|
29
29
|
"process": "0.11.10"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { generateText } from 'ai'
|
|
2
2
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
3
|
-
import { App, TFile } from 'obsidian'
|
|
3
|
+
import { App, Notice, TFile } from 'obsidian'
|
|
4
4
|
import { RouteConfig } from '../types'
|
|
5
5
|
import StrapiExporterPlugin from '../main'
|
|
6
|
+
import { processContentLinks } from '../utils/process-file'
|
|
7
|
+
import { extractFrontMatterAndContent } from '../utils/analyse-file'
|
|
8
|
+
import yaml from 'js-yaml'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Interface defining the structure of a field in the schema
|
|
@@ -55,11 +58,72 @@ export class FrontmatterGenerator {
|
|
|
55
58
|
* @returns Promise<string> Updated content with new frontmatter
|
|
56
59
|
*/
|
|
57
60
|
async updateContentFrontmatter(file: TFile, app: App): Promise<string> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
try {
|
|
62
|
+
// Read original content
|
|
63
|
+
const originalContent = await app.vault.read(file)
|
|
64
|
+
|
|
65
|
+
// Process images in the content first
|
|
66
|
+
const processedContent = await this.processContentWithImages(
|
|
67
|
+
originalContent,
|
|
68
|
+
app
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Generate new frontmatter
|
|
72
|
+
const newFrontmatter = await this.generateFrontmatter(
|
|
73
|
+
file,
|
|
74
|
+
app,
|
|
75
|
+
processedContent
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Replace or add frontmatter to the processed content
|
|
79
|
+
const updatedContent = this.replaceFrontmatter(
|
|
80
|
+
processedContent,
|
|
81
|
+
newFrontmatter
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return updatedContent
|
|
85
|
+
} catch (error) {
|
|
86
|
+
new Notice(`Error processing content: ${error.message}`)
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Process content including images
|
|
93
|
+
* @param content - Original content
|
|
94
|
+
* @param app - The Obsidian app instance
|
|
95
|
+
* @returns Promise<string> Processed content with updated image links
|
|
96
|
+
*/
|
|
97
|
+
private async processContentWithImages(
|
|
98
|
+
content: string,
|
|
99
|
+
app: App
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
try {
|
|
102
|
+
const { frontmatter, body } = extractFrontMatterAndContent(content)
|
|
103
|
+
|
|
104
|
+
// Process only the images in the content, leaving regular links unchanged
|
|
105
|
+
const processedContent = await processContentLinks(
|
|
106
|
+
body,
|
|
107
|
+
app,
|
|
108
|
+
this.plugin.settings
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// If frontmatter exists, reconstruct the content with it
|
|
112
|
+
if (Object.keys(frontmatter).length > 0) {
|
|
113
|
+
return [
|
|
114
|
+
'---',
|
|
115
|
+
yaml.dump(frontmatter),
|
|
116
|
+
'---',
|
|
117
|
+
'',
|
|
118
|
+
processedContent,
|
|
119
|
+
].join('\n')
|
|
120
|
+
}
|
|
61
121
|
|
|
62
|
-
|
|
122
|
+
return processedContent
|
|
123
|
+
} catch (error) {
|
|
124
|
+
new Notice(`Error processing images: ${error.message}`)
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
63
127
|
}
|
|
64
128
|
|
|
65
129
|
/**
|
|
@@ -105,10 +169,8 @@ export class FrontmatterGenerator {
|
|
|
105
169
|
return ['example1', 'example2']
|
|
106
170
|
}
|
|
107
171
|
|
|
108
|
-
// Analyze transform string to determine array structure
|
|
109
172
|
const transform = field.transform
|
|
110
173
|
|
|
111
|
-
// Handle different array structures based on transform content
|
|
112
174
|
if (transform.includes('name:')) {
|
|
113
175
|
return [
|
|
114
176
|
{ name: 'example1', id: 1 },
|
|
@@ -144,7 +206,7 @@ export class FrontmatterGenerator {
|
|
|
144
206
|
.map(([field]) => field)
|
|
145
207
|
.join(', ')
|
|
146
208
|
|
|
147
|
-
|
|
209
|
+
return `Generate YAML frontmatter that follows this exact schema and format:
|
|
148
210
|
|
|
149
211
|
Schema Definition:
|
|
150
212
|
${JSON.stringify(config.fieldMappings, null, 2)}
|
|
@@ -169,8 +231,6 @@ DO NOT include the content IN the generated frontmatter. Just use the content to
|
|
|
169
231
|
the content field is ${config.contentField}. Please delete the content field from the frontmatter.
|
|
170
232
|
|
|
171
233
|
Return complete YAML frontmatter with opening and closing "---" markers.`
|
|
172
|
-
|
|
173
|
-
return prompt
|
|
174
234
|
}
|
|
175
235
|
|
|
176
236
|
/**
|
|
@@ -233,13 +293,18 @@ Return complete YAML frontmatter with opening and closing "---" markers.`
|
|
|
233
293
|
}
|
|
234
294
|
|
|
235
295
|
/**
|
|
236
|
-
*
|
|
296
|
+
* Generates frontmatter based on file content and schema
|
|
237
297
|
* @param file - The file to process
|
|
238
298
|
* @param app - The Obsidian app instance
|
|
299
|
+
* @param processedContent - Optional pre-processed content
|
|
239
300
|
* @returns Promise<string> Generated frontmatter
|
|
240
301
|
*/
|
|
241
|
-
async generateFrontmatter(
|
|
242
|
-
|
|
302
|
+
async generateFrontmatter(
|
|
303
|
+
file: TFile,
|
|
304
|
+
app: App,
|
|
305
|
+
processedContent?: string
|
|
306
|
+
): Promise<string> {
|
|
307
|
+
const content = processedContent || (await app.vault.read(file))
|
|
243
308
|
const currentRoute = this.getCurrentRoute()
|
|
244
309
|
|
|
245
310
|
if (!currentRoute?.generatedConfig) {
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { RouteConfig, AnalyzedContent } from '../types'
|
|
2
2
|
import { StrapiExporterSettings } from '../types/settings'
|
|
3
3
|
import { extractFrontMatterAndContent } from '../utils/analyse-file'
|
|
4
|
-
import { App, TFile } from 'obsidian'
|
|
4
|
+
import { App, Notice, TFile } from 'obsidian'
|
|
5
5
|
import { uploadImageToStrapi } from '../utils/strapi-uploader'
|
|
6
6
|
import * as yaml from 'js-yaml'
|
|
7
|
+
import { createOpenAI } from '@ai-sdk/openai'
|
|
8
|
+
import { generateText } from 'ai'
|
|
7
9
|
|
|
8
10
|
export class StrapiExportService {
|
|
11
|
+
private model
|
|
12
|
+
|
|
9
13
|
constructor(
|
|
10
14
|
private settings: StrapiExporterSettings,
|
|
11
15
|
private app: App,
|
|
12
16
|
private file: TFile
|
|
13
|
-
) {
|
|
17
|
+
) {
|
|
18
|
+
const openai = createOpenAI({
|
|
19
|
+
apiKey: this.settings.openaiApiKey,
|
|
20
|
+
})
|
|
21
|
+
this.model = openai('gpt-4o-mini')
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
private async sendToStrapi(data: any, route: RouteConfig): Promise<void> {
|
|
16
25
|
const url = `${this.settings.strapiUrl}${route.url}`
|
|
@@ -390,18 +399,247 @@ export class StrapiExportService {
|
|
|
390
399
|
this.validateSettings()
|
|
391
400
|
this.validateRoute(route)
|
|
392
401
|
|
|
393
|
-
|
|
402
|
+
try {
|
|
403
|
+
let exportData = await this.prepareExportData(this.app, this.file, route)
|
|
394
404
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
if (exportData.data[route.contentField]) {
|
|
406
|
+
const { content: processedContent } = await this.processContentImages(
|
|
407
|
+
exportData.data[route.contentField]
|
|
408
|
+
)
|
|
409
|
+
exportData.data[route.contentField] = processedContent
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
exportData.data = await this.convertImageUrlsToIds(exportData.data)
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
await this.sendToStrapi(exportData, route)
|
|
416
|
+
} catch (strapiError) {
|
|
417
|
+
new Notice(`Failed to upload, retrying with simplified data...`)
|
|
418
|
+
// Parse and analyze the error
|
|
419
|
+
const errorDetails = await this.parseAndAnalyzeError(strapiError)
|
|
420
|
+
|
|
421
|
+
// Regenerate data with error details
|
|
422
|
+
exportData = await this.regenerateDataWithError(
|
|
423
|
+
exportData,
|
|
424
|
+
errorDetails,
|
|
425
|
+
route
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
// Retry sending to Strapi
|
|
429
|
+
await this.sendToStrapi(exportData, route)
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new Error(`Export failed: ${error.message}`)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Process individual error messages from Strapi response
|
|
438
|
+
*/
|
|
439
|
+
private async parseAndAnalyzeError(error: any): Promise<any> {
|
|
440
|
+
try {
|
|
441
|
+
// If we already have JSON data in the error response
|
|
442
|
+
if (error.response?.data) {
|
|
443
|
+
return {
|
|
444
|
+
statusCode: error.response.status,
|
|
445
|
+
error: error.response.data.error,
|
|
446
|
+
details: error.response.data.error?.details || {},
|
|
447
|
+
message: error.response.data.error?.message || 'Unknown error',
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Handle string error message
|
|
452
|
+
if (typeof error === 'string') {
|
|
453
|
+
return {
|
|
454
|
+
statusCode: 500,
|
|
455
|
+
error: { message: error },
|
|
456
|
+
details: {},
|
|
457
|
+
message: error,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Extract detailed error info
|
|
462
|
+
const errorInfo = error.toString().split(': ').pop()
|
|
463
|
+
let parsedError
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
parsedError = JSON.parse(errorInfo)
|
|
467
|
+
} catch {
|
|
468
|
+
parsedError = { message: errorInfo }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
statusCode: error.response?.status || 500,
|
|
473
|
+
error: parsedError,
|
|
474
|
+
details: parsedError.details || {},
|
|
475
|
+
message: parsedError.message || 'Unknown error',
|
|
476
|
+
}
|
|
477
|
+
} catch (e) {
|
|
478
|
+
console.error('Error parsing Strapi response:', e)
|
|
479
|
+
return {
|
|
480
|
+
statusCode: error.response?.status || 500,
|
|
481
|
+
error: { message: 'Failed to parse error response' },
|
|
482
|
+
details: {},
|
|
483
|
+
message: error.message || 'Unknown error',
|
|
484
|
+
}
|
|
400
485
|
}
|
|
486
|
+
}
|
|
401
487
|
|
|
402
|
-
|
|
488
|
+
/**
|
|
489
|
+
* Regenerate data based on Strapi error feedback
|
|
490
|
+
*/
|
|
491
|
+
private async regenerateDataWithError(
|
|
492
|
+
originalData: any,
|
|
493
|
+
errorDetails: any,
|
|
494
|
+
route: RouteConfig
|
|
495
|
+
): Promise<any> {
|
|
496
|
+
const { text } = await generateText({
|
|
497
|
+
model: this.model,
|
|
498
|
+
system: `You are an API expert specializing in Strapi CMS. You MUST return only valid JSON data.
|
|
499
|
+
Do not include any explanations or text outside of the JSON structure.
|
|
500
|
+
The response must be parseable by JSON.parse().`,
|
|
501
|
+
prompt: `
|
|
502
|
+
Fix this failed Strapi API request.
|
|
503
|
+
|
|
504
|
+
Error Information:
|
|
505
|
+
Status Code: ${errorDetails.statusCode}
|
|
506
|
+
Message: ${errorDetails.message}
|
|
507
|
+
Details: ${JSON.stringify(errorDetails.details, null, 2)}
|
|
508
|
+
|
|
509
|
+
Original Request Data:
|
|
510
|
+
${JSON.stringify(originalData, null, 2)}
|
|
511
|
+
|
|
512
|
+
Route Configuration and Schema:
|
|
513
|
+
${route.generatedConfig}
|
|
514
|
+
|
|
515
|
+
IMPORTANT: Return ONLY valid JSON in the exact format:
|
|
516
|
+
{
|
|
517
|
+
"data": {
|
|
518
|
+
// corrected fields here
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
If the error is something similar with an Unknown error, try to delete the complexe fields, galleries, lists, etc. and keep only the simple fields.
|
|
523
|
+
|
|
524
|
+
Do not include any text explanations - use the "__comment" field inside the JSON if needed.
|
|
525
|
+
The entire response must be valid JSON that can be parsed with JSON.parse().
|
|
526
|
+
`,
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
// First, try to clean the response if needed
|
|
531
|
+
const cleanedText = this.cleanAIResponse(text)
|
|
532
|
+
|
|
533
|
+
let correctedData: any
|
|
534
|
+
try {
|
|
535
|
+
correctedData = JSON.parse(cleanedText)
|
|
536
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
537
|
+
} catch (e) {
|
|
538
|
+
// If parsing fails, attempt to extract JSON from the response
|
|
539
|
+
const extractedJson = this.extractJsonFromText(cleanedText)
|
|
540
|
+
if (!extractedJson) {
|
|
541
|
+
console.error('Original AI response:', text)
|
|
542
|
+
console.error('Cleaned response:', cleanedText)
|
|
543
|
+
throw new Error('Could not extract valid JSON from AI response')
|
|
544
|
+
}
|
|
545
|
+
correctedData = JSON.parse(extractedJson)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Validate basic structure
|
|
549
|
+
if (!correctedData.data) {
|
|
550
|
+
correctedData = { data: correctedData }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Log for debugging
|
|
554
|
+
console.log('Original data:', originalData)
|
|
555
|
+
console.log('Error details:', errorDetails)
|
|
556
|
+
console.log('Corrected data:', correctedData)
|
|
557
|
+
|
|
558
|
+
if (correctedData.__comment) {
|
|
559
|
+
console.log('Correction comment:', correctedData.__comment)
|
|
560
|
+
new Notice(`Changes made: ${correctedData.__comment}`)
|
|
561
|
+
delete correctedData.__comment
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return correctedData
|
|
565
|
+
} catch (e) {
|
|
566
|
+
console.error('AI Response:', text)
|
|
567
|
+
console.error('Generation error:', e)
|
|
568
|
+
|
|
569
|
+
// Fallback: return a sanitized version of the original data
|
|
570
|
+
return this.createFallbackData(originalData, errorDetails)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Clean AI response text to ensure valid JSON
|
|
576
|
+
*/
|
|
577
|
+
private cleanAIResponse(text: string): string {
|
|
578
|
+
// Remove markdown code blocks if present
|
|
579
|
+
// eslint-disable-next-line no-useless-escape
|
|
580
|
+
text = text.replace(/```json\n?|\```\n?/g, '')
|
|
581
|
+
|
|
582
|
+
// Remove any leading/trailing whitespace
|
|
583
|
+
text = text.trim()
|
|
584
|
+
|
|
585
|
+
// Remove any text before the first {
|
|
586
|
+
const firstBrace = text.indexOf('{')
|
|
587
|
+
if (firstBrace > 0) {
|
|
588
|
+
text = text.substring(firstBrace)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Remove any text after the last }
|
|
592
|
+
const lastBrace = text.lastIndexOf('}')
|
|
593
|
+
if (lastBrace !== -1 && lastBrace < text.length - 1) {
|
|
594
|
+
text = text.substring(0, lastBrace + 1)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return text
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Extract JSON from text that might contain other content
|
|
602
|
+
*/
|
|
603
|
+
private extractJsonFromText(text: string): string | null {
|
|
604
|
+
const jsonRegex = /{[\s\S]*}/
|
|
605
|
+
const match = text.match(jsonRegex)
|
|
606
|
+
return match ? match[0] : null
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Create fallback data when regeneration fails
|
|
611
|
+
*/
|
|
612
|
+
private createFallbackData(originalData: any, errorDetails: any): any {
|
|
613
|
+
console.log('Using fallback data generation')
|
|
614
|
+
|
|
615
|
+
// Start with the original data
|
|
616
|
+
const fallbackData = { ...originalData }
|
|
617
|
+
|
|
618
|
+
// Remove fields mentioned in error details
|
|
619
|
+
if (errorDetails.details?.errors) {
|
|
620
|
+
errorDetails.details.errors.forEach((error: any) => {
|
|
621
|
+
if (error.path) {
|
|
622
|
+
const path = Array.isArray(error.path) ? error.path : [error.path]
|
|
623
|
+
let current = fallbackData.data
|
|
624
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
625
|
+
if (current[path[i]]) {
|
|
626
|
+
current = current[path[i]]
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const lastKey = path[path.length - 1]
|
|
630
|
+
if (current[lastKey]) {
|
|
631
|
+
delete current[lastKey]
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Ensure the data property exists
|
|
638
|
+
if (!fallbackData.data) {
|
|
639
|
+
fallbackData.data = {}
|
|
640
|
+
}
|
|
403
641
|
|
|
404
|
-
|
|
642
|
+
return fallbackData
|
|
405
643
|
}
|
|
406
644
|
|
|
407
645
|
/**
|
|
@@ -2,76 +2,127 @@ import { App, TFile } from 'obsidian'
|
|
|
2
2
|
import { StrapiExporterSettings, AnalyzedContent } from '../types'
|
|
3
3
|
import { uploadImageToStrapi } from './strapi-uploader'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Types for link detection and processing
|
|
7
|
+
*/
|
|
8
|
+
interface LinkMatch {
|
|
9
|
+
type: 'link'
|
|
10
|
+
fullMatch: string
|
|
11
|
+
text: string
|
|
12
|
+
url: string
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
interface ImageMatch {
|
|
16
|
+
type: 'image'
|
|
6
17
|
fullMatch: string
|
|
7
18
|
altText: string
|
|
8
|
-
|
|
19
|
+
path: string
|
|
20
|
+
isWikiLink: boolean
|
|
9
21
|
}
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*/
|
|
14
|
-
export async function processImages(
|
|
15
|
-
content: AnalyzedContent,
|
|
16
|
-
app: App,
|
|
17
|
-
settings: StrapiExporterSettings
|
|
18
|
-
): Promise<AnalyzedContent> {
|
|
19
|
-
const processedContent = { ...content }
|
|
23
|
+
export function extractLinks(content: string): (LinkMatch | ImageMatch)[] {
|
|
24
|
+
const matches: (LinkMatch | ImageMatch)[] = []
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// Regular expressions for different link types
|
|
27
|
+
const patterns = {
|
|
28
|
+
wikiImage: /!\[\[([^\]]+\.(png|jpe?g|gif|svg|bmp|webp))\]\]/gi,
|
|
29
|
+
markdownImage: /!\[([^\]]*)\]\(([^)]+\.(png|jpe?g|gif|svg|bmp|webp))\)/gi,
|
|
30
|
+
standardLink: /(?<!!)\[([^\]]+)\]\(([^)]+)\)/gi, // Negative lookbehind to exclude image links
|
|
31
|
+
pastedImage: /\[\[Pasted image [0-9-]+\.png\]\]/gi,
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
// Process Wiki-style image links
|
|
35
|
+
let match
|
|
36
|
+
while ((match = patterns.wikiImage.exec(content)) !== null) {
|
|
37
|
+
matches.push({
|
|
38
|
+
type: 'image',
|
|
39
|
+
fullMatch: match[0],
|
|
40
|
+
altText: match[1].split('|')[1] || match[1].split('/').pop() || '',
|
|
41
|
+
path: match[1].split('|')[0],
|
|
42
|
+
isWikiLink: true,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Process Markdown-style image links
|
|
47
|
+
while ((match = patterns.markdownImage.exec(content)) !== null) {
|
|
48
|
+
matches.push({
|
|
49
|
+
type: 'image',
|
|
50
|
+
fullMatch: match[0],
|
|
51
|
+
altText: match[1],
|
|
52
|
+
path: match[2],
|
|
53
|
+
isWikiLink: false,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Process pasted images
|
|
58
|
+
while ((match = patterns.pastedImage.exec(content)) !== null) {
|
|
59
|
+
matches.push({
|
|
60
|
+
type: 'image',
|
|
61
|
+
fullMatch: match[0],
|
|
62
|
+
altText: 'Pasted image',
|
|
63
|
+
path: match[0].slice(2, -2), // Remove [[ and ]]
|
|
64
|
+
isWikiLink: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Process standard links (non-image)
|
|
69
|
+
while ((match = patterns.standardLink.exec(content)) !== null) {
|
|
70
|
+
matches.push({
|
|
71
|
+
type: 'link',
|
|
72
|
+
fullMatch: match[0],
|
|
73
|
+
text: match[1],
|
|
74
|
+
url: match[2],
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return matches
|
|
28
79
|
}
|
|
29
80
|
|
|
30
81
|
/**
|
|
31
|
-
* Process
|
|
82
|
+
* Process identified links and images
|
|
32
83
|
*/
|
|
33
|
-
async function
|
|
84
|
+
export async function processContentLinks(
|
|
34
85
|
content: string,
|
|
35
86
|
app: App,
|
|
36
87
|
settings: StrapiExporterSettings
|
|
37
88
|
): Promise<string> {
|
|
38
|
-
const imageMatches = extractImageMatches(content)
|
|
39
|
-
|
|
40
89
|
let processedContent = content
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
const matches = extractLinks(content)
|
|
91
|
+
|
|
92
|
+
for (const match of matches) {
|
|
93
|
+
if (match.type === 'image') {
|
|
94
|
+
const processedImage = await processImageMatch(match, app, settings)
|
|
95
|
+
if (processedImage) {
|
|
96
|
+
processedContent = processedContent.replace(
|
|
97
|
+
match.fullMatch,
|
|
98
|
+
processedImage
|
|
99
|
+
)
|
|
100
|
+
}
|
|
52
101
|
}
|
|
102
|
+
// Standard links are left unchanged
|
|
53
103
|
}
|
|
54
104
|
|
|
55
105
|
return processedContent
|
|
56
106
|
}
|
|
57
107
|
|
|
58
108
|
/**
|
|
59
|
-
*
|
|
109
|
+
* Process images in content fields and upload them to Strapi
|
|
60
110
|
*/
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
111
|
+
export async function processImages(
|
|
112
|
+
content: AnalyzedContent,
|
|
113
|
+
app: App,
|
|
114
|
+
settings: StrapiExporterSettings
|
|
115
|
+
): Promise<AnalyzedContent> {
|
|
116
|
+
const processedContent = { ...content }
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|
|
118
|
+
for (const [key, value] of Object.entries(processedContent)) {
|
|
119
|
+
if (typeof value === 'string') {
|
|
120
|
+
// Process all links and images in the content
|
|
121
|
+
processedContent[key] = await processContentLinks(value, app, settings)
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
124
|
|
|
74
|
-
return
|
|
125
|
+
return processedContent
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
/**
|
|
@@ -79,44 +130,41 @@ function extractImageMatches(content: string): ImageMatch[] {
|
|
|
79
130
|
*/
|
|
80
131
|
async function processImageMatch(
|
|
81
132
|
match: ImageMatch,
|
|
82
|
-
content: string,
|
|
83
133
|
app: App,
|
|
84
134
|
settings: StrapiExporterSettings
|
|
85
|
-
): Promise<string> {
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
): Promise<string | null> {
|
|
136
|
+
// Skip external URLs
|
|
137
|
+
if (isExternalUrl(match.path)) {
|
|
138
|
+
return null
|
|
88
139
|
}
|
|
89
140
|
|
|
90
|
-
const file = app.vault.getAbstractFileByPath(match.
|
|
141
|
+
const file = app.vault.getAbstractFileByPath(match.path)
|
|
91
142
|
if (!(file instanceof TFile)) {
|
|
92
|
-
return
|
|
143
|
+
return null
|
|
93
144
|
}
|
|
94
145
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (uploadedImage?.url) {
|
|
107
|
-
const newContent = content.replace(
|
|
108
|
-
match.fullMatch,
|
|
109
|
-
``
|
|
146
|
+
try {
|
|
147
|
+
const uploadedImage = await uploadImageToStrapi(
|
|
148
|
+
file,
|
|
149
|
+
file.name,
|
|
150
|
+
settings,
|
|
151
|
+
app,
|
|
152
|
+
{
|
|
153
|
+
alternativeText: match.altText || file.basename,
|
|
154
|
+
caption: match.altText || file.basename,
|
|
155
|
+
}
|
|
110
156
|
)
|
|
111
|
-
|
|
157
|
+
|
|
158
|
+
if (uploadedImage?.url) {
|
|
159
|
+
return ``
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`Failed to process image ${match.path}:`, error)
|
|
112
163
|
}
|
|
113
164
|
|
|
114
|
-
return
|
|
165
|
+
return null
|
|
115
166
|
}
|
|
116
167
|
|
|
117
|
-
/**
|
|
118
|
-
* Check if URL is external
|
|
119
|
-
*/
|
|
120
168
|
function isExternalUrl(url: string): boolean {
|
|
121
169
|
return url.startsWith('http://') || url.startsWith('https://')
|
|
122
170
|
}
|