Skip to content

Commit

Permalink
feat: evaluate headers near form element
Browse files Browse the repository at this point in the history
  • Loading branch information
dbajpeyi committed Dec 19, 2024
1 parent a08905c commit 0640b6f
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 88 deletions.
76 changes: 59 additions & 17 deletions dist/autofill-debug.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 59 additions & 17 deletions dist/autofill.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 60 additions & 20 deletions src/Form/FormAnalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,18 @@ class FormAnalyzer {
return this;
}

areLoginOrSignupSignalsWeak() {
return Math.abs(this.autofillSignal) < 10;
}

/**
* Hybrid forms can be used for both login and signup
* @returns {boolean}
*/
get isHybrid() {
// When marking for hybrid we also want to ensure other signals are weak
const areOtherSignalsWeak = Math.abs(this.autofillSignal) < 10;

return this.hybridSignal > 0 && areOtherSignalsWeak;
return this.hybridSignal > 0 && this.areLoginOrSignupSignalsWeak();
}

get isLogin() {
Expand Down Expand Up @@ -229,9 +232,41 @@ class FormAnalyzer {
});
}

/**
* Takes an element and returns all its children that are text-only nodes
* @param {HTMLElement|Element} element
* @param {number} maxDepth
* @param {number} currentDepth
* @returns {HTMLElement[]|Element[]}
*/
getElementsWithOnlyTextChild(element, maxDepth = 2, currentDepth = 0) {
// Array to collect elements with only text child nodes
const elementsWithTextChild = [];

// If we've reached the max depth, stop traversing further
if (currentDepth > maxDepth) {
return elementsWithTextChild;
}

updateFormHeaderSignals() {
// Check if the current element has only one text child node
if (element.nodeType === Node.ELEMENT_NODE) {
const childNodes = element.childNodes;

if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) {
elementsWithTextChild.push(element);
}
}

// Recurse through each child element and collect matching elements
for (const child of element.children) {
// Recursively get elements from child elements, increasing depth by 1
elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1));
}

return elementsWithTextChild;
}

evaluateFormHeaderSignals() {
const isVisuallyBeforeForm = (el) => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top;

const isHeaderSized = (el) => {
Expand All @@ -242,32 +277,34 @@ class FormAnalyzer {
const computedStyle = window.getComputedStyle(el);
const fontWeight = computedStyle.fontWeight;
const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1;
if (fontWeight === 'bold' || parseFloat(fontWeight) >= 700 || isRelativelyTall) {
return true
if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) {
return true;
}
}
};

const allSiblings = Array.from(this.form.parentElement?.children ?? [])
.filter((element) => element !== this.form)
if (allSiblings.length === 0) return false;
.map((element) => this.getElementsWithOnlyTextChild(element))
.flat();

if (allSiblings.length === 0) return false;

allSiblings.forEach((sibling) => {
if (sibling instanceof HTMLElement && sibling.childElementCount === 1 && isVisuallyBeforeForm(sibling) && isHeaderSized(sibling)) {
const string = sibling.textContent?.trim();
allSiblings.forEach((element) => {
if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) {
const string = element.textContent?.trim();
if (string) {
if (safeRegexTest(/^(sign[- ]?in|log[- ]?in)$/, string)) {
return this.decreaseSignalBy(3, 'Strong login header before form');
} else if (safeRegexTest(/^(sign[- ]?up)$/, string)) {
return this.increaseSignalBy(3, 'Strong signup header before form');
if (safeRegexTest(/^(sign[- ]?in|log[- ]?in)$/i, string)) {
return this.decreaseSignalBy(3, 'Strong login signal above form');
} else if (safeRegexTest(/^(sign[- ]?up)$/i, string)) {
return this.increaseSignalBy(3, 'Strong signup signal above form');
}
}
}
});
}

hasPasswordHints() {
return Array.from(this.form.querySelectorAll('div, span'))
evaluatePasswordHints() {
const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span'))
.filter(
(div) =>
div.textContent != null &&
Expand All @@ -276,6 +313,9 @@ class FormAnalyzer {
window.getComputedStyle(div).visibility !== 'hidden',
)
.some((div) => div.textContent && safeRegexTest(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent));
if (hasPasswordHints) {
this.increaseSignalBy(3, 'Password hints');
}
}

/**
Expand Down Expand Up @@ -385,10 +425,10 @@ class FormAnalyzer {
this.increaseSignalBy(relevantFields.length * 1.5, 'many fields: it is probably not a login');
}

this.updateFormHeaderSignals();

if (this.hasPasswordHints()) {
this.increaseSignalBy(3, 'Password hints');
// If we can't decide at this point, try reading form headers and password hints
if (this.areLoginOrSignupSignalsWeak()) {
this.evaluatePasswordHints();
this.evaluateFormHeaderSignals();
}

// If we can't decide at this point, try reading page headings
Expand Down
Loading

0 comments on commit 0640b6f

Please sign in to comment.