Skip to content

Commit

Permalink
Add support for inline image (#256)
Browse files Browse the repository at this point in the history
Co-authored-by: Robert Kozik <[email protected]>
  • Loading branch information
tomekzaw and robertKozik authored Apr 9, 2024
1 parent 48afbe1 commit bb63ffb
Show file tree
Hide file tree
Showing 4 changed files with 647 additions and 41 deletions.
138 changes: 138 additions & 0 deletions parser/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,141 @@ describe('trailing whitespace', () => {
});
});
});

describe('inline image', () => {
test('with alt text', () => {
expect('![test](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 6, length: 1},
{type: 'syntax', start: 7, length: 1},
{type: 'link', start: 8, length: 29},
{type: 'syntax', start: 37, length: 1},
]);
});

test('without alt text', () => {
expect('![](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 2, length: 1},
{type: 'syntax', start: 3, length: 1},
{type: 'link', start: 4, length: 29},
{type: 'syntax', start: 33, length: 1},
]);
});

test('with same alt text as src', () => {
expect('![https://example.com/image.png](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 31, length: 1},
{type: 'syntax', start: 32, length: 1},
{type: 'link', start: 33, length: 29},
{type: 'syntax', start: 62, length: 1},
]);
});

test('text containing images', () => {
expect('An image of a banana: ![banana](https://example.com/banana.png) an image of a developer: ![dev](https://example.com/developer.png)').toBeParsedAs([
{type: 'syntax', start: 22, length: 1},
{type: 'syntax', start: 23, length: 1},
{type: 'syntax', start: 30, length: 1},
{type: 'syntax', start: 31, length: 1},
{type: 'link', start: 32, length: 30},
{type: 'syntax', start: 62, length: 1},
{type: 'syntax', start: 89, length: 1},
{type: 'syntax', start: 90, length: 1},
{type: 'syntax', start: 94, length: 1},
{type: 'syntax', start: 95, length: 1},
{type: 'link', start: 96, length: 33},
{type: 'syntax', start: 129, length: 1},
]);
});

test('with alt text containing markdown', () => {
expect('![# fake-heading *bold* _italic_ ~strike~ [:-)]](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 47, length: 1},
{type: 'syntax', start: 48, length: 1},
{type: 'link', start: 49, length: 29},
{type: 'syntax', start: 78, length: 1},
]);
});

test('text containing image and autolink', () => {
expect('An image of a banana: ![banana](https://example.com/banana.png) an autolink: example.com').toBeParsedAs([
{type: 'syntax', start: 22, length: 1},
{type: 'syntax', start: 23, length: 1},
{type: 'syntax', start: 30, length: 1},
{type: 'syntax', start: 31, length: 1},
{type: 'link', start: 32, length: 30},
{type: 'syntax', start: 62, length: 1},
{type: 'link', start: 77, length: 11},
]);
});

test('with invalid url', () => {
expect('![test](invalid)').toBeParsedAs([]);
});

test('trying to pass additional attributes', () => {
expect('![test](https://example.com/image.png "title" class="image")').toBeParsedAs([{type: 'link', start: 8, length: 29}]);
});

test('trying to inject additional attributes', () => {
expect('![test" onerror="alert(\'xss\')](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 29, length: 1},
{type: 'syntax', start: 30, length: 1},
{type: 'link', start: 31, length: 29},
{type: 'syntax', start: 60, length: 1},
]);
});

test('inline code in alt', () => {
expect('![`code`](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 8, length: 1},
{type: 'syntax', start: 9, length: 1},
{type: 'link', start: 10, length: 29},
{type: 'syntax', start: 39, length: 1},
]);
});

test('blockquote in alt', () => {
expect('![```test```](https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 12, length: 1},
{type: 'syntax', start: 13, length: 1},
{type: 'link', start: 14, length: 29},
{type: 'syntax', start: 43, length: 1},
]);
});

test('image without alt text', () => {
expect('!(https://example.com/image.png)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'link', start: 2, length: 29},
{type: 'syntax', start: 31, length: 1},
]);
});

test('image with empty alt text and not completed link', () => {
expect('# ![](example.com)').toBeParsedAs([
{type: 'syntax', start: 0, length: 2},
{type: 'h1', start: 2, length: 12},
{type: 'syntax', start: 2, length: 1},
{type: 'syntax', start: 3, length: 1},
{type: 'syntax', start: 4, length: 1},
{type: 'syntax', start: 5, length: 1},
{type: 'link', start: 6, length: 7},
{type: 'syntax', start: 13, length: 1},
]);
});
});
22 changes: 21 additions & 1 deletion parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ function parseTokensToTree(tokens: Token[]): StackItem {
const child = stack.pop();
const top = stack[stack.length - 1];
top!.children.push(child!);
} else if (payload.endsWith('/>')) {
// self-closing tag
const top = stack[stack.length - 1];
top!.children.push({tag: payload, children: []});
} else {
// opening tag
stack.push({tag: payload, children: []});
Expand Down Expand Up @@ -146,7 +150,7 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, Range[]] {
} else if (node.tag.startsWith('<a href="')) {
const rawHref = node.tag.match(/href="([^"]*)"/)![1]!; // always present
const href = _.unescape(rawHref);
const isLabeledLink = node.tag.match(/link-variant="([^"]*)"/)![1] === 'labeled';
const isLabeledLink = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
const dataRawHref = node.tag.match(/data-raw-href="([^"]*)"/);
const matchString = dataRawHref ? _.unescape(dataRawHref[1]!) : href;
if (!isLabeledLink && node.children.length === 1 && typeof node.children[0] === 'string' && (node.children[0] === matchString || `mailto:${node.children[0]}` === href)) {
Expand All @@ -158,6 +162,22 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, Range[]] {
addChildrenWithStyle(matchString, 'link');
appendSyntax(')');
}
} else if (node.tag.startsWith('<img src="')) {
const src = node.tag.match(/src="([^"]*)"/)![1]!; // always present
const alt = node.tag.match(/alt="([^"]*)"/);
const hasAlt = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
const rawLink = node.tag.match(/data-raw-href="([^"]*)"/);
const linkString = rawLink ? _.unescape(rawLink[1]!) : src;

appendSyntax('!');
if (hasAlt) {
appendSyntax('[');
processChildren(_.unescape(alt?.[1] || ''));
appendSyntax(']');
}
appendSyntax('(');
addChildrenWithStyle(linkString, 'link');
appendSyntax(')');
} else {
throw new Error(`Unknown tag: ${node.tag}`);
}
Expand Down
Loading

0 comments on commit bb63ffb

Please sign in to comment.