import { load } from "cheerio"; import type { ExtractedRecord, Parser, RawDocument } from "@dci/connectors"; import { certificationsFrom, cleanText, coolingFrom, description, firstText, lastSlug, mwWithContext, parseCityLine, parseMw, record, renewableFrom, statusFromText } from "./shared.js"; /** * CyrusOne facility pages (www.cyrusone.com/data-centers//). HubSpot CMS: *

Frankfurt, Germany

FRA5

, breadcrumb #hs_cos_wrapper_name "Frankfurt, Germany: FRA5", * "Key Features" tiles:

54 MW

Total IT Capacity

,

18,000 sq. m.

Technical IT Space

, * accordions "Technical Specifications" / "Sustainability" (li/span items). * No street address and no coordinates are published → geo left null (city-level from other sources). */ export const cyrusOneFacility: Parser = { name: "cyrusone_facility_v1", version: "1.0.0", pageTypes: ["facility_page"], parse(doc: RawDocument): ExtractedRecord[] { const url = doc.finalUrl; if (!/\/data-centers\/(north-america|emea|apac)\/[a-z0-9-]+\/?$/.test(url)) return []; const $ = load(doc.text); const h1 = firstText($, "h1"); if (!h1) return []; const codeLine = cleanText($("h1").first().nextAll("h3").first().text()); const codes = codeLine ? (codeLine.toUpperCase().match(/\b[A-Z]{2,4}\d{1,2}[A-Z]?\b/g) ?? []) : []; // h1 is normally "City, ST" / "City, Country"; campus pages ("Yorkville Technology Campus") name the place in the meta description ("… campus in Yorkville, Illinois") let cl = parseCityLine(h1); const metaDesc = description(doc.text); if (!h1.includes(",") || !cl.countryIso2) { const inLoc = metaDesc?.match(/\b(?:in|near|located in)\s+([A-Z][A-Za-z .'-]+?),\s*([A-Z][A-Za-z .]+?)(?:[,.;]|\s-|$)/); if (inLoc) { const alt = parseCityLine(`${inLoc[1]}, ${inLoc[2]}`); if (alt.countryIso2) cl = alt; } if (!h1.includes(",") && cl.city === h1) cl = { ...cl, city: null }; } const slug = lastSlug(url); const key = codes.length === 1 ? `cyrusone:${codes[0]}` : `cyrusone:${slug}`; const name = codes.length ? `CyrusOne ${codes.join("/")} (${cl.city ?? h1})` : `CyrusOne ${h1}`; // Key Features tiles let itCapacityMw: number | null = null; let techSpace: string | null = null; $("h3").each((_, e) => { const v = cleanText($(e).text()); const label = cleanText($(e).nextAll("p").first().text()) ?? cleanText($(e).parent().find("p").first().text()); if (!v || !label || !/\d/.test(v)) return; // tile values are figures ("54 MW", "18,000 sq. m."); headings like "Key Features:" are not if (/IT capacity|critical (?:IT )?(?:load|power)/i.test(label) && itCapacityMw == null) itCapacityMw = parseMw(v); if (/technical (?:IT )?space|white ?space|data hall space|raised floor/i.test(label) && !techSpace) techSpace = v; }); const text = cleanText($("main, body").first().text()) ?? ""; const techSpaceStr = techSpace as string | null; // assigned inside the .each callback (TS narrows the let to null otherwise) itCapacityMw ??= mwWithContext(text, /\s*(?:of\s+)?(?:total\s+)?IT\s+capacity/i) ?? mwWithContext(text, /\s*(?:of\s+)?critical\s+(?:IT\s+)?load/i); const accordionItems = $("li, span").filter((_, e) => $(e).children().length === 0).map((_, e) => cleanText($(e).text()) ?? "").get().filter((t) => t.length > 2 && t.length < 160); const certifications = certificationsFrom(accordionItems.filter((t) => /ISO|SOC|PCI|HIPAA|LEED|BREEAM|Uptime|Tier|NIST|FedRAMP|HITRUST|EN\s?50600/i.test(t))); const sustText = accordionItems.filter((t) => /renewable|carbon|green|wind|solar|water|PUE|WUE/i.test(t)).join(". "); const h2 = firstText($, "h2"); const firstP = cleanText($("h2").first().nextAll("div, p").find("p").first().text()) ?? cleanText($("h2").first().next("p").text()); const desc = description(doc.text); const status = statusFromText(`${h1} ${codeLine ?? ""} ${h2 ?? ""} ${firstP ?? ""}`); const data: Record = { name, code: codes.length === 1 ? codes[0] : null, aliases: codes.map((c) => `CyrusOne ${c}`), city: cl.city, regionName: cl.region, countryIso2: cl.countryIso2, itCapacityMw, certifications, renewableClaim: renewableFrom(sustText), coolingType: coolingFrom(`${sustText} ${accordionItems.join(". ")}`), status, description: [desc, techSpaceStr ? `Technical IT space: ${techSpaceStr.replace(/\.+$/, "")}.` : null].filter(Boolean).join(" ") || null, externalIds: { cyrusone_slug: slug }, }; const methods: Record = { name: "selector:h1+h3", code: "selector:h1~h3", city: "selector:h1+parse", regionName: "selector:h1+parse", countryIso2: "selector:h1+parse>country", itCapacityMw: "selector:key-features(h3+p 'IT Capacity')>mw", certifications: "selector:accordion li+regex", renewableClaim: "selector:accordion li+regex", coolingType: "regex:cooling_v1", status: "regex:status_v1", description: "meta:description+key-features", externalIds: "url:slug", }; return [record("facility", key, url, data, methods, codes.length ? 0.9 : 0.75)]; }, };