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 {