SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.3 KB · 111 lines typescript
Raw Blame History
1/**2 * Parser for the Certified Collectibles Group cert-verification pages (CGC Comics, CGC Cards, NGC, PMG):3 * all four sites share one AngularJS template — a `.results-pane` holding `<dl><dt>label</dt><dd>value</dd></dl>`4 * pairs, `.certlookup-stats-item-value` population lines ("In 9.0: 2", "In Higher Grades: 0") and a5 * `.certlookup-images` gallery. Not-found pages render the lookup form with "This item cannot be found".6 */7import { html as H } from '@rareindex/connectors';8import { clean, toInt } from './text.js';9import type { PageStatus } from './cert-base.js';1011export interface CcgPage {12  /** label → value (labels normalised: trailing colon removed, whitespace collapsed) */13  fields: Record<string, string>;14  /** `<br>`-separated multi-line values, kept as arrays */15  lines: Record<string, string[]>;16  stats: string[];17  populationUrl: string | null;18  /** population at the cert's grade ("In 9.0: 2" → 2); null when absent or "Coming Soon!" */19  atGrade: number | null;20  higher: number | null;21  /** grade key as printed in the stats line ("9.0", "MS 61", "67EPQ") */22  popGradeKey: string | null;23  images: string[];24}2526/** Trim a CCG document to its results pane (fixtures store this). */27export function ccgTrim(doc: string): string {28  const $ = H.load(doc);29  $('script, style, noscript, iframe, svg').remove();30  const pane = $('.results-pane').first();31  if (pane.length) return $.html(pane);32  const form = $('form[name="form"], .certlookup-form').first().closest('div');33  const err = $('.error, .error-message').first();34  return `<div class="results-pane g2-trimmed">${form.length ? $.html(form) : ''}${err.length ? $.html(err) : ''}</div>`;35}3637export function ccgClassify(doc: string): PageStatus {38  const $ = H.load(doc);39  const hasCert = $('dl dt')40    .toArray()41    .some((dt) => /cert/i.test($(dt).text()));42  if (hasCert && $('dl dd').length) return 'found';43  const text = clean($('body').text() || $.root().text());44  if (/cannot be found|could not be found|not be found|no results/i.test(text)) return 'not_found';45  // Rendered lookup form without results (e.g. NGC/PMG URL missing the grade) is a definitive "nothing here".46  if ($('form[name="form"], .certlookup-form, input[name="CertNum"], .certlookup-search-box').length) return 'not_found';47  return 'unknown';48}4950export function ccgParse(snapshot: string): CcgPage {51  const $ = H.load(snapshot);52  const fields: Record<string, string> = {};53  const lines: Record<string, string[]> = {};54  $('dl').each((_, dl) => {55    const dt = clean($(dl).find('dt').first().text()).replace(/\s*:$/, '');56    const dd = $(dl).find('dd').first();57    if (!dt || !dd.length) return;58    const html = dd.html() ?? '';59    const parts = html60      .split(/<br\s*\/?>/i)61      .map((h) => clean(H.load(`<div>${h}</div>`)('div').text()))62      .filter(Boolean);63    lines[dt] = parts;64    fields[dt] = parts.join(' | ');65  });66  const stats = $('.certlookup-stats-item-value')67    .toArray()68    .map((e) => clean($(e).text()))69    .filter(Boolean);70  let atGrade: number | null = null;71  let higher: number | null = null;72  let popGradeKey: string | null = null;73  for (const s of stats) {74    const h = s.match(/^In Higher Grades?:\s*(.+)$/i);75    if (h) {76      higher = toInt(h[1]);77      continue;78    }79    const g = s.match(/^In\s+(.+?)(?:\s+Grade)?:\s*(.+)$/i);80    if (g) {81      popGradeKey = clean(g[1]);82      atGrade = toInt(g[2]);83    }84  }85  const populationUrl = $('.certlookup-stats a[href*="population-report"]').first().attr('href') ?? null;86  const images = [87    ...new Set(88      $('.certlookup-images a[href]')89        .toArray()90        .map((a) => $(a).attr('href') ?? '')91        .filter((u) => /^https?:\/\//.test(u)),92    ),93  ];94  if (!images.length) {95    for (const img of $('.certlookup-images img').toArray()) {96      const src = $(img).attr('src');97      if (src && /^https?:\/\//.test(src)) images.push(src);98    }99  }100  return { fields, lines, stats, populationUrl, atGrade, higher, popGradeKey, images };101}102103/** First present value among several label spellings. */104export function pick(fields: Record<string, string>, ...labels: string[]): string | null {105  for (const l of labels) {106    const hit = Object.keys(fields).find((k) => k.toLowerCase() === l.toLowerCase());107    if (hit && fields[hit]) return fields[hit]!;108  }109  return null;110}111