From 5cecfe19011f9f67c21ef149c6b35d61b1c567ef Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Tue, 27 Oct 2026 20:26:59 +0100 Subject: [PATCH 1/5] perf(parse): stream script tags with htmlparser2 instead of building a himalaya AST himalaya tokenizes, builds a full node tree, and walks it just to find are found (himalaya dropped them) Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- src/index.js | 40 +++++++++++---------- test/index.js | 65 +++++++++++++++++++++++++++++++++++ test/snapshots/index.js.md | 2 +- test/snapshots/index.js.snap | Bin 464 -> 464 bytes 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 2787119..5a3f56d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "src" ], "dependencies": { - "himalaya": "~1.1.0" + "htmlparser2": "~10.1.0" }, "devDependencies": { "@commitlint/cli": "latest", diff --git a/src/index.js b/src/index.js index ba3df43..a14c008 100644 --- a/src/index.js +++ b/src/index.js @@ -1,28 +1,30 @@ 'use strict' -const { parse } = require('himalaya') +const { Parser } = require('htmlparser2') -const getValue = value => { - if (value === null) return true +const getValue = (value, quote) => { + if (quote === undefined) return true if (value === '1') return true return value } -const fromPairs = collection => - collection.reduce((acc, { key, value }) => { - acc[key] = getValue(value) - return acc - }, {}) - -const collectScripts = (nodes, acc = []) => { - for (const node of nodes) { - if (node.tagName === 'script') acc.push(node) - if (node.children) collectScripts(node.children, acc) - } - return acc -} - module.exports = html => { - const scripts = collectScripts(parse(html)) - return scripts.map(({ attributes }) => fromPairs(attributes)) + const scripts = [] + let attributes = null + let templateDepth = 0 + const parser = new Parser({ + onopentagname: name => { + if (name === 'template') templateDepth++ + attributes = name === 'script' && templateDepth === 0 ? {} : null + if (attributes) scripts.push(attributes) + }, + onattribute: (name, value, quote) => { + if (attributes) attributes[name] = getValue(value, quote) + }, + onclosetag: name => { + if (name === 'template') templateDepth-- + } + }) + parser.end(html) + return scripts } diff --git a/test/index.js b/test/index.js index 09a28e9..1e39d94 100644 --- a/test/index.js +++ b/test/index.js @@ -31,3 +31,68 @@ test('detect scripts from instagram iframe', t => { const scripts = parseIframe(html) t.snapshot(scripts) }) + +test('lowercase attribute names', t => { + t.deepEqual(parseIframe(''), [ + { src: '/a.js', async: true } + ]) +}) + +test('decode entities in attribute values', t => { + t.deepEqual( + parseIframe(''), + [{ src: '/a.js?x=1&y=<2>' }] + ) +}) + +test('map boolean and "1" attribute values to true', t => { + t.deepEqual( + parseIframe( + '' + ), + [{ async: true, defer: true, 'data-empty': '', 'data-zero': '0' }] + ) +}) + +test('keep the last value of a repeated attribute', t => { + t.deepEqual(parseIframe(''), [ + { src: '/b.js' } + ]) +}) + +test('keep unquoted attribute values containing slashes', t => { + t.deepEqual( + parseIframe( + '' + ), + [ + { src: 'https://cdn.example.com/a.js', async: true }, + { type: 'module', src: '/m.js' } + ] + ) +}) + +test('detect scripts after a stray closing script tag', t => { + t.deepEqual( + parseIframe(''), + [{}, { src: '/b.js' }] + ) +}) + +test('ignore markup inside script content and comments', t => { + t.deepEqual( + parseIframe( + '' + ), + [{}, { src: '/d.js' }] + ) +}) + +test('ignore scripts inside template', t => { + t.deepEqual( + parseIframe( + '' + ), + [{ src: '/after.js' }] + ) +}) diff --git a/test/snapshots/index.js.md b/test/snapshots/index.js.md index 064df99..f49b5b9 100644 --- a/test/snapshots/index.js.md +++ b/test/snapshots/index.js.md @@ -25,7 +25,7 @@ Generated by [AVA](https://avajs.dev). async: true, crossorigin: 'anonymous', defer: true, - src: 'https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v5.0', + src: 'https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v5.0', }, ] diff --git a/test/snapshots/index.js.snap b/test/snapshots/index.js.snap index f7325ff23705a63f6b14fc36ab4a0a7e8fb8f498..e7c9a6a3721b40ef4a3b1e7b9d99388eb55bb83b 100644 GIT binary patch literal 464 zcmV;>0WbbRRzViVK@bPX~s)G7a$8|Z0X3XDzT5&S5 z2M;PM)<~aCa#P`coucnc<3w!gyEtwRiR zA7{MWsIs+9s~cU!ecR%i7=ksTS|}B)6gGVM_@UrxtVXC$%*4Z_zFB07^il%AQVw&@{v%a*I8u1B8vn$uJ G0{{R+qve7C literal 464 zcmV;>0WbbRRzV?l>x!^ldS}{HYa->_y7a;*jaF3Hb%f{FMIlK1{MU%)qQ?XC5U;b{VlBh$ zM`-@=UW@>Omn2C9ixEHsO8|QzX{N{aly$|r4au{I`3d&|@z`y(S`mDRpb Date: Tue, 27 Oct 2026 20:59:18 +0100 Subject: [PATCH 2/5] docs: boolean attributes come out as true Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55014c1..ef73502 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ const scripts = getScripts('