SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.7 KB · 69 lines typescript
Raw Blame History
1import { load } from "cheerio";2import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors";3import { cleanText, cloudsIn, coolingFrom, countryFromText, description, extractCode, firstText, ixpsIn, lastSlug, mwWithContext, parseAddress, record, renewableFrom, statusFromText } from "./shared.js";45/**6 * NTT Global Data Centers facility pages7 * (services.global.ntt/en-us/services-and-products/global-data-centers/global-locations/<region>/<slug>).8 * Sitecore page: h1.hero-image-video__title, p.hero-image-video__strapline ("12.6MW of critical IT load…"), an <h4> with the9 * street address followed by "Contact number", and h3.icon__title / p.icon__summary spec blocks (Power, Connectivity, Cooling, Security).10 * Metro index pages end in "-data-centers" and are skipped. Campus-level totals ("224MW campus") are never used as facility MW.11 */12export const nttFacility: Parser = {13  name: "ntt_facility_v1",14  version: "1.0.0",15  pageTypes: ["facility_page"],16  parse(doc: RawDocument): ExtractedRecord[] {17    const url = doc.finalUrl;18    if (/-data-centers\/?$|\/virtual-tour\/?$|\/global-locations\/?$/.test(url)) return [];19    const m = url.match(/\/global-locations\/([a-z-]+)\/([a-z0-9-]+)\/?$/);20    if (!m) return [];21    const region = m[1]!;22    const slug = lastSlug(url);23    const $ = load(doc.text);24    const h1 = firstText($, "h1.hero-image-video__title", "h1");25    if (!h1) return [];26    const strap = firstText($, "p.hero-image-video__strapline");27    const summaryTitle = firstText($, "h2.content-summary-old__title", "h2.content-summary__title");28    const desc = description(doc.text);2930    const blocks: Record<string, string> = {};31    $("h3.icon__title").each((_, e) => {32      const k = cleanText($(e).text())?.toLowerCase();33      const v = cleanText($(e).siblings("p.icon__summary").first().text()) ?? cleanText($(e).parent().find("p.icon__summary").first().text());34      if (k && v) blocks[k] = v;35    });36    const power = blocks["power"] ?? null;37    const connectivity = blocks["connectivity"] ?? null;38    const cooling = blocks["cooling"] ?? null;3940    // Address: the <h4> holding "street, city … Contact number: …" (prefer the one with "Contact number"; card titles with digits are not addresses)41    const h4s = $("h4").map((_, e) => cleanText($(e).text()) ?? "").get().filter(Boolean);42    const addrH4 = h4s.find((t) => /Contact number/i.test(t) && /\d/.test(t.split(/Contact number/i)[0] ?? "")) ?? h4s.find((t) => /^\d+[\w\s.,'-]*,\s*[A-Za-z]/.test(t) || /\b\d{4,6}\b.*,/.test(t));43    const addrStr: string | null = addrH4 ? (cleanText(addrH4.split(/Contact number/i)[0] ?? "")?.replace(/[,\s]+$/, "").replace(/,(\S)/g, ", $1") ?? null) : null;44    const pa = parseAddress(addrStr);4546    const mwText = [strap, power, summaryTitle, desc].filter(Boolean).join(" \n ");47    const itCapacityMw = mwWithContext(mwText, /\s*(?:of\s+)?critical\s+(?:IT\s+)?(?:load|capacity|power)/i) ?? mwWithContext(mwText, /\s*(?:of\s+)?IT\s+(?:load|capacity)/i);48    const code = extractCode(h1);49    const cityGuess = cleanText(h1.replace(/\b[A-Z]{2,4}\s?\d{1,2}[A-Za-z]?\b/g, " ").replace(/\b(Data\s+Cent(?:er|re)s?|Campus)\b/gi, " ").replace(/\b\d+[a-z]?\b/g, " ").replace(/\b(I{1,3}V?|and)\b/g, " "));50    const countryIso2 = pa?.countryIso2 ?? countryFromText(addrStr) ?? countryFromText(h1) ?? (region === "india" ? "IN" : null);51    const status = statusFromText(`${h1} ${strap ?? ""}`);5253    const data: Record<string, unknown> = {54      name: h1, code, aliases: code ? [`NTT ${code}`] : [],55      address: pa?.address ?? addrStr, city: pa?.city ?? (pa?.address ? null : cityGuess), regionName: pa?.region ?? null, postalCode: pa?.postal ?? null, countryIso2,56      itCapacityMw, coolingType: coolingFrom(cooling), renewableClaim: renewableFrom(power),57      carriers: [], cloudProviders: cloudsIn(connectivity), ixps: ixpsIn(connectivity),58      status, description: desc ?? strap,59      externalIds: { ntt_slug: slug },60    };61    const methods: Record<string, string> = {62      name: "selector:h1.hero-image-video__title", code: "regex:code_v1", address: "selector:h4+parse", city: pa?.city ? "selector:h4+parse" : "selector:h1+strip", regionName: "selector:h4+parse", postalCode: "selector:h4+parse", countryIso2: "selector:h4+parse>country",63      itCapacityMw: "regex:mw_critical_it_load_v1", coolingType: "selector:p.icon__summary(cooling)+regex", renewableClaim: "selector:p.icon__summary(power)+regex", cloudProviders: "selector:p.icon__summary(connectivity)+regex", ixps: "selector:p.icon__summary(connectivity)+regex",64      status: "regex:status_v1", description: "meta:description", externalIds: "url:slug",65    };66    return [record("facility", `ntt:${slug}`, url, data, methods, 0.9)];67  },68};69