Skip to content

Commit dc045dc

Browse files
DavertMikclaude
andcommitted
fix: keep element text in simplifyHtmlElement
removeNonInteractiveElements dropped non-interactive descendants, so an interactive element whose label lives in nested spans was serialized as an empty shell. Add an opt-in keepText option, off by default, and use it from simplifyHtmlElement which describes a single element. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JbhzmY1M51pyRysUx32MdP
1 parent eb1bcdc commit dc045dc

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

lib/html.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@ const defaultHtmlOpts = {
7878
textElements: ['label', 'h1', 'h2'],
7979
allowedAttrs: ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'role'],
8080
allowedRoles: ['button', 'checkbox', 'search', 'textbox', 'tab'],
81+
keepText: false,
8182
}
8283

8384
function removeNonInteractiveElements(html, opts = {}) {
8485
opts = { ...defaultHtmlOpts, ...opts }
85-
const { interactiveElements, textElements, allowedAttrs, allowedRoles } = opts
86+
const { interactiveElements, textElements, allowedAttrs, allowedRoles, keepText } = opts
8687

8788
// Parse the HTML into a document tree
8889
const document = parse(html)
@@ -111,8 +112,14 @@ function removeNonInteractiveElements(html, opts = {}) {
111112
return false
112113
}
113114

115+
function hasVisibleText(node) {
116+
if (node.nodeName === '#text') return !!node.value.trim()
117+
return (node.childNodes || []).some(hasVisibleText)
118+
}
119+
114120
function hasMeaningfulText(node) {
115121
if (textElements.includes(node.nodeName)) return true
122+
if (keepText && hasVisibleText(node)) return true
116123
return false
117124
}
118125

@@ -294,7 +301,7 @@ function splitByChunks(text, chunkSize) {
294301

295302
function simplifyHtmlElement(html, maxLength = 300) {
296303
try {
297-
html = removeNonInteractiveElements(html)
304+
html = removeNonInteractiveElements(html, { keepText: true })
298305
html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
299306
} catch (e) {
300307
// keep raw html if minification fails

test/unit/html_test.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path'
33
import { expect } from 'chai'
44
import { fileURLToPath } from 'url'
55
import * as cheerio from 'cheerio'
6-
import { scanForErrorMessages, removeNonInteractiveElements, minifyHtml, splitByChunks, cleanHtml, formatHtml, isTrashClass } from '../../lib/html.js'
6+
import { scanForErrorMessages, removeNonInteractiveElements, minifyHtml, splitByChunks, cleanHtml, formatHtml, isTrashClass, simplifyHtmlElement } from '../../lib/html.js'
77

88
const __filename = fileURLToPath(import.meta.url)
99
const __dirname = path.dirname(__filename)
@@ -224,4 +224,35 @@ describe('HTML module', () => {
224224
expect(out).to.include('<span>hi</span>')
225225
})
226226
})
227+
228+
describe('#simplifyHtmlElement', () => {
229+
const button = label =>
230+
'<button type="button"><span class="content inline-flex items-center gap-3 w-full">' +
231+
'<span class="badge badge-type manual"><svg class="md-icon md-icon-file-document-outline"></svg></span>' +
232+
`<span>${label}</span></span></button>`
233+
234+
it('keeps the visible label when it is nested in non-interactive elements', () => {
235+
const out = simplifyHtmlElement(button('New test'))
236+
expect(out).to.include('New test')
237+
expect(out).to.include('<button type="button">')
238+
})
239+
240+
it('keeps elements with different labels distinguishable', () => {
241+
expect(simplifyHtmlElement(button('New test'))).not.to.equal(simplifyHtmlElement(button('New tests from requirement')))
242+
})
243+
244+
it('drops nested elements without text', () => {
245+
expect(simplifyHtmlElement(button('New test'))).not.to.include('<svg')
246+
})
247+
248+
it('truncates to maxLength', () => {
249+
const out = simplifyHtmlElement(button('New test'), 50)
250+
expect(out).to.have.length(53)
251+
expect(out.endsWith('...')).to.be.true
252+
})
253+
254+
it('does not change removeNonInteractiveElements by default', () => {
255+
expect(removeNonInteractiveElements(button('New test'))).not.to.include('New test')
256+
})
257+
})
227258
})

0 commit comments

Comments
 (0)