From 7a6d98497717be5e38c7e3203fedffd25145038b Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Wed, 29 May 2024 23:33:37 +0300 Subject: [PATCH] fix(html-parser): interpolating inside attribs --- packages/html-parser/src/html-parser.test.ts | 35 ++++++++++++++++++ packages/html-parser/src/index.ts | 38 ++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/html-parser/src/html-parser.test.ts b/packages/html-parser/src/html-parser.test.ts index c780ff79..75cc64df 100644 --- a/packages/html-parser/src/html-parser.test.ts +++ b/packages/html-parser/src/html-parser.test.ts @@ -501,6 +501,41 @@ describe('HtmlMessageEntityParser', () => { ) }) + it('should handle interpolation into attrs', () => { + test( + htm`link`, + [ + createEntity('messageEntityTextUrl', 0, 4, { + url: 'https://example.com/"foo/bar/baz?foo=bar&baz=egg', + }), + ], + 'link', + ) + test( + // at the same time testing that non-quoted attributes work + htm`user`, + [ + createEntity('inputMessageEntityMentionName', 0, 4, { + userId: { + _: 'inputUser', + userId: 1234567, + accessHash: Long.fromString('aabbccddaabbccdd', 16), + }, + }), + ], + 'user', + ) + test( + htm`🚀`, + [ + createEntity('messageEntityCustomEmoji', 0, 2, { + documentId: Long.fromString('123123123123'), + }), + ], + '🚀', + ) + }) + it('should skip falsy values', () => { test(htm`some text ${null} some ${false} more text`, [], 'some text some more text') }) diff --git a/packages/html-parser/src/index.ts b/packages/html-parser/src/index.ts index a6c7752c..34d30af4 100644 --- a/packages/html-parser/src/index.ts +++ b/packages/html-parser/src/index.ts @@ -27,6 +27,8 @@ function parse( let plainText = '' let pendingText = '' + let isInsideAttrib = false + function processPendingText(tagEnd = false, keepWhitespace = false) { if (!pendingText.length) return @@ -212,8 +214,30 @@ function parse( ontext(data) { pendingText += data }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any }) + // a hack for interpolating inside attributes + // instead of hacking into the parser itself (which would require a lot of work + // and test coverage because of the number of edge cases), we'll just feed + // an escaped version of the text right to the parser. + // however, to do that we need to know if we are inside an attribute or not, + // and htmlparser2 doesn't really expose that. + // it only exposes .onattribute, which isn't really useful here, as + // we want to know if we are mid-attribute or not + const onattribname = parser.onattribname + const onattribend = parser.onattribend + + parser.onattribname = function (name) { + onattribname.call(this, name) + isInsideAttrib = true + } + + parser.onattribend = function (quote) { + onattribend.call(this, quote) + isInsideAttrib = false + } + if (typeof strings === 'string') strings = [strings] as unknown as TemplateStringsArray sub.forEach((it, idx) => { @@ -221,6 +245,20 @@ function parse( if (typeof it === 'boolean' || !it) return + if (isInsideAttrib) { + let text: string + + if (typeof it === 'string') text = it + else if (typeof it === 'number') text = it.toString() + else { + // obviously we can't have entities inside attributes, so just use the text + text = it.text + } + parser.write(escape(text, true)) + + return + } + if (typeof it === 'string' || typeof it === 'number') { pendingText += it } else {