Skip to content
Merged
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
24 changes: 18 additions & 6 deletions lib/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,12 @@ export function parseTldList(input, limit = MAX_BULK_TLDS) {
price = parsePriceToken(`${field}${fields[index + 1]}`);
if (price !== null) index++;
}
// A price is unambiguous — it is the only field that can start with `$`
// or be all digits, and an all-numeric ending is rejected anyway. So
// anything that is not a price is the ending this one points at.
// A marked price — `$2`, `2.00`, `USD 2` — is unambiguous: it is the
// only field that can start with `$` or carry a decimal/USD marker. A
// bare integer is NOT taken as a price, because all-numeric endings are
// legal (`.420`, `.911`) and the token is far more likely to name the
// ending this one points at. Anything that is not a price is therefore
// the ending this one points at.
if (price !== null) priceUsd = price;
else aliasOf = normalizeToken(field);
}
Expand Down Expand Up @@ -334,10 +337,19 @@ function normalizeToken(value) {
* Forgiving because it is typed by hand in a textarea next to a dollar sign,
* and strict about the shape because the alternative reading of a stray token
* is "the ending this one points at", which would silently mis-route a name.
*
* A bare integer (`420`) is NOT a price. All-numeric endings are legal here —
* `.420`, `.911` — so an unmarked number is far more likely to name the ending
* this line points at than to set a price, and reading it as one silently
* minted four-figure prices on what were meant to be aliases. A price must say
* it is money: a `$`, a decimal point, or a USD affix.
*/
function parsePriceToken(value) {
const raw = String(value ?? "").trim().toLowerCase().replace(/^usd/, "").replace(/usd$/, "").replace(/^\$/, "").trim();
if (!raw || !/^\d+(\.\d{1,2})?$/.test(raw)) return null;
const price = Number(raw);
const raw = String(value ?? "").trim().toLowerCase();
const markedAsMoney = /^\$/.test(raw) || /^usd/.test(raw) || /usd$/.test(raw) || raw.includes(".");
if (!markedAsMoney) return null;
const digits = raw.replace(/^usd/, "").replace(/usd$/, "").replace(/^\$/, "").trim();
if (!digits || !/^\d+(\.\d{1,2})?$/.test(digits)) return null;
const price = Number(digits);
return Number.isFinite(price) && price > 0 ? price : null;
}
21 changes: 21 additions & 0 deletions test/name.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,27 @@ test("a pasted list reads endings and names alike", async (t) => {
[{ tld: "eggs", label: null, aliasOf: null, priceUsd: 2 }]);
});

await t.test("a bare integer is an alias to a numeric ending, not a price", () => {
// All-numeric endings are legal — `.420`, `.911` — so an unmarked number
// names the ending this line points at. Reading it as a price silently
// minted four-figure prices on what were meant to be aliases, and made an
// alias to a numeric ending impossible to write at all.
assert.deepEqual(parseTldList("eggs 420").entries,
[{ tld: "eggs", label: null, aliasOf: "420", priceUsd: null }]);
assert.deepEqual(parseTldList("eggs 911 $2").entries,
[{ tld: "eggs", label: null, aliasOf: "911", priceUsd: 2 }]);
// A name under a numeric ending aliases the same way.
assert.deepEqual(parseTldList("eggs blue.420").entries,
[{ tld: "eggs", label: null, aliasOf: "blue.420", priceUsd: null }]);
});

await t.test("documented price forms still read as prices", () => {
for (const [line, expected] of [["eggs $2", 2], ["eggs 2.00", 2], ["eggs $2.00usd", 2], ["eggs usd2", 2]]) {
assert.deepEqual(parseTldList(line).entries,
[{ tld: "eggs", label: null, aliasOf: null, priceUsd: expected }], line);
}
});

await t.test("neither an ending nor a name survives to be refused by name", () => {
// Three labels is neither. Dropping it here would leave it out of the
// caller's report entirely, so it is handed on carrying its own bad text.
Expand Down
Loading