/** * Parser for the Certified Collectibles Group cert-verification pages (CGC Comics, CGC Cards, NGC, PMG): * all four sites share one AngularJS template — a `.results-pane` holding `
label
value
` * pairs, `.certlookup-stats-item-value` population lines ("In 9.0: 2", "In Higher Grades: 0") and a * `.certlookup-images` gallery. Not-found pages render the lookup form with "This item cannot be found". */ import { html as H } from '@rareindex/connectors'; import { clean, toInt } from './text.js'; import type { PageStatus } from './cert-base.js'; export interface CcgPage { /** label → value (labels normalised: trailing colon removed, whitespace collapsed) */ fields: Record; /** `
`-separated multi-line values, kept as arrays */ lines: Record; stats: string[]; populationUrl: string | null; /** population at the cert's grade ("In 9.0: 2" → 2); null when absent or "Coming Soon!" */ atGrade: number | null; higher: number | null; /** grade key as printed in the stats line ("9.0", "MS 61", "67EPQ") */ popGradeKey: string | null; images: string[]; } /** Trim a CCG document to its results pane (fixtures store this). */ export function ccgTrim(doc: string): string { const $ = H.load(doc); $('script, style, noscript, iframe, svg').remove(); const pane = $('.results-pane').first(); if (pane.length) return $.html(pane); const form = $('form[name="form"], .certlookup-form').first().closest('div'); const err = $('.error, .error-message').first(); return `
${form.length ? $.html(form) : ''}${err.length ? $.html(err) : ''}
`; } export function ccgClassify(doc: string): PageStatus { const $ = H.load(doc); const hasCert = $('dl dt') .toArray() .some((dt) => /cert/i.test($(dt).text())); if (hasCert && $('dl dd').length) return 'found'; const text = clean($('body').text() || $.root().text()); if (/cannot be found|could not be found|not be found|no results/i.test(text)) return 'not_found'; // Rendered lookup form without results (e.g. NGC/PMG URL missing the grade) is a definitive "nothing here". if ($('form[name="form"], .certlookup-form, input[name="CertNum"], .certlookup-search-box').length) return 'not_found'; return 'unknown'; } export function ccgParse(snapshot: string): CcgPage { const $ = H.load(snapshot); const fields: Record = {}; const lines: Record = {}; $('dl').each((_, dl) => { const dt = clean($(dl).find('dt').first().text()).replace(/\s*:$/, ''); const dd = $(dl).find('dd').first(); if (!dt || !dd.length) return; const html = dd.html() ?? ''; const parts = html .split(//i) .map((h) => clean(H.load(`
${h}
`)('div').text())) .filter(Boolean); lines[dt] = parts; fields[dt] = parts.join(' | '); }); const stats = $('.certlookup-stats-item-value') .toArray() .map((e) => clean($(e).text())) .filter(Boolean); let atGrade: number | null = null; let higher: number | null = null; let popGradeKey: string | null = null; for (const s of stats) { const h = s.match(/^In Higher Grades?:\s*(.+)$/i); if (h) { higher = toInt(h[1]); continue; } const g = s.match(/^In\s+(.+?)(?:\s+Grade)?:\s*(.+)$/i); if (g) { popGradeKey = clean(g[1]); atGrade = toInt(g[2]); } } const populationUrl = $('.certlookup-stats a[href*="population-report"]').first().attr('href') ?? null; const images = [ ...new Set( $('.certlookup-images a[href]') .toArray() .map((a) => $(a).attr('href') ?? '') .filter((u) => /^https?:\/\//.test(u)), ), ]; if (!images.length) { for (const img of $('.certlookup-images img').toArray()) { const src = $(img).attr('src'); if (src && /^https?:\/\//.test(src)) images.push(src); } } return { fields, lines, stats, populationUrl, atGrade, higher, popGradeKey, images }; } /** First present value among several label spellings. */ export function pick(fields: Record, ...labels: string[]): string | null { for (const l of labels) { const hit = Object.keys(fields).find((k) => k.toLowerCase() === l.toLowerCase()); if (hit && fields[hit]) return fields[hit]!; } return null; }