import { load } from "cheerio"; import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors"; import { certificationsFrom, cleanText, countryFromText, description, lastSlug, record, renewableFrom, statusFromText, usStateCode } from "./shared.js"; /** * Equinix IBX pages (www.equinix.com/data-centers/-colocation/-colocation/-data-centers/). * The page embeds plain JS constants for the map (title, metro, address1/2, city, postalCode, country, latitude, longitude) * plus a spec table (labelvalue) and a certifications list. No MW figure is published per IBX. */ function jsVar(html: string, name: string): string | null { const m = html.match(new RegExp(`const ${name} = "((?:[^"\\\\]|\\\\.)*)"`)); if (!m) return null; try { return cleanText(JSON.parse(`"${m[1]}"`) as string); } catch { return cleanText(m[1]!); } } export const equinixFacility: Parser = { name: "equinix_facility_v1", version: "1.0.0", pageTypes: ["facility_page"], parse(doc: RawDocument): ExtractedRecord[] { const html = doc.text; if (!html || !/const latitude = /.test(html)) return []; const $ = load(html); const code = (jsVar(html, "title") ?? lastSlug(doc.finalUrl)).toUpperCase(); if (!/^[A-Z]{2,3}\d{1,2}[A-Z]?$/.test(code)) return []; const h1 = cleanText($("h1#hero-banner-heading").first().text()) ?? cleanText($("h1").first().text()); const metro = jsVar(html, "metro"); const city = jsVar(html, "city"); const address1 = jsVar(html, "address1"); const address2 = jsVar(html, "address2"); const postalCode = jsVar(html, "postalCode"); const country = jsVar(html, "country"); const ibxType = jsVar(html, "ibxType"); const lat = Number(jsVar(html, "latitude")); const lng = Number(jsVar(html, "longitude")); const desc = description(html); // US state from the meta description ("… located at 21715 Filigree Court, Ashburn, VA. See …") let regionName: string | null = null; const rm = desc?.match(/,\s*([A-Z]{2})\.\s/); if (rm && usStateCode(rm[1])) regionName = rm[1]!; // headline metrics: N+1Power redundancy const metrics: Record = {}; $('span[data-role="metric"]').each((_, el) => { const q = cleanText($(el).siblings('span[data-role="qualifier"]').first().text()); const v = cleanText($(el).text()); if (q && v) metrics[q.toLowerCase()] = v; }); // spec table rows const specs: Record = {}; $("table tr").each((_, tr) => { const tds = $(tr).find("td"); if (tds.length >= 2) { const k = cleanText(tds.eq(0).text()); const v = cleanText(tds.eq(1).text()); if (k && v) specs[k.toLowerCase()] = v; } }); const coloSpaceRaw = specs["colocation space"] ?? null; const coloM = coloSpaceRaw?.match(/([\d,.]+)\s*m(?:²|2)?\b/); const coloSqm = coloM ? Number(coloM[1]!.replace(/,/g, "")) : null; // certifications block const certHeader = $("h3").filter((_, e) => /Certified to global standards/i.test($(e).text())).first(); let certScope = certHeader.closest("article"); if (!certScope.length) certScope = certHeader.parents().filter((_, e) => $(e).find("ul li").length > 0).first(); const certItems = certScope.find("li span").filter((_, e) => !$(e).attr("data-role") && !$(e).find("svg").length).map((_, e) => cleanText($(e).text()) ?? "").get().filter(Boolean); const certifications = certificationsFrom(certItems); // Sustainability row: keep only the bullet that states the renewable / REC claim const sustRow = $("table tr").filter((_, tr) => /^sustainability$/i.test(cleanText($(tr).find("td").first().text()) ?? "")).first(); const sustItems = sustRow.find("li").map((_, li) => cleanText($(li).text()) ?? "").get().filter(Boolean); const sustainability = sustItems.find((t) => /\b(RECs?|renewable|wind|solar|hydro|carbon[- ]free|green[- ]e|PPA)\b/i.test(t)) ?? null; const heroP = cleanText($("h1").first().nextAll("p").first().text()); const status = statusFromText(`${h1 ?? ""} ${heroP ?? ""}`); const name = h1 && h1.toUpperCase().includes(code) ? h1 : `${metro ?? city ?? "Equinix"} ${code}`; const extraDesc = [coloSpaceRaw ? `Colocation space: ${coloSpaceRaw}.` : null, metrics["power redundancy"] ? `Power redundancy: ${metrics["power redundancy"]}.` : null, specs["ups redundancy"] ? `UPS redundancy: ${specs["ups redundancy"]}.` : null].filter(Boolean).join(" "); const data: Record = { name, code, aliases: [`Equinix ${code}`, `${code} IBX`], address: [address1, address2].filter(Boolean).join(", ") || null, city, regionName, postalCode, countryIso2: country ? countryFromText(country) : null, lat: Number.isFinite(lat) && lat !== 0 ? lat : null, lng: Number.isFinite(lng) && lng !== 0 ? lng : null, geoPrecision: "exact", certifications, renewableClaim: renewableFrom(sustainability), status, facilityType: ibxType && /xscale/i.test(ibxType) ? "hyperscale" : "colocation", description: [desc, extraDesc].filter(Boolean).join(" ") || null, campusName: metro && /campus/i.test(heroP ?? "") ? `${metro} campus` : null, externalIds: { equinix_ibx: code }, _coloSqm: coloSqm, }; delete data._coloSqm; // colocation (white) space is not building size — kept in description only const methods: Record = { name: "selector:h1#hero-banner-heading", code: "js-var:title", address: "js-var:address1", city: "js-var:city", regionName: "meta:description+regex:state", postalCode: "js-var:postalCode", countryIso2: "js-var:country>country", lat: "js-var:latitude", lng: "js-var:longitude", certifications: "selector:certifications-list", renewableClaim: "table:sustainability+regex:renewable_v1", status: "regex:status_v1", facilityType: "js-var:ibxType", description: "meta:description+table:specs", campusName: "regex:campus", externalIds: "js-var:title", }; return [record("facility", `equinix:${code}`, doc.finalUrl, data, methods, 0.95)]; }, };