Bug 1370429 - Part 2: Implement address-lines parser and refactor getInfo function. r=MattN
MozReview-Commit-ID: 5gseB36n1M0
--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -18,36 +18,92 @@ Cu.import("resource://formautofill/FormA
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
/**
* A scanner for traversing all elements in a form and retrieving the field
- * detail with FormAutofillHeuristics.getInfo function.
+ * detail with FormAutofillHeuristics.getInfo function. It also provides a
+ * cursor (indexParsing) to indicate which element is waiting for parsing.
*/
class FieldScanner {
/**
* Create a FieldScanner based on form elements with the existing
* fieldDetails.
*
* @param {Array.DOMElement} elements
* The elements from a form for each parser.
*/
constructor(elements) {
this._elementsWeakRef = Cu.getWeakReference(elements);
this.fieldDetails = [];
+ this._indexParsing = 0;
}
get _elements() {
return this._elementsWeakRef.get();
}
/**
+ * This cursor means the index of the element which is waiting for parsing.
+ *
+ * @returns {number}
+ * The index of the element which is waiting for parsing.
+ */
+ get indexParsing() {
+ return this._indexParsing;
+ }
+
+ /**
+ * Move the indexParsing to the next elements. Any elements behind this index
+ * means the parsing tasks are finished.
+ *
+ * @param {number} index
+ * The latest index of elements waiting for parsing.
+ */
+ set indexParsing(index) {
+ if (index > this.fieldDetails.length) {
+ log.warn("The parsing index is out of range.");
+ index = this.fieldDetails.length;
+ }
+ this._indexParsing = index;
+ }
+
+ /**
+ * Retrieve the field detail by the index. If the field detail is not ready,
+ * the elements will be traversed until matching the index.
+ *
+ * @param {number} index
+ * The index of the element that you want to retrieve.
+ * @returns {Object}
+ * The field detail at the specific index.
+ */
+ getFieldDetailByIndex(index) {
+ if (index >= this._elements.length) {
+ return null;
+ }
+
+ if (this.fieldDetails.length > index) {
+ return this.fieldDetails[index];
+ }
+
+ for (let i = this.fieldDetails.length; i < (index + 1); i++) {
+ this.pushDetail();
+ }
+
+ return this.fieldDetails[index];
+ }
+
+ get parsingFinished() {
+ return this.indexParsing >= this._elements.length;
+ }
+
+ /**
* This function will prepare an autocomplete info object with getInfo
* function and push the detail to fieldDetails property. Any duplicated
* detail will be marked as _duplicated = true for the parser.
*
* Any element without the related detail will be used for adding the detail
* to the end of field details.
*/
pushDetail() {
@@ -74,16 +130,39 @@ class FieldScanner {
// A field with the same identifier already exists.
log.debug("Not collecting a field matching another with the same info:", info);
fieldInfo._duplicated = true;
}
this.fieldDetails.push(fieldInfo);
}
+ /**
+ * When a field detail should be changed its fieldName after parsing, use
+ * this function to update the fieldName which is at a specific index.
+ *
+ * @param {number} index
+ * The index indicates a field detail to be updated.
+ * @param {string} fieldName
+ * The new fieldName
+ */
+ updateFieldName(index, fieldName) {
+ if (index >= this.fieldDetails.length) {
+ log.warn("Try to update an inexistent field detail.");
+ return;
+ }
+ this.fieldDetails[index].fieldName = fieldName;
+
+ delete this.fieldDetails[index]._duplicated;
+ let indexSame = this.findSameField(this.fieldDetails[index]);
+ if (indexSame != index && indexSame != -1) {
+ this.fieldDetails[index]._duplicated = true;
+ }
+ }
+
findSameField(info) {
return this.fieldDetails.findIndex(f => f.section == info.section &&
f.addressType == info.addressType &&
f.contactType == info.contactType &&
f.fieldName == info.fieldName);
}
/**
@@ -121,81 +200,102 @@ this.FormAutofillHeuristics = {
"country",
],
TEL: ["tel"],
EMAIL: ["email"],
},
RULES: null,
+ /**
+ * This function tries to find the correct address-line[1-3] sequence and
+ * correct their field names.
+ *
+ * @param {Object} fieldScanner
+ * The current parsing status for all elements
+ * @returns {boolean}
+ * Return true if there is any field can be recognized in the parser,
+ * otherwise false.
+ */
+ _parseAddressFields(fieldScanner) {
+ let parsedFields = false;
+ let addressLines = ["address-line1", "address-line2", "address-line3"];
+ for (let i = 0; !fieldScanner.parsingFinished && i < addressLines.length; i++) {
+ let detail = fieldScanner.getFieldDetailByIndex(fieldScanner.indexParsing);
+ if (!detail || !addressLines.includes(detail.fieldName)) {
+ // When the field is not related to any address-line[1-3] fields, it
+ // means the parsing process can be terminated.
+ break;
+ }
+ fieldScanner.updateFieldName(fieldScanner.indexParsing, addressLines[i]);
+ fieldScanner.indexParsing++;
+ parsedFields = true;
+ }
+
+ return parsedFields;
+ },
+
getFormInfo(form) {
- if (form.autocomplete == "off") {
+ if (form.autocomplete == "off" || form.elements.length <= 0) {
return [];
}
+
let fieldScanner = new FieldScanner(form.elements);
- for (let i = 0; i < fieldScanner.elements.length; i++) {
- let element = fieldScanner.elements[i];
- let info = this.getInfo(element, fieldScanner.fieldDetails);
- fieldScanner.pushDetail(i, info);
+ while (!fieldScanner.parsingFinished) {
+ let parsedAddressFields = this._parseAddressFields(fieldScanner);
+
+ // If there is no any field parsed, the parsing cursor can be moved
+ // forward to the next one.
+ if (!parsedAddressFields) {
+ fieldScanner.indexParsing++;
+ }
}
return fieldScanner.trimmedFieldDetail;
},
/**
* Get the autocomplete info (e.g. fieldName) determined by the regexp
- * (this.RULES) matching to a feature string. The result is based on the
- * existing field names to prevent duplicating predictions
- * (e.g. address-line[1-3).
+ * (this.RULES) matching to a feature string.
*
* @param {string} string a feature string to be determined.
- * @param {Array<string>} existingFieldNames the array of exising field names
- * in a form.
* @returns {Object}
* Provide the predicting result including the field name.
*
*/
- _matchStringToFieldName(string, existingFieldNames) {
+ _matchStringToFieldName(string) {
let result = {
fieldName: "",
section: "",
addressType: "",
contactType: "",
};
if (this.RULES.email.test(string)) {
result.fieldName = "email";
return result;
}
if (this.RULES.tel.test(string)) {
result.fieldName = "tel";
return result;
}
for (let fieldName of this.FIELD_GROUPS.ADDRESS) {
if (this.RULES[fieldName].test(string)) {
- // If "address-line1" or "address-line2" exist already, the string
- // could be satisfied with "address-line2" or "address-line3".
- if ((fieldName == "address-line1" &&
- existingFieldNames.includes("address-line1")) ||
- (fieldName == "address-line2" &&
- existingFieldNames.includes("address-line2"))) {
- continue;
- }
result.fieldName = fieldName;
return result;
}
}
for (let fieldName of this.FIELD_GROUPS.NAME) {
if (this.RULES[fieldName].test(string)) {
result.fieldName = fieldName;
return result;
}
}
return null;
},
- getInfo(element, fieldDetails) {
+ getInfo(element) {
if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
return null;
}
let info = element.getAutocompleteInfo();
// An input[autocomplete="on"] will not be early return here since it stll
// needs to find the field name.
if (info && info.fieldName && info.fieldName != "on") {
@@ -214,35 +314,31 @@ this.FormAutofillHeuristics = {
return {
fieldName: "email",
section: "",
addressType: "",
contactType: "",
};
}
- let existingFieldNames = fieldDetails ? fieldDetails.map(i => i.fieldName) : "";
-
for (let elementString of [element.id, element.name]) {
- let fieldNameResult = this._matchStringToFieldName(elementString,
- existingFieldNames);
+ let fieldNameResult = this._matchStringToFieldName(elementString);
if (fieldNameResult) {
return fieldNameResult;
}
}
let labels = FormAutofillUtils.findLabelElements(element);
if (!labels || labels.length == 0) {
log.debug("No label found for", element);
return null;
}
for (let label of labels) {
let strings = FormAutofillUtils.extractLabelStrings(label);
for (let string of strings) {
- let fieldNameResult = this._matchStringToFieldName(string,
- existingFieldNames);
+ let fieldNameResult = this._matchStringToFieldName(string);
if (fieldNameResult) {
return fieldNameResult;
}
}
}
return null;
},
--- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
+++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js
@@ -88,18 +88,18 @@ const TESTCASES = [
<input id="line1" autocomplete="address-line1">
<input id="line3" autocomplete="address-line3">
</form>`,
profileData: [Object.assign({}, DEFAULT_PROFILE)],
expectedResult: [{
"guid": "123",
"street-address": "2 Harrison St line2 line3",
"-moz-street-address-one-line": "2 Harrison St line2 line3",
- "address-line1": "2 Harrison St line2",
- "address-line2": "line2",
+ "address-line1": "2 Harrison St",
+ "address-line2": "line2 line3",
"address-line3": "line3",
}],
},
];
for (let testcase of TESTCASES) {
add_task(async function() {
do_print("Starting testcase: " + testcase.description);
--- a/browser/extensions/formautofill/test/unit/test_getInfo.js
+++ b/browser/extensions/formautofill/test/unit/test_getInfo.js
@@ -91,39 +91,22 @@ const TESTCASES = [
expectedReturnValue: {
fieldName: "address-level1",
section: "",
addressType: "",
contactType: "",
},
},
{
- description: "2 address line inputs",
+ description: "address line input",
document: `<label for="targetElement">street</label>
<input id="targetElement" type="text">`,
elementId: "targetElement",
- addressFieldDetails: [{fieldName: "address-line1"}],
expectedReturnValue: {
- fieldName: "address-line2",
- section: "",
- addressType: "",
- contactType: "",
- },
- },
- {
- description: "3 address line inputs",
- document: `<label for="targetElement">street</label>
- <input id="targetElement" type="text">`,
- elementId: "targetElement",
- addressFieldDetails: [
- {fieldName: "address-line1"},
- {fieldName: "address-line2"},
- ],
- expectedReturnValue: {
- fieldName: "address-line3",
+ fieldName: "address-line1",
section: "",
addressType: "",
contactType: "",
},
},
{
description: "CJK character - Traditional Chinese",
document: `<label> 郵遞區號
@@ -213,13 +196,13 @@ const TESTCASES = [
TESTCASES.forEach(testcase => {
add_task(async function() {
do_print("Starting testcase: " + testcase.description);
let doc = MockDocument.createTestDocument(
"http://localhost:8080/test/", testcase.document);
let element = doc.getElementById(testcase.elementId);
- let value = FormAutofillHeuristics.getInfo(element, testcase.addressFieldDetails);
+ let value = FormAutofillHeuristics.getInfo(element);
Assert.deepEqual(value, testcase.expectedReturnValue);
});
});