Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ const defaultHtmlOpts = {
textElements: ['label', 'h1', 'h2'],
allowedAttrs: ['id', 'for', 'class', 'name', 'type', 'value', 'tabindex', 'aria-labelledby', 'aria-label', 'label', 'placeholder', 'title', 'alt', 'src', 'role'],
allowedRoles: ['button', 'checkbox', 'search', 'textbox', 'tab'],
keepText: false,
}

function removeNonInteractiveElements(html, opts = {}) {
opts = { ...defaultHtmlOpts, ...opts }
const { interactiveElements, textElements, allowedAttrs, allowedRoles } = opts
const { interactiveElements, textElements, allowedAttrs, allowedRoles, keepText } = opts

// Parse the HTML into a document tree
const document = parse(html)
Expand Down Expand Up @@ -111,8 +112,14 @@ function removeNonInteractiveElements(html, opts = {}) {
return false
}

function hasVisibleText(node) {
if (node.nodeName === '#text') return !!node.value.trim()
return (node.childNodes || []).some(hasVisibleText)
}

function hasMeaningfulText(node) {
if (textElements.includes(node.nodeName)) return true
if (keepText && hasVisibleText(node)) return true
return false
}

Expand Down Expand Up @@ -294,7 +301,7 @@ function splitByChunks(text, chunkSize) {

function simplifyHtmlElement(html, maxLength = 300) {
try {
html = removeNonInteractiveElements(html)
html = removeNonInteractiveElements(html, { keepText: true })
html = html.replace(/<html>(?:<head>.*?<\/head>)?<body>(.*)<\/body><\/html>/s, '$1').trim()
} catch (e) {
// keep raw html if minification fails
Expand Down
33 changes: 32 additions & 1 deletion test/unit/html_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'path'
import { expect } from 'chai'
import { fileURLToPath } from 'url'
import * as cheerio from 'cheerio'
import { scanForErrorMessages, removeNonInteractiveElements, minifyHtml, splitByChunks, cleanHtml, formatHtml, isTrashClass } from '../../lib/html.js'
import { scanForErrorMessages, removeNonInteractiveElements, minifyHtml, splitByChunks, cleanHtml, formatHtml, isTrashClass, simplifyHtmlElement } from '../../lib/html.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
Expand Down Expand Up @@ -224,4 +224,35 @@ describe('HTML module', () => {
expect(out).to.include('<span>hi</span>')
})
})

describe('#simplifyHtmlElement', () => {
const button = label =>
'<button type="button"><span class="content inline-flex items-center gap-3 w-full">' +
'<span class="badge badge-type manual"><svg class="md-icon md-icon-file-document-outline"></svg></span>' +
`<span>${label}</span></span></button>`

it('keeps the visible label when it is nested in non-interactive elements', () => {
const out = simplifyHtmlElement(button('New test'))
expect(out).to.include('New test')
expect(out).to.include('<button type="button">')
})

it('keeps elements with different labels distinguishable', () => {
expect(simplifyHtmlElement(button('New test'))).not.to.equal(simplifyHtmlElement(button('New tests from requirement')))
})

it('drops nested elements without text', () => {
expect(simplifyHtmlElement(button('New test'))).not.to.include('<svg')
})

it('truncates to maxLength', () => {
const out = simplifyHtmlElement(button('New test'), 50)
expect(out).to.have.length(53)
expect(out.endsWith('...')).to.be.true
})

it('does not change removeNonInteractiveElements by default', () => {
expect(removeNonInteractiveElements(button('New test'))).not.to.include('New test')
})
})
})