-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdetect.js
More file actions
194 lines (174 loc) · 6.76 KB
/
Copy pathdetect.js
File metadata and controls
194 lines (174 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// detect.js
// Shared logic: parse an Amazon product page (HTML string) and decide whether the
// item is currently eligible for FREE shipping to the signed-in delivery address.
//
// This runs in the background page (DOMParser available) and in the popup (for
// parseAsin). It intentionally works on raw HTML so the same code path handles
// both a background fetch() and the outerHTML of a rendered fallback tab.
var FreeShipDetect = (function () {
"use strict";
// Defaults are tuned for amazon.com with a delivery address set to Israel.
// Every one is overridable from Settings so the single user can re-tune if
// Amazon changes its wording, without touching code.
var DEFAULTS = {
// A destination-scoped free-shipping line, e.g. "FREE Shipping to Israel",
// "FREE International Shipping", "FREE delivery".
freeRegex: "free\\s+(international\\s+)?(shipping|delivery)",
// A real shipping charge line, e.g. "$12.98 Shipping to Israel".
// Deliberately NOT matching "Import Fees Deposit" — those can apply even when
// shipping itself is free, so they must not count as a shipping charge.
chargeRegex: "\\$\\s?\\d[\\d,]*\\.\\d{2}[^.$|]{0,30}shipping",
// The item can't be delivered here at all right now.
unavailableRegex:
"cannot be shipped to|does(n't| not) (currently )?ship to|item cannot be shipped|this item does not ship"
};
// Blocks on the product page that carry delivery / shipping-cost wording.
var SELECTORS = [
"#deliveryBlockMessage",
"#mir-layout-DELIVERY_BLOCK",
"#amazonGlobal_feature_div",
"#globalStoreBadgePopoverInsideBuybox",
"#contextualIngressPtLabel",
"#unifiedDeliveryMessage",
"#exportsCosts_feature_div",
"#deliveryPromiseInsideBuyBox_feature_div",
"#desktop_buybox",
"#buybox"
];
var PRICE_SELECTORS = [
"#corePriceDisplay_desktop_feature_div .a-price .a-offscreen",
"#corePrice_feature_div .a-price .a-offscreen",
"#priceblock_ourprice",
"#priceblock_dealprice",
"#price_inside_buybox",
".a-price .a-offscreen"
];
function firstText(doc, selectors) {
for (var i = 0; i < selectors.length; i++) {
var el = doc.querySelector(selectors[i]);
if (el && el.textContent && el.textContent.trim()) return el.textContent.trim();
}
return null;
}
function deliveryText(doc) {
var parts = [];
for (var i = 0; i < SELECTORS.length; i++) {
var el = doc.querySelector(SELECTORS[i]);
if (el) {
var t = el.textContent.replace(/\s+/g, " ").trim();
if (t) parts.push(t);
}
}
// Drop generic marketing like "FREE Shipping on orders over $49" so it can't
// masquerade as a real per-item free-shipping signal.
return parts
.join(" | ")
.replace(/free\s+(international\s+)?shipping\s+on\s+(all\s+)?orders?[^.|]*/gi, " ");
}
// The main product photo. Works on both the static fetched HTML (data-dynamic
// JSON / og:image) and a rendered tab (resolved src). Skips lazy-load data:
// URIs so we never store a blank placeholder pixel.
function extractImage(doc) {
var el =
doc.querySelector("#landingImage") ||
doc.querySelector("#imgTagWrapperId img") ||
doc.querySelector("#main-image") ||
doc.querySelector("#imgBlkFront");
if (el) {
var hires = el.getAttribute("data-old-hires");
if (hires && !/^data:/.test(hires)) return hires;
var dyn = el.getAttribute("data-a-dynamic-image");
if (dyn) {
try {
var k = Object.keys(JSON.parse(dyn))[0];
if (k && !/^data:/.test(k)) return k;
} catch (e) {
/* ignore */
}
}
var src = el.getAttribute("src");
if (src && !/^data:/.test(src)) return src;
}
var og = doc.querySelector('meta[property="og:image"]');
if (og) {
var c = og.getAttribute("content");
if (c && !/^data:/.test(c)) return c;
}
return null;
}
function isRobotPage(html) {
return /Robot Check|Enter the characters you see|api-services-support@amazon\.com|To discuss automated access|validateCaptcha/i.test(
html
);
}
function snippetAround(text, regexes) {
for (var i = 0; i < regexes.length; i++) {
var m = regexes[i].exec(text);
if (m) {
var start = Math.max(0, m.index - 30);
var end = Math.min(text.length, m.index + m[0].length + 50);
return (start > 0 ? "…" : "") + text.slice(start, end).trim() + (end < text.length ? "…" : "");
}
}
return null;
}
// Core decision. Returns:
// { ok, status, freeShip, title, price, snippet } on success
// { ok:false, reason } when the page couldn't be read
// status ∈ "free" | "paid" | "unavailable" | "unknown"
function detect(html, opts) {
opts = opts || {};
if (!html || html.length < 120) return { ok: false, reason: "empty" };
if (isRobotPage(html)) return { ok: false, reason: "robot" };
var free = new RegExp(opts.freeRegex || DEFAULTS.freeRegex, "i");
var charge = new RegExp(opts.chargeRegex || DEFAULTS.chargeRegex, "i");
var unavailable = new RegExp(opts.unavailableRegex || DEFAULTS.unavailableRegex, "i");
var doc = new DOMParser().parseFromString(html, "text/html");
var title = firstText(doc, ["#productTitle", "#title"]);
var price = firstText(doc, PRICE_SELECTORS);
var text = deliveryText(doc);
if (!title && !text) return { ok: false, reason: "noparse" };
var status, freeShip = false;
if (free.test(text)) {
status = "free";
freeShip = true;
} else if (unavailable.test(text)) {
status = "unavailable";
} else if (charge.test(text)) {
status = "paid";
} else {
status = "unknown";
}
var snippet = snippetAround(text, [free, unavailable, charge]) || text.slice(0, 160).trim();
return {
ok: true,
status: status,
freeShip: freeShip,
title: title ? title.replace(/\s+/g, " ").trim() : null,
price: price || null,
snippet: snippet || null,
image: extractImage(doc)
};
}
// Pull a 10-char ASIN out of a URL, a full paste, or a bare ASIN.
function parseAsin(input) {
if (!input) return null;
input = String(input).trim();
if (/^[A-Z0-9]{10}$/i.test(input)) return input.toUpperCase();
var m =
input.match(/\/(?:dp|gp\/product|product|gp\/aw\/d|gp\/offer-listing)\/([A-Z0-9]{10})/i) ||
input.match(/[?&]asin=([A-Z0-9]{10})/i) ||
input.match(/\/([A-Z0-9]{10})(?:[/?#]|$)/i);
return m ? m[1].toUpperCase() : null;
}
function canonicalUrl(asin) {
return "https://www.amazon.com/dp/" + asin + "?psc=1";
}
return {
detect: detect,
parseAsin: parseAsin,
canonicalUrl: canonicalUrl,
DEFAULTS: DEFAULTS
};
})();
if (typeof module !== "undefined") module.exports = FreeShipDetect;