Ctrl K
ComunidadGuías…

lib/ascii-fold.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * ascii-fold.mjs — fold a name to ASCII for comparison against an ASCII target.
 *
 * Two places in the tree build a private key by DELETING every character
 * outside `[a-z0-9]` instead of folding it. The deletion is always wrong when
 * the thing being compared against is ASCII, because the accented letter has an
 * obvious ASCII counterpart that the target actually uses:
 *
 *   - verify-portals.mjs      "Telefónica"       -> "telefnica", never the real
 *                                                   Greenhouse slug "telefonica" (#2930)
 *   - providers/_trust-validator.mjs
 *                             "Société Générale" -> "socit gnrale", so the company
 *                                                   failed to match its own domain (#2924)
 *
 * WHY NFD IS RIGHT HERE AND WRONG IN foldStatusInput (tracker-utils.mjs, #2705):
 * there the fold must not collapse distinct identities — Żubr must never become
 * Zubr, because two different companies would merge. Here the comparison target
 * is ASCII by construction (a hostname, an ATS slug), so folding to the ASCII
 * base letter IS the intent: "telefonica" is the ASCII folding of "Telefónica".
 * Different question, different answer.
 *
 * A name with no Latin content at all (CJK, Cyrillic, Greek) folds to ''. That
 * is a real answer, not a failure: no ASCII target can contain it. Callers must
 * decide what '' means for them — see each call site.
 */

/**
 * Latin letters that do NOT decompose under NFD, so stripping combining marks
 * alone still deletes them. Lowercase only; `asciiFold` lowercases first.
 */
// A stroke or bar through a letter is part of the GLYPH, not a combining
// mark, so NFD leaves it and the [^a-z0-9] strip below then deletes the letter
// outright — the failure this whole module exists to stop, surviving inside
// it. Unlike the hostname case there is no substring luck here: "Işık" derived
// "isk" and never "isik", so --add probed a slug no board uses.
// ŋ is "ng", not "n" — the plausible one-to-one mapping is the wrong one.
// (CodeRabbit, reviewing #2927.)
const NON_DECOMPOSING_LATIN = [
  [/ø/g, 'o'], [/æ/g, 'ae'], [/œ/g, 'oe'], [/ß/g, 'ss'],
  [/đ/g, 'd'], [/ł/g, 'l'], [/þ/g, 'th'], [/ð/g, 'd'],
  [/ħ/g, 'h'], [/ı/g, 'i'], [/ŋ/g, 'ng'], [/ŧ/g, 't'],
  [/ĸ/g, 'k'], [/ſ/g, 's'],
];

/**
 * Lowercase and fold a name to ASCII letters, digits and single spaces.
 *
 * @param {string} value - Raw display name.
 * @param {{punctuation?: 'space'|'delete'}} [options] - How residual punctuation
 *   is treated; see the note in the body. Word-level callers care, slug-style
 *   callers do not.
 * @returns {string} Folded name, or '' when nothing Latin survives.
 */
export function asciiFold(value, { punctuation = 'space' } = {}) {
  let out = String(value ?? '').toLowerCase().normalize('NFD').replace(/\p{M}+/gu, '');
  for (const [re, to] of NON_DECOMPOSING_LATIN) out = out.replace(re, to);
  // 'space' (default) turns residual punctuation into a separator, so
  // "Smith&Jones" becomes two words. 'delete' removes it, so it stays one.
  //
  // NOT cosmetic, which is why this is an option and not a harmonization
  // (#3040). A caller that then matches WORDS gains words it never had:
  //
  //   Smith&Jones   space  -> "smith jones"  words: [smith, jones]
  //                 delete -> "smithjones"   words: [smithjones]
  //
  // and `smith` substring-matches smithfield.com, so a
  // company/hostname mismatch that should be flagged silently is not.
  // Slug-style callers that collapse spaces converge either way; word-level
  // callers do not, so the caller states which it needs.
  //
  // The 'delete' class is `[^a-z0-9 ]` with a LITERAL space, not `\s`: a tab or
  // newline is removed rather than collapsed, matching the behaviour
  // _trust-validator.mjs had before it moved here.
  out = punctuation === 'delete'
    ? out.replace(/[^a-z0-9 ]/g, '')
    : out.replace(/[^a-z0-9\s]/g, ' ');
  return out.replace(/\s+/g, ' ').trim();
}