diff --git a/src/fallback_extraction.js b/src/fallback_extraction.js new file mode 100644 index 0000000..db1f6ee --- /dev/null +++ b/src/fallback_extraction.js @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +* Uses CSS selectors, or failing that, Open Graph tags to extract +* a product from its product page, where a 'product' is defined by the bundle +* of features that makes it identifiable. +* +* Features: title, image, price +*/ + +import extractionData from 'commerce/product_extraction_data.json'; + +const OPEN_GRAPH_PROPERTY_VALUES = { + title: 'og:title', + image: 'og:image', + price: 'og:price:amount', +}; + +/** + * Returns any extraction data found for the vendor based on the URL + * for the page. + */ +function getProductAttributeInfo() { + const hostname = new URL(window.location.href).host; + for (const [vendor, attributeInfo] of Object.entries(extractionData)) { + if (hostname.includes(vendor)) { + return attributeInfo; + } + } + return null; +} + +/** + * Extracts and returns the string value for a given element property or attribute. + * + * @param {HTMLElement} element + * @param {string} extractionProperty + */ +function extractValueFromElement(element, extractionProperty) { + switch (extractionProperty) { + case 'content': + return element.getAttribute('content'); + case 'innerText': + return element.innerText; + case 'src': + return element.src; + default: + throw new Error(`Unrecognized extraction property or attribute '${extractionProperty}'.`); + } +} + +/** + * Returns any product information available on the page from CSS + * selectors if they exist, otherwise from Open Graph tags. + */ +export default function extractProduct() { + const data = {}; + const attributeInfo = getProductAttributeInfo(); + if (attributeInfo) { + for (const [productAttribute, extractor] of Object.entries(attributeInfo)) { + const {selectors, extractUsing} = extractor; + for (const selector of selectors) { + const element = document.querySelector(selector); + if (element) { + data[productAttribute] = extractValueFromElement(element, extractUsing); + if (data[productAttribute]) { + break; + } else { + throw new Error(`Element found did not return a valid product ${productAttribute}.`); + } + } else if (selector === selectors[selectors.length - 1]) { + // None of the selectors matched an element on the page + throw new Error(`No elements found with vendor data for product ${productAttribute}.`); + } + } + } + } else { + for (const [key, value] of Object.entries(OPEN_GRAPH_PROPERTY_VALUES)) { + const metaEle = document.querySelector(`meta[property='${value}']`); + if (metaEle) { + data[key] = metaEle.getAttribute('content'); + } + } + } + return data; +} diff --git a/src/fathom_ruleset.js b/src/fathom_extraction.js similarity index 70% rename from src/fathom_ruleset.js rename to src/fathom_extraction.js index efc29da..ba4846e 100644 --- a/src/fathom_ruleset.js +++ b/src/fathom_extraction.js @@ -1,9 +1,9 @@ -/** This Source Code Form is subject to the terms of the Mozilla Public +/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* - * Using Fathom to extract a product from its product page, + * Uses Fathom to extract a product from its product page, * where a 'product' is defined by the bundle of features that * makes it identifiable. * @@ -42,10 +42,10 @@ const rules = ruleset( ); /** - * Extracts the highest scoring element above a score threshold for a - * given feature contained in a page's HTML document. + * Extracts the highest scoring element above a score threshold + * contained in a page's HTML document. */ -export default function runTuningRoutine(doc) { +function runRuleset(doc) { let fnodesList = rules.against(doc).get('product-price'); fnodesList = fnodesList.filter(fnode => fnode.scoreFor('priceish') >= SCORE_THRESHOLD); // It is possible for multiple elements to have the same highest score. @@ -54,3 +54,19 @@ export default function runTuningRoutine(doc) { } return null; } + +/* + * Run the ruleset for the product features against the current window document + */ +export default function extractProduct(doc) { + const priceEle = runRuleset(doc); + if (priceEle) { + const price = (priceEle.tagName !== 'META') ? priceEle.textContent : priceEle.getAttribute('content'); + if (price) { + return { + price, + }; + } + } + return null; +} diff --git a/src/product_extraction_data.json b/src/product_extraction_data.json index b17318d..9194a7b 100644 --- a/src/product_extraction_data.json +++ b/src/product_extraction_data.json @@ -27,6 +27,7 @@ "price": { "selectors": [ "#priceblock_ourprice", + "#priceblock_dealprice", ".display-price", ".offer-price" ], diff --git a/src/product_info.js b/src/product_info.js index c17c28e..1852e6b 100644 --- a/src/product_info.js +++ b/src/product_info.js @@ -7,15 +7,9 @@ * which is after all DOM content has been loaded. */ -import runTuningRoutine from 'commerce/fathom_ruleset'; +import extractProductWithFathom from 'commerce/fathom_extraction'; +import extractProductWithFallback from 'commerce/fallback_extraction'; import {retry} from 'commerce/utils'; -import extractionData from 'commerce/product_extraction_data.json'; - -const OPEN_GRAPH_PROPERTY_VALUES = { - title: 'og:title', - image: 'og:image', - price: 'og:price:amount', -}; /** * Open a Port to the background script and wait for the background script to @@ -55,103 +49,14 @@ async function openBackgroundPort() { } }()); -const fallbackExtraction = { - /** - * Returns any extraction data found for the vendor based on the URL - * for the page. - */ - getProductAttributeInfo() { - const hostname = new URL(window.location.href).host; - for (const [vendor, attributeInfo] of Object.entries(extractionData)) { - if (hostname.includes(vendor)) { - return attributeInfo; - } - } - return null; - }, - - /** - * Extracts and returns the string value for a given element property or attribute. - * - * @param {HTMLElement} element - * @param {string} extractionProperty - */ - extractValueFromElement(element, extractionProperty) { - switch (extractionProperty) { - case 'content': - return element.getAttribute('content'); - case 'innerText': - return element.innerText; - case 'src': - return element.src; - default: - throw new Error(`Unrecognized extraction property or attribute '${extractionProperty}'.`); - } - }, - - /** - * Returns any product information available on the page from CSS - * selectors if they exist, otherwise from Open Graph tags. - */ - extractProduct() { - const data = {}; - const attributeInfo = this.getProductAttributeInfo(); - if (attributeInfo) { - for (const [productAttribute, extractor] of Object.entries(attributeInfo)) { - const {selectors, extractUsing} = extractor; - for (const selector of selectors) { - const element = document.querySelector(selector); - if (element) { - data[productAttribute] = this.extractValueFromElement(element, extractUsing); - if (data[productAttribute]) { - break; - } else { - throw new Error(`Element found did not return a valid product ${productAttribute}.`); - } - } else if (selector === selectors[selectors.length - 1]) { - // None of the selectors matched an element on the page - throw new Error(`No elements found with vendor data for product ${productAttribute}.`); - } - } - } - } else { - for (const [key, value] of Object.entries(OPEN_GRAPH_PROPERTY_VALUES)) { - const metaEle = document.querySelector(`meta[property='${value}']`); - if (metaEle) { - data[key] = metaEle.getAttribute('content'); - } - } - } - data.url = window.document.URL; - return data; - }, -}; - -const fathomExtraction = { - /* - * Run the ruleset for the product features against the current window document - */ - extractProduct() { - const priceEle = runTuningRoutine(window.document); - if (priceEle) { - const price = (priceEle.tagName !== 'META') ? priceEle.textContent : priceEle.getAttribute('content'); - if (price) { - return { - price, - url: window.document.URL, - }; - } - } - return null; - }, -}; - /** * Checks to see if any product information for the page was found, * and if so, sends it to the background script via the port. */ async function getProductInfo(port) { - const extractedProduct = fathomExtraction.extractProduct() || fallbackExtraction.extractProduct(); + const extractedProduct = (extractProductWithFathom(window.document) + || extractProductWithFallback()); + extractedProduct.url = window.document.URL; port.postMessage({ from: 'content', subject: 'ready',