mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
38 changed files with 559 additions and 199 deletions
@ -0,0 +1,112 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { markdownExtractImage, markdownTransformImages } from './markdown-transform'; |
|||
|
|||
describe('MarkdownTransform', () => { |
|||
it('should not extract image if markdown contains no image', () => { |
|||
const md = '# Header'; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toBeNull(); |
|||
}); |
|||
|
|||
it('should not extract image if URL is not valid', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toBeNull(); |
|||
}); |
|||
|
|||
it('should extract image', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'image.webp' }); |
|||
}); |
|||
|
|||
it('should extract image with name', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'my-picture.webp' }); |
|||
}); |
|||
|
|||
it('should extract image with lax name', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'picture.webp' }); |
|||
}); |
|||
|
|||
it('should extract image with alt', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownExtractImage(md); |
|||
|
|||
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'alt-text.webp' }); |
|||
}); |
|||
|
|||
it('should transform image url', async () => { |
|||
const md = ''; |
|||
|
|||
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`)); |
|||
|
|||
expect(result).toEqual(''); |
|||
}); |
|||
|
|||
it('should transform with name', async () => { |
|||
const md = ''; |
|||
|
|||
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`)); |
|||
|
|||
expect(result).toEqual(''); |
|||
}); |
|||
|
|||
it('should transform with lax name', async () => { |
|||
const md = ''; |
|||
|
|||
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`)); |
|||
|
|||
expect(result).toEqual(''); |
|||
}); |
|||
|
|||
it('should transform with alt', async () => { |
|||
const md = ''; |
|||
|
|||
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`)); |
|||
|
|||
expect(result).toEqual(''); |
|||
}); |
|||
|
|||
it('should transform multiple images', async () => { |
|||
const md = ` |
|||
# Header 1 |
|||
|
|||

|
|||

|
|||
|
|||
## Header 2 |
|||
`;
|
|||
|
|||
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`)); |
|||
|
|||
expect(result).toEqual(` |
|||
# Header 1 |
|||
|
|||

|
|||

|
|||
|
|||
## Header 2 |
|||
`);
|
|||
}); |
|||
}); |
|||
@ -0,0 +1,93 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import slugify from 'slugify'; |
|||
import { MathHelper } from './math-helper'; |
|||
|
|||
const IMAGE_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/; |
|||
const IMAGES_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/g; |
|||
|
|||
export type MarkdownImage = { url: string; name: string }; |
|||
|
|||
export function markdownHasImage(markdown: string) { |
|||
return !!markdown && !!markdown.match(IMAGES_REGEX); |
|||
} |
|||
|
|||
export function markdownExtractImage(markdown: string): MarkdownImage | null { |
|||
if (!markdown) { |
|||
return null; |
|||
} |
|||
|
|||
const match = markdown.match(IMAGE_REGEX); |
|||
|
|||
if (!match?.groups) { |
|||
return null; |
|||
} |
|||
|
|||
const { url, alt, name } = match.groups as { url: string; alt?: string; name?: string }; |
|||
|
|||
if (!isURL(url)) { |
|||
return null; |
|||
} |
|||
|
|||
return toImage({ url, alt, name }); |
|||
} |
|||
|
|||
export async function markdownTransformImages(markdown: string, replace: (image: MarkdownImage) => Promise<string>) { |
|||
if (!markdown) { |
|||
return markdown; |
|||
} |
|||
|
|||
const jobs: { id: string; url: string; name?: string; alt?: string }[] = []; |
|||
|
|||
let transformed = markdown.replace(IMAGES_REGEX, (_, alt, url, _other, name) => { |
|||
const id = MathHelper.guid(); |
|||
|
|||
jobs.push({ id, url, name, alt }); |
|||
return id; |
|||
}); |
|||
|
|||
const promises = jobs.map(async job => { |
|||
const url = await replace(toImage(job)); |
|||
|
|||
return { job, url }; |
|||
}); |
|||
|
|||
const results = await Promise.all(promises); |
|||
|
|||
for (const result of results) { |
|||
const { job, url } = result; |
|||
const name = job.name ? ` "${job.name}"` : ''; |
|||
|
|||
transformed = transformed.replace(result.job.id, ``); |
|||
} |
|||
|
|||
return transformed; |
|||
} |
|||
|
|||
const IMAGE_EXTENSIONS = ['.avif', '.jpeg', '.jpg', '.png', '.webp']; |
|||
|
|||
function toImage(image: { url: string; name?: string; alt?: string }): MarkdownImage { |
|||
let name = image.name || image.alt || 'image'; |
|||
|
|||
name = slugify(name, { lower: true, trim: true }); |
|||
|
|||
if (!IMAGE_EXTENSIONS.find(ex => name.endsWith(ex))) { |
|||
name += '.webp'; |
|||
} |
|||
|
|||
return { url: image.url, name }; |
|||
} |
|||
|
|||
function isURL(input: string) { |
|||
try { |
|||
new URL(input); |
|||
return true; |
|||
} catch { |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { markdownRender } from './markdown'; |
|||
|
|||
describe('Markdown', () => { |
|||
it('should render text', () => { |
|||
const md = 'Text'; |
|||
|
|||
const result = markdownRender(md, false); |
|||
|
|||
expect(result).toEqual('<p>Text</p>\n'); |
|||
}); |
|||
|
|||
it('should render text inline', () => { |
|||
const md = 'Text'; |
|||
|
|||
const result = markdownRender(md, true); |
|||
|
|||
expect(result).toEqual('Text'); |
|||
}); |
|||
|
|||
it('should render escaped', () => { |
|||
const md = '<h1>Header</h1>'; |
|||
|
|||
const result = markdownRender(md, false); |
|||
|
|||
expect(result).toEqual('<p><h1>Header</h1></p>\n'); |
|||
}); |
|||
|
|||
it('should render non escaped', () => { |
|||
const md = '<h1>Header</h1>'; |
|||
|
|||
const result = markdownRender(md, false, true); |
|||
|
|||
expect(result).toEqual('<h1>Header</h1>'); |
|||
}); |
|||
|
|||
it('should render mailto link', () => { |
|||
const md = '[mail](mailto:hello@squidex.io)'; |
|||
|
|||
const result = markdownRender(md, true); |
|||
|
|||
expect(result).toEqual('<a href="mailto:hello@squidex.io">mail</a>'); |
|||
}); |
|||
|
|||
it('should render normal link', () => { |
|||
const md = '[squidex](https://squidex.io)'; |
|||
|
|||
const result = markdownRender(md, true); |
|||
|
|||
expect(result).toEqual('<a href="https://squidex.io" target="_blank", rel="noopener">squidex <i class="icon-external-link"></i></a>'); |
|||
}); |
|||
|
|||
it('should render image', () => { |
|||
const md = ''; |
|||
|
|||
const result = markdownRender(md, true); |
|||
|
|||
expect(result).toEqual('<img src="https://localhost:5001/ai-images/dall-e/ea68c867-6472-4d77-a526-7c9d9c4698fe" alt="{name}">'); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue