/**
* 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;
}