(function installLTJTProductLookup(root, factory) { const api = factory(); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (root) root.LTJTProductLookup = api; }(typeof self !== 'undefined' ? self : globalThis, () => { 'use strict'; function compactProductLookupText(value) { return String(value || '') .normalize('NFKC') .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, ''); } function normalizeProductQueryText(value) { // ERP products commonly write duration as 8D7N while users may write // 8天7晚. Only convert duration suffixes after a number so a product name // containing an ordinary Chinese character such as “天” is not changed. return compactProductLookupText(value) .replace(/(\d+)天/gu, '$1d') .replace(/(\d+)(?:晚|夜)/gu, '$1n'); } function normalizeProductDuration(value) { return normalizeProductQueryText(value); } function isOrderedSubsequence(query, value) { let cursor = 0; for (const character of query) { const next = value.indexOf(character, cursor); if (next < 0) return false; cursor = next + character.length; } return true; } function collapseWhitespace(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function rawCellText(element) { return String(element?.innerText || element?.textContent || '').trim(); } function productLabelFromRow(row) { return collapseWhitespace(rawCellText(row?.cells?.[1] || row)); } function productNameAndDuration(label) { const normalizedLabel = collapseWhitespace(label); // The current ERP renders the product name and duration in the same // second cell, separated by whitespace (for example “江西老挝 10D9N”). // Restrict the suffix parser to duration-shaped text so names containing // dates or numbers such as “尊享老挝3.10” remain intact. const durationMatch = normalizedLabel.match(/(?:^|\s)(\d+(?:\.\d+)?\s*(?:D|天)\s*\d+(?:\.\d+)?\s*(?:N|晚|夜)?)(?:\s*)$/iu); if (!durationMatch) return { name: normalizedLabel, duration: '' }; const name = normalizedLabel.slice(0, durationMatch.index).trim(); return { name: name || normalizedLabel, duration: collapseWhitespace(durationMatch[1]).replace(/\s+/g, '') }; } function productCandidateFromRow(row, input = null) { const label = productLabelFromRow(row); const parts = productNameAndDuration(label); return { input, row, text: collapseWhitespace(rawCellText(row)), label, name: parts.name, duration: parts.duration, cpid: String(input?.value || '').trim() }; } function productQueryTokens(query) { const normalizedQuery = normalizeProductQueryText(query); const hanRuns = normalizedQuery.match(/\p{Script=Han}+/gu) || []; const nonHanTokens = normalizedQuery.match(/\d+[a-z]+|[a-z]+\d*|\d+/g) || []; const durationTokens = nonHanTokens.filter((token) => /\d+d|\d+n/i.test(token)); const nameTokens = nonHanTokens.filter((token) => !durationTokens.includes(token)); return { normalizedQuery, hanRuns, durationTokens, nameTokens }; } function productQueryMatch(candidateOrLabel, query) { const candidate = typeof candidateOrLabel === 'string' ? { label: candidateOrLabel, name: candidateOrLabel, duration: '' } : (candidateOrLabel || {}); const normalizedLabel = compactProductLookupText(candidate.label || candidate.name || ''); const normalizedQuery = normalizeProductQueryText(query); if (!normalizedQuery) return { matched: true, rule: 'empty_keyword' }; if (normalizedLabel.includes(normalizedQuery)) return { matched: true, rule: 'unique_contiguous' }; const { hanRuns, durationTokens, nameTokens } = productQueryTokens(query); if (!hanRuns.length && !durationTokens.length && !nameTokens.length) { return { matched: false, rule: 'no_tokens' }; } const normalizedName = compactProductLookupText(candidate.name || candidate.label || ''); const normalizedDuration = normalizeProductDuration(candidate.duration || ''); if (hanRuns.some((run) => !isOrderedSubsequence(run, normalizedName))) { return { matched: false, rule: 'name_ordered_tokens' }; } if (durationTokens.some((token) => !normalizedDuration.includes(token) && !normalizedLabel.includes(token))) { return { matched: false, rule: 'duration_tokens' }; } if (nameTokens.some((token) => !normalizedName.includes(token) && !normalizedLabel.includes(token))) { return { matched: false, rule: 'name_numeric_tokens' }; } return { matched: true, rule: durationTokens.length ? 'unique_name_duration_tokens' : 'unique_ordered_tokens' }; } function collectProductCandidates(scopeDocument) { return Array.from(scopeDocument?.querySelectorAll?.('input[name="cp_id"], input[name="cpid"]') || []) .map((input) => { const row = input.closest('tr') || input.parentElement?.parentElement || input.parentElement; return productCandidateFromRow(row, input); }); } function matchProductCandidates(candidates, query, cpid = '') { const expectedCpid = String(cpid || '').trim(); return (candidates || []) .filter((candidate) => !expectedCpid || String(candidate.cpid || '').trim() === expectedCpid) .map((candidate) => ({ ...candidate, match: productQueryMatch(candidate, query) })) .filter((candidate) => candidate.match.matched); } return { compactProductLookupText, normalizeProductQueryText, normalizeProductDuration, isOrderedSubsequence, productLabelFromRow, productNameAndDuration, productCandidateFromRow, productQueryTokens, productQueryMatch, collectProductCandidates, matchProductCandidates }; }));