Skip to content

Commit 0e970bc

Browse files
committed
Refactor Markdown regexes to constants module
1 parent c7e4bf4 commit 0e970bc

3 files changed

Lines changed: 250 additions & 28 deletions

File tree

src/lib/services/contents/widgets/markdown/components/definitions.js

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import {
55
encodeQuotes,
66
replaceQuotes,
77
} from '$lib/services/contents/widgets/markdown/components/utils';
8+
import {
9+
IMAGE_OR_LINKED_IMAGE_REGEX,
10+
IMAGE_REGEX,
11+
} from '$lib/services/contents/widgets/markdown/constants';
812

913
/**
1014
* @import { EditorComponentDefinition } from '$lib/types/public';
@@ -16,33 +20,6 @@ import {
1620
*/
1721
export const customComponentRegistry = new Map();
1822

19-
/**
20-
* Regular expression to match Markdown images, including those with spaces and brackets in the src,
21-
* e.g. `![alt text](image.jpg "Image title")`. It also matches images with empty alt text, e.g.
22-
* `![](image.jpg)`, images with parentheses in the filename, e.g. `![alt](image (1).jpg)`, and
23-
* supports escaped characters like `![alt](image\(1\).jpg)` and titles with escaped quotes.
24-
* @type {RegExp}
25-
*/
26-
export const IMAGE_REGEX =
27-
/!\[(?<alt>(?:[^\]\\]|\\.)*)\]\((?<src>(?:[^"()\\]|\\.|\([^)]*\)|"[^"]*")*?)(?:\s+"(?<title>(?:[^"\\]|\\.)*)")?\)/;
28-
29-
/**
30-
* Regular expression to match Markdown linked images, including those with spaces and brackets in
31-
* the src, e.g. `[![alt text](image.jpg "Image title")](link)`. It also matches linked images with
32-
* parentheses in the filename, e.g. `[![alt](image (1).jpg)](https://example.com)`.
33-
* @type {RegExp}
34-
*/
35-
export const LINKED_IMAGE_REGEX =
36-
/\[!\[(?<alt2>(?:[^\]\\]|\\.)*)\]\((?<src2>(?:[^"()\\]|\\.|\([^)]*\)|"[^"]*")*?)(?:\s+"(?<title2>(?:[^"\\]|\\.)*)")?\)\](?:\((?<link>[^)]*\([^)]*\)[^)]*|[^)]*)\))/;
37-
38-
/**
39-
* Regular expression to match either a Markdown image or a linked image.
40-
* @type {RegExp}
41-
*/
42-
export const IMAGE_OR_LINKED_IMAGE_REGEX = new RegExp(
43-
`${IMAGE_REGEX.source}|${LINKED_IMAGE_REGEX.source}`,
44-
);
45-
4623
/**
4724
* Built-in image component definition. The labels are localized in `getComponentDef()`.
4825
* @type {EditorComponentDefinition}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Regular expression to match Markdown images, including those with spaces and brackets in the src,
3+
* e.g. `![alt text](image.jpg "Image title")`. It also matches images with empty alt text, e.g.
4+
* `![](image.jpg)`, images with parentheses in the filename, e.g. `![alt](image (1).jpg)`, and
5+
* supports escaped characters like `![alt](image\(1\).jpg)` and titles with escaped quotes.
6+
* @type {RegExp}
7+
*/
8+
export const IMAGE_REGEX =
9+
/!\[(?<alt>(?:[^\]\\]|\\.)*)\]\((?<src>(?:[^"()\\]|\\.|\([^)]*\)|"[^"]*")*?)(?:\s+"(?<title>(?:[^"\\]|\\.)*)")?\)/;
10+
11+
/**
12+
* Global version of `IMAGE_REGEX` (with the `g` flag).
13+
* @type {RegExp}
14+
*/
15+
export const GLOBAL_IMAGE_REGEX = new RegExp(IMAGE_REGEX, 'g');
16+
17+
/**
18+
* Regular expression to match Markdown linked images, including those with spaces and brackets in
19+
* the src, e.g. `[![alt text](image.jpg "Image title")](link)`. It also matches linked images with
20+
* parentheses in the filename, e.g. `[![alt](image (1).jpg)](https://example.com)`.
21+
* @type {RegExp}
22+
*/
23+
export const LINKED_IMAGE_REGEX =
24+
/\[!\[(?<alt2>(?:[^\]\\]|\\.)*)\]\((?<src2>(?:[^"()\\]|\\.|\([^)]*\)|"[^"]*")*?)(?:\s+"(?<title2>(?:[^"\\]|\\.)*)")?\)\](?:\((?<link>[^)]*\([^)]*\)[^)]*|[^)]*)\))/;
25+
26+
/**
27+
* Regular expression to match either a Markdown image or a linked image.
28+
* @type {RegExp}
29+
*/
30+
export const IMAGE_OR_LINKED_IMAGE_REGEX = new RegExp(
31+
`${IMAGE_REGEX.source}|${LINKED_IMAGE_REGEX.source}`,
32+
);

src/lib/services/contents/widgets/markdown/components/definitions.test.js renamed to src/lib/services/contents/widgets/markdown/constants.test.js

Lines changed: 214 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, expect, test } from 'vitest';
22

3-
import { IMAGE_OR_LINKED_IMAGE_REGEX, IMAGE_REGEX } from './definitions.js';
3+
import {
4+
GLOBAL_IMAGE_REGEX,
5+
IMAGE_OR_LINKED_IMAGE_REGEX,
6+
IMAGE_REGEX,
7+
LINKED_IMAGE_REGEX,
8+
} from './constants.js';
49

510
describe('Test IMAGE_REGEX', () => {
611
test('matches simple image markdown syntax', () => {
@@ -229,3 +234,211 @@ describe('Test regex basic functionality', () => {
229234
expect(match?.groups?.alt).toBe('alt with spaces');
230235
});
231236
});
237+
238+
describe('Test GLOBAL_IMAGE_REGEX', () => {
239+
test('matches multiple images in a single string', () => {
240+
const markdown = `
241+
Here is the first image: ![first](image1.jpg)
242+
And here is the second: ![second](image2.png "Title")
243+
Finally, here's the third: ![third](path/to/image3.gif)
244+
`.trim();
245+
246+
const matches = Array.from(markdown.matchAll(GLOBAL_IMAGE_REGEX));
247+
248+
expect(matches).toHaveLength(3);
249+
250+
expect(matches[0]?.groups?.alt).toBe('first');
251+
expect(matches[0]?.groups?.src).toBe('image1.jpg');
252+
expect(matches[0]?.groups?.title).toBeUndefined();
253+
254+
expect(matches[1]?.groups?.alt).toBe('second');
255+
expect(matches[1]?.groups?.src).toBe('image2.png');
256+
expect(matches[1]?.groups?.title).toBe('Title');
257+
258+
expect(matches[2]?.groups?.alt).toBe('third');
259+
expect(matches[2]?.groups?.src).toBe('path/to/image3.gif');
260+
expect(matches[2]?.groups?.title).toBeUndefined();
261+
});
262+
263+
test('matches images with complex filenames globally', () => {
264+
const markdown = `
265+
![screenshot](assets/screenshot (2024-01-01).png)
266+
![backup](backup\\(1\\).jpg "Backup image")
267+
![test](file (1) (copy).webp)
268+
`.trim();
269+
270+
const matches = Array.from(markdown.matchAll(GLOBAL_IMAGE_REGEX));
271+
272+
expect(matches).toHaveLength(3);
273+
274+
expect(matches[0]?.groups?.alt).toBe('screenshot');
275+
expect(matches[0]?.groups?.src).toBe('assets/screenshot (2024-01-01).png');
276+
277+
expect(matches[1]?.groups?.alt).toBe('backup');
278+
expect(matches[1]?.groups?.src).toBe('backup\\(1\\).jpg');
279+
expect(matches[1]?.groups?.title).toBe('Backup image');
280+
281+
expect(matches[2]?.groups?.alt).toBe('test');
282+
expect(matches[2]?.groups?.src).toBe('file (1) (copy).webp');
283+
});
284+
285+
test('handles mixed content with images and text', () => {
286+
const markdown = `
287+
# My Document
288+
289+
This is a paragraph with an image ![inline](inline.jpg) in the middle.
290+
291+
## Gallery
292+
293+
Here are some images:
294+
- ![first](gallery/first.png "First image")
295+
- ![second](gallery/second.jpg)
296+
297+
And some ![more](assets/more.gif "More images") content.
298+
`.trim();
299+
300+
const matches = Array.from(markdown.matchAll(GLOBAL_IMAGE_REGEX));
301+
302+
expect(matches).toHaveLength(4);
303+
304+
expect(matches[0]?.groups?.alt).toBe('inline');
305+
expect(matches[0]?.groups?.src).toBe('inline.jpg');
306+
307+
expect(matches[1]?.groups?.alt).toBe('first');
308+
expect(matches[1]?.groups?.src).toBe('gallery/first.png');
309+
expect(matches[1]?.groups?.title).toBe('First image');
310+
311+
expect(matches[2]?.groups?.alt).toBe('second');
312+
expect(matches[2]?.groups?.src).toBe('gallery/second.jpg');
313+
314+
expect(matches[3]?.groups?.alt).toBe('more');
315+
expect(matches[3]?.groups?.src).toBe('assets/more.gif');
316+
expect(matches[3]?.groups?.title).toBe('More images');
317+
});
318+
319+
test('returns empty array when no images found', () => {
320+
const markdown =
321+
'# No Images Here\n\nThis is just text without any images.\nSome code and a link: [link text](https://example.com)';
322+
323+
const matches = Array.from(markdown.matchAll(GLOBAL_IMAGE_REGEX));
324+
325+
expect(matches).toHaveLength(0);
326+
});
327+
328+
test('handles edge cases with multiple images on same line', () => {
329+
const markdown = 'Before ![first](a.jpg) middle ![second](b.png "title") after';
330+
const matches = Array.from(markdown.matchAll(GLOBAL_IMAGE_REGEX));
331+
332+
expect(matches).toHaveLength(2);
333+
334+
expect(matches[0]?.groups?.alt).toBe('first');
335+
expect(matches[0]?.groups?.src).toBe('a.jpg');
336+
337+
expect(matches[1]?.groups?.alt).toBe('second');
338+
expect(matches[1]?.groups?.src).toBe('b.png');
339+
expect(matches[1]?.groups?.title).toBe('title');
340+
});
341+
});
342+
343+
describe('Test LINKED_IMAGE_REGEX', () => {
344+
test('matches basic linked image', () => {
345+
const markdown = '[![alt text](image.jpg)](https://example.com)';
346+
const match = markdown.match(LINKED_IMAGE_REGEX);
347+
348+
expect(match).toBeTruthy();
349+
expect(match?.groups?.alt2).toBe('alt text');
350+
expect(match?.groups?.src2).toBe('image.jpg');
351+
expect(match?.groups?.title2).toBeUndefined();
352+
expect(match?.groups?.link).toBe('https://example.com');
353+
});
354+
355+
test('matches linked image with title', () => {
356+
const markdown = '[![alt](image.jpg "Image title")](https://example.com)';
357+
const match = markdown.match(LINKED_IMAGE_REGEX);
358+
359+
expect(match).toBeTruthy();
360+
expect(match?.groups?.alt2).toBe('alt');
361+
expect(match?.groups?.src2).toBe('image.jpg');
362+
expect(match?.groups?.title2).toBe('Image title');
363+
expect(match?.groups?.link).toBe('https://example.com');
364+
});
365+
366+
test('matches linked image with parentheses in filename', () => {
367+
const markdown = '[![alt](image (1).jpg)](https://example.com)';
368+
const match = markdown.match(LINKED_IMAGE_REGEX);
369+
370+
expect(match).toBeTruthy();
371+
expect(match?.groups?.alt2).toBe('alt');
372+
expect(match?.groups?.src2).toBe('image (1).jpg');
373+
expect(match?.groups?.link).toBe('https://example.com');
374+
});
375+
376+
test('matches linked image with parentheses in link URL', () => {
377+
const markdown = '[![alt](image.jpg)](https://example.com/page(1))';
378+
const match = markdown.match(LINKED_IMAGE_REGEX);
379+
380+
expect(match).toBeTruthy();
381+
expect(match?.groups?.alt2).toBe('alt');
382+
expect(match?.groups?.src2).toBe('image.jpg');
383+
expect(match?.groups?.link).toBe('https://example.com/page(1)');
384+
});
385+
386+
test('matches linked image with complex nested parentheses', () => {
387+
const markdown = '[![test](file (1) (copy).jpg)](https://example.com/gallery(main))';
388+
const match = markdown.match(LINKED_IMAGE_REGEX);
389+
390+
expect(match).toBeTruthy();
391+
expect(match?.groups?.alt2).toBe('test');
392+
expect(match?.groups?.src2).toBe('file (1) (copy).jpg');
393+
expect(match?.groups?.link).toBe('https://example.com/gallery(main)');
394+
});
395+
396+
test('matches linked image with escaped characters', () => {
397+
const markdown =
398+
'[![alt with \\[brackets\\]](image\\(1\\).jpg "Title with \\"quotes\\"")](https://example.com)';
399+
400+
const match = markdown.match(LINKED_IMAGE_REGEX);
401+
402+
expect(match).toBeTruthy();
403+
expect(match?.groups?.alt2).toBe('alt with \\[brackets\\]');
404+
expect(match?.groups?.src2).toBe('image\\(1\\).jpg');
405+
expect(match?.groups?.title2).toBe('Title with \\"quotes\\"');
406+
expect(match?.groups?.link).toBe('https://example.com');
407+
});
408+
409+
test('does not match regular image without link', () => {
410+
const markdown = '![alt text](image.jpg)';
411+
const match = markdown.match(LINKED_IMAGE_REGEX);
412+
413+
expect(match).toBe(null);
414+
});
415+
416+
test('does not match malformed linked image', () => {
417+
const markdown = '[![alt](image.jpg)[link]'; // Missing closing parentheses
418+
const match = markdown.match(LINKED_IMAGE_REGEX);
419+
420+
expect(match).toBe(null);
421+
});
422+
423+
test('matches linked image with empty alt text', () => {
424+
const markdown = '[![](image.jpg)](https://example.com)';
425+
const match = markdown.match(LINKED_IMAGE_REGEX);
426+
427+
expect(match).toBeTruthy();
428+
expect(match?.groups?.alt2).toBe('');
429+
expect(match?.groups?.src2).toBe('image.jpg');
430+
expect(match?.groups?.link).toBe('https://example.com');
431+
});
432+
433+
test('handles complex URLs with query parameters', () => {
434+
const markdown =
435+
'[![alt](https://cdn.example.com/image.jpg?v=1&size=large)](https://example.com/page?id=1&ref=gallery)';
436+
437+
const match = markdown.match(LINKED_IMAGE_REGEX);
438+
439+
expect(match).toBeTruthy();
440+
expect(match?.groups?.alt2).toBe('alt');
441+
expect(match?.groups?.src2).toBe('https://cdn.example.com/image.jpg?v=1&size=large');
442+
expect(match?.groups?.link).toBe('https://example.com/page?id=1&ref=gallery');
443+
});
444+
});

0 commit comments

Comments
 (0)