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