@pharmatools/drug-data 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,22 @@ await compareLabels("atorvastatin");
37
37
 
38
38
  CommonJS consumers use `require("@pharmatools/drug-data")` — same API.
39
39
 
40
+ ### General (all-section) access
41
+
42
+ `getLabels` returns a compact 5-field view (uses, dosing, contraindications,
43
+ warnings, adverse). For every available label section, use the general API:
44
+
45
+ ```ts
46
+ import { getUsLabel, getUkSmpc } from "@pharmatools/drug-data";
47
+
48
+ await getUsLabel("atorvastatin"); // { drugName, setId, publishedDate, sections[], dailymedUrl }
49
+ await getUsLabel("atorvastatin", ["drug interactions"]); // filter to specific sections
50
+ await getUkSmpc("atorvastatin"); // { drugName, productId, sections[], url } — all SmPC sections
51
+ await getUkSmpc("atorvastatin", ["4.5"]); // filter by section number or name
52
+ ```
53
+
54
+ Each `section` is `{ code, title, content }`.
55
+
40
56
  ## Develop
41
57
 
42
58
  ```bash
package/dist/index.cjs CHANGED
@@ -36,6 +36,8 @@ __export(index_exports, {
36
36
  formatFdaDate: () => formatFdaDate,
37
37
  getLabels: () => getLabels,
38
38
  getSmpc: () => getSmpc,
39
+ getUkSmpc: () => getUkSmpc,
40
+ getUsLabel: () => getUsLabel,
39
41
  getUspi: () => getUspi,
40
42
  resolveName: () => resolveName
41
43
  });
@@ -166,9 +168,17 @@ async function resolveViaRxNorm(name) {
166
168
  brandNames = single.length ? single : fallback;
167
169
  }
168
170
  let genericName = null;
169
- if (minNames.length) genericName = titleCase(minNames[0]);
170
- else if (inNames.length === 1) genericName = titleCase(inNames[0]);
171
- else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(" / "));
171
+ if (tty === "IN" || tty === "MIN") {
172
+ genericName = titleCase(canonicalName);
173
+ } else if (minNames.length) {
174
+ genericName = titleCase(minNames[0]);
175
+ } else if (inNames.length === 1) {
176
+ genericName = titleCase(inNames[0]);
177
+ } else if (inNames.length > 1) {
178
+ genericName = titleCase(uniq(inNames).join(" / "));
179
+ } else {
180
+ genericName = titleCase(canonicalName);
181
+ }
172
182
  const classRxcui = !isBrand ? rxcui : (() => {
173
183
  const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x) => x.tty === "IN");
174
184
  return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;
@@ -223,7 +233,30 @@ async function resolveName(name) {
223
233
  var cheerio = __toESM(require("cheerio"), 1);
224
234
  var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
225
235
  var EMC_BASE = "https://www.medicines.org.uk";
226
- async function getUspi(drug) {
236
+ var US_SECTION_MAP = [
237
+ { field: "boxed_warning", code: "34066-1", title: "Boxed Warning" },
238
+ { field: "indications_and_usage", code: "34067-9", title: "Indications and Usage" },
239
+ { field: "dosage_and_administration", code: "34068-7", title: "Dosage and Administration" },
240
+ { field: "dosage_forms_and_strengths", code: "43678-2", title: "Dosage Forms and Strengths" },
241
+ { field: "contraindications", code: "34070-3", title: "Contraindications" },
242
+ { field: "warnings_and_cautions", code: "43685-7", title: "Warnings and Precautions" },
243
+ { field: "warnings", code: "34071-1", title: "Warnings" },
244
+ { field: "adverse_reactions", code: "34084-4", title: "Adverse Reactions" },
245
+ { field: "drug_interactions", code: "34073-7", title: "Drug Interactions" },
246
+ { field: "use_in_specific_populations", code: "42228-7", title: "Use in Specific Populations" },
247
+ { field: "overdosage", code: "34088-5", title: "Overdosage" },
248
+ { field: "clinical_pharmacology", code: "34090-1", title: "Clinical Pharmacology" },
249
+ { field: "description", code: "34089-3", title: "Description" },
250
+ { field: "how_supplied", code: "34069-5", title: "How Supplied" }
251
+ ];
252
+ function score(r, q) {
253
+ const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
254
+ let s = 0;
255
+ if (g && g.split(/,| and /).length === 1) s += 2;
256
+ if (g.includes(q.toLowerCase())) s += 1;
257
+ return s;
258
+ }
259
+ async function fetchOpenFdaBest(drug) {
227
260
  if (!drug) return null;
228
261
  const q = drug.trim().replace(/"/g, "");
229
262
  const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
@@ -231,8 +264,49 @@ async function getUspi(drug) {
231
264
  const data = await getJson(url);
232
265
  const results = data?.results ?? [];
233
266
  if (results.length === 0) return null;
234
- const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));
235
- const r = ranked[0];
267
+ return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];
268
+ }
269
+ function usMeta(r, fallbackName) {
270
+ const setId = r.openfda?.spl_set_id?.[0] ?? "";
271
+ const brand = r.openfda?.brand_name?.[0] ?? "";
272
+ const generic = r.openfda?.generic_name?.[0] ?? fallbackName;
273
+ return {
274
+ setId,
275
+ brand,
276
+ generic,
277
+ productName: brand ? `${brand} (${generic})` : generic,
278
+ dailymedUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/"
279
+ };
280
+ }
281
+ function matchesUsFilter(entry, requested) {
282
+ return requested.some((req) => {
283
+ const r = req.toLowerCase().trim();
284
+ return entry.code === req || entry.title.toLowerCase().includes(r) || String(entry.field).replace(/_/g, " ").includes(r) || r.includes(entry.title.toLowerCase());
285
+ });
286
+ }
287
+ async function getUsLabel(drug, requested) {
288
+ const r = await fetchOpenFdaBest(drug);
289
+ if (!r) return null;
290
+ const meta = usMeta(r, drug.trim());
291
+ const sections = [];
292
+ for (const entry of US_SECTION_MAP) {
293
+ if (requested?.length && !matchesUsFilter(entry, requested)) continue;
294
+ const content = cleanSection(r[entry.field]);
295
+ if (content) sections.push({ code: entry.code, title: entry.title, content });
296
+ }
297
+ if (sections.length === 0) return null;
298
+ return {
299
+ drugName: meta.productName,
300
+ setId: meta.setId,
301
+ publishedDate: formatFdaDate(r.effective_time),
302
+ sections,
303
+ dailymedUrl: meta.dailymedUrl
304
+ };
305
+ }
306
+ async function getUspi(drug) {
307
+ const r = await fetchOpenFdaBest(drug);
308
+ if (!r) return null;
309
+ const meta = usMeta(r, drug.trim());
236
310
  const dosage = cleanSection(r.dosage_and_administration);
237
311
  const boxed = cleanSection(r.boxed_warning);
238
312
  const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
@@ -241,31 +315,21 @@ async function getUspi(drug) {
241
315
  const indications = cleanSection(r.indications_and_usage);
242
316
  const contraindications = cleanSection(r.contraindications);
243
317
  if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
244
- const setId = r.openfda?.spl_set_id?.[0] ?? "";
245
- const brand = r.openfda?.brand_name?.[0] ?? "";
246
- const generic = r.openfda?.generic_name?.[0] ?? q;
247
318
  return {
248
319
  region: "US",
249
320
  authority: "FDA Prescribing Information",
250
- productName: brand ? `${brand} (${generic})` : generic,
321
+ productName: meta.productName,
251
322
  indications: indications || null,
252
323
  dosage: dosage || null,
253
324
  contraindications: contraindications || null,
254
325
  warnings: warnings || null,
255
326
  adverse: adverse || null,
256
327
  updated: formatFdaDate(r.effective_time),
257
- sourceUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/",
328
+ sourceUrl: meta.dailymedUrl,
258
329
  sourceLabel: "DailyMed (U.S. National Library of Medicine)"
259
330
  };
260
331
  }
261
- function score(r, q) {
262
- const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
263
- let s = 0;
264
- if (g && g.split(/,| and /).length === 1) s += 2;
265
- if (g.includes(q.toLowerCase())) s += 1;
266
- return s;
267
- }
268
- async function searchEmc(drug) {
332
+ async function searchEmcAll(drug) {
269
333
  const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
270
334
  const res = await timedFetch(url);
271
335
  if (!res.ok) return [];
@@ -283,13 +347,15 @@ async function searchEmc(drug) {
283
347
  });
284
348
  return out;
285
349
  }
286
- var SMPC_WANT = {
287
- "4.1": "indications",
288
- "4.2": "dosage",
289
- "4.3": "contraindications",
290
- "4.4": "warnings",
291
- "4.8": "adverse"
292
- };
350
+ async function searchEmcBest(drug) {
351
+ const matches = await searchEmcAll(drug);
352
+ if (matches.length === 0) return null;
353
+ const q = drug.toLowerCase();
354
+ matches.sort(
355
+ (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
356
+ );
357
+ return matches[0];
358
+ }
293
359
  var SMPC_SECTION_NAMES = {
294
360
  "4.1": "Therapeutic indications",
295
361
  "4.2": "Posology and method of administration",
@@ -302,7 +368,13 @@ var SMPC_SECTION_NAMES = {
302
368
  "4.9": "Overdose",
303
369
  "5.1": "Pharmacodynamic properties",
304
370
  "5.2": "Pharmacokinetic properties",
305
- "5.3": "Preclinical safety data"
371
+ "5.3": "Preclinical safety data",
372
+ "6.1": "List of excipients",
373
+ "6.2": "Incompatibilities",
374
+ "6.3": "Shelf life",
375
+ "6.4": "Special precautions for storage",
376
+ "6.5": "Nature and contents of container",
377
+ "6.6": "Special precautions for disposal"
306
378
  };
307
379
  function htmlToText(html) {
308
380
  const $ = cheerio.load(html);
@@ -316,7 +388,25 @@ function htmlToText(html) {
316
388
  });
317
389
  return $.text().replace(/\n{3,}/g, "\n\n").trim();
318
390
  }
319
- function parseSmpcSections(html) {
391
+ function matchesSmpcFilter(code, title, requested) {
392
+ return requested.some((req) => {
393
+ const r = req.toLowerCase().trim();
394
+ if (code === req || code === r) return true;
395
+ if (title.toLowerCase().includes(r)) return true;
396
+ if (r.includes("indication")) return code === "4.1";
397
+ if (r.includes("dosage") || r.includes("dosing") || r.includes("posology")) return code === "4.2";
398
+ if (r.includes("contraindication")) return code === "4.3";
399
+ if (r.includes("warning") || r.includes("precaution")) return code === "4.4";
400
+ if (r.includes("interaction")) return code === "4.5";
401
+ if (r.includes("pregnancy") || r.includes("fertility")) return code === "4.6";
402
+ if (r.includes("adverse") || r.includes("undesirable") || r.includes("side effect")) return code === "4.8";
403
+ if (r.includes("overdose") || r.includes("overdosage")) return code === "4.9";
404
+ if (r.includes("pharmacodynamic")) return code === "5.1";
405
+ if (r.includes("pharmacokinetic")) return code === "5.2";
406
+ return false;
407
+ });
408
+ }
409
+ function parseSmpcSections(html, requested) {
320
410
  const $ = cheerio.load(html);
321
411
  const sectionElements = [];
322
412
  $("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
@@ -335,11 +425,9 @@ function parseSmpcSections(html) {
335
425
  }
336
426
  });
337
427
  }
338
- const fields = {};
428
+ const out = [];
339
429
  for (let i = 0; i < sectionElements.length; i++) {
340
430
  const current = sectionElements[i];
341
- const field = SMPC_WANT[current.code];
342
- if (!field) continue;
343
431
  const next = sectionElements[i + 1];
344
432
  let content = "";
345
433
  let node = current.element.next();
@@ -351,39 +439,59 @@ function parseSmpcSections(html) {
351
439
  }
352
440
  if (!content.trim()) {
353
441
  const parentHtml = $.html(current.element.parent());
354
- if (parentHtml) {
355
- content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
356
- }
442
+ if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
357
443
  }
358
- fields[field] = cleanSection(content);
444
+ const cleaned = cleanSection(content);
445
+ if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });
446
+ }
447
+ if (requested?.length) {
448
+ return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));
449
+ }
450
+ return out;
451
+ }
452
+ async function getUkSmpc(drug, requested) {
453
+ if (!drug) return null;
454
+ try {
455
+ const best = await searchEmcBest(drug);
456
+ if (!best) return null;
457
+ const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
458
+ if (!res.ok) return null;
459
+ const sections = parseSmpcSections(await res.text(), requested);
460
+ if (sections.length === 0) return null;
461
+ return {
462
+ drugName: best.name,
463
+ productId: best.product_id,
464
+ sections,
465
+ url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`
466
+ };
467
+ } catch {
468
+ return null;
359
469
  }
360
- return fields;
361
470
  }
362
471
  async function getSmpc(drug) {
363
472
  if (!drug) return null;
364
473
  try {
365
- const matches = await searchEmc(drug);
366
- if (matches.length === 0) return null;
367
- const q = drug.toLowerCase();
368
- matches.sort(
369
- (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
370
- );
371
- const best = matches[0];
474
+ const best = await searchEmcBest(drug);
475
+ if (!best) return null;
372
476
  const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
373
477
  if (!res.ok) return null;
374
- const html = await res.text();
375
- const sec = parseSmpcSections(html);
376
- if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)
377
- return null;
478
+ const all = parseSmpcSections(await res.text());
479
+ const pick = (code) => all.find((s) => s.code === code)?.content ?? null;
480
+ const indications = pick("4.1");
481
+ const dosage = pick("4.2");
482
+ const contraindications = pick("4.3");
483
+ const warnings = pick("4.4");
484
+ const adverse = pick("4.8");
485
+ if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
378
486
  return {
379
487
  region: "UK/EU",
380
488
  authority: "Summary of Product Characteristics (SmPC)",
381
489
  productName: best.name,
382
- indications: sec.indications || null,
383
- dosage: sec.dosage || null,
384
- contraindications: sec.contraindications || null,
385
- warnings: sec.warnings || null,
386
- adverse: sec.adverse || null,
490
+ indications,
491
+ dosage,
492
+ contraindications,
493
+ warnings,
494
+ adverse,
387
495
  updated: "",
388
496
  sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
389
497
  sourceLabel: "electronic medicines compendium (emc), UK"
@@ -430,6 +538,8 @@ async function compareLabels(drug) {
430
538
  formatFdaDate,
431
539
  getLabels,
432
540
  getSmpc,
541
+ getUkSmpc,
542
+ getUsLabel,
433
543
  getUspi,
434
544
  resolveName
435
545
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["// @pharmatools/drug-data — public surface\n\nexport { resolveName } from \"./names.js\";\nexport { getUspi, getSmpc, getLabels } from \"./labels.js\";\nexport { compareLabels, SECTION_MAP } from \"./compare.js\";\nexport { cleanSection, formatFdaDate } from \"./fetch.js\";\nexport type {\n Region,\n RegionLabel,\n LabelResult,\n ResolveResult,\n LabelComparison,\n CompareResult,\n} from \"./types.js\";\n","/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic display name: prefer the canonical combo string, else single, else joined.\n let genericName: string | null = null;\n if (minNames.length) genericName = titleCase(minNames[0]);\n else if (inNames.length === 1) genericName = titleCase(inNames[0]);\n else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(\" / \"));\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Every network call is best-effort: any failure returns null so callers never\n * break. Pure fetch + parse — caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n dosage_and_administration?: string[];\n boxed_warning?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n indications_and_usage?: string[];\n contraindications?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n/* --------------------------- US: openFDA --------------------------- */\n\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n\n // Prefer a single-ingredient product over combination labels.\n const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));\n const r = ranked[0];\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? q;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: brand ? `${brand} (${generic})` : generic,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmc(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\n// SmPC section number -> the field we store it under\nconst SMPC_WANT: Record<string, keyof Pick<RegionLabel, \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\">> = {\n \"4.1\": \"indications\",\n \"4.2\": \"dosage\",\n \"4.3\": \"contraindications\",\n \"4.4\": \"warnings\",\n \"4.8\": \"adverse\",\n};\n\n// Canonical SmPC section titles — used as a fallback heading detector\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\n/**\n * Parse the wanted SmPC sections from eMC HTML. Detect numbered headings, then\n * collect sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string): Record<string, string> {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n // Strategy 1: headings whose text starts with a section number\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n // Strategy 2: fall back to scanning leaf nodes for known section headers\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const fields: Record<string, string> = {};\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const field = SMPC_WANT[current.code];\n if (!field) continue;\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) {\n content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n }\n\n fields[field] = cleanSection(content);\n }\n return fields;\n}\n\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const matches = await searchEmc(drug);\n if (matches.length === 0) return null;\n\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n const best = matches[0];\n\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const html = await res.text();\n const sec = parseSmpcSections(html);\n\n if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)\n return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications: sec.indications || null,\n dosage: sec.dosage || null,\n contraindications: sec.contraindications || null,\n warnings: sec.warnings || null,\n adverse: sec.adverse || null,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both labels for a drug. Each side is null if unavailable. Never throws. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["// @pharmatools/drug-data — public surface\n\nexport { resolveName } from \"./names.js\";\n// 5-field convenience view (Pharmonym)\nexport { getUspi, getSmpc, getLabels } from \"./labels.js\";\n// General all-section access (PubCrawl)\nexport { getUsLabel, getUkSmpc } from \"./labels.js\";\nexport { compareLabels, SECTION_MAP } from \"./compare.js\";\nexport { cleanSection, formatFdaDate } from \"./fetch.js\";\nexport type {\n Region,\n RegionLabel,\n LabelResult,\n LabelSection,\n UsLabelResult,\n UkSmpcResult,\n ResolveResult,\n LabelComparison,\n CompareResult,\n} from \"./types.js\";\n","/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic name must reflect the concept's OWN ingredient(s). A single-ingredient\n // lookup (e.g. \"atorvastatin\", tty IN) relates to every combination it appears\n // in (Atorvastatin / Ezetimibe, ...), so the previous MIN-first logic\n // mis-resolved single generics to arbitrary combinations and broke downstream\n // label lookups. When the input concept is itself an ingredient (IN) or a\n // multi-ingredient concept (MIN), its own canonical name is authoritative;\n // only for brands/products do we read the related MIN as the combo generic.\n let genericName: string | null = null;\n if (tty === \"IN\" || tty === \"MIN\") {\n genericName = titleCase(canonicalName);\n } else if (minNames.length) {\n genericName = titleCase(minNames[0]);\n } else if (inNames.length === 1) {\n genericName = titleCase(inNames[0]);\n } else if (inNames.length > 1) {\n genericName = titleCase(uniq(inNames).join(\" / \"));\n } else {\n genericName = titleCase(canonicalName);\n }\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Two layers:\n * - getUsLabel / getUkSmpc — general, all-section access (LabelSection[]),\n * used by PubCrawl's get_uspi / get_smpc / compare_labels tools.\n * - getUspi / getSmpc / getLabels — a 5-field convenience view (RegionLabel)\n * used by Pharmonym. Built on the same fetch + parse internals.\n *\n * Every network call is best-effort: failures return null. Pure fetch + parse;\n * caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult, LabelSection, UsLabelResult, UkSmpcResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n boxed_warning?: string[];\n indications_and_usage?: string[];\n dosage_and_administration?: string[];\n dosage_forms_and_strengths?: string[];\n contraindications?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n drug_interactions?: string[];\n use_in_specific_populations?: string[];\n overdosage?: string[];\n clinical_pharmacology?: string[];\n description?: string[];\n how_supplied?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n// openFDA field -> { LOINC code, display title }. Order = label reading order.\nconst US_SECTION_MAP: Array<{ field: keyof OpenFdaLabel; code: string; title: string }> = [\n { field: \"boxed_warning\", code: \"34066-1\", title: \"Boxed Warning\" },\n { field: \"indications_and_usage\", code: \"34067-9\", title: \"Indications and Usage\" },\n { field: \"dosage_and_administration\", code: \"34068-7\", title: \"Dosage and Administration\" },\n { field: \"dosage_forms_and_strengths\", code: \"43678-2\", title: \"Dosage Forms and Strengths\" },\n { field: \"contraindications\", code: \"34070-3\", title: \"Contraindications\" },\n { field: \"warnings_and_cautions\", code: \"43685-7\", title: \"Warnings and Precautions\" },\n { field: \"warnings\", code: \"34071-1\", title: \"Warnings\" },\n { field: \"adverse_reactions\", code: \"34084-4\", title: \"Adverse Reactions\" },\n { field: \"drug_interactions\", code: \"34073-7\", title: \"Drug Interactions\" },\n { field: \"use_in_specific_populations\", code: \"42228-7\", title: \"Use in Specific Populations\" },\n { field: \"overdosage\", code: \"34088-5\", title: \"Overdosage\" },\n { field: \"clinical_pharmacology\", code: \"34090-1\", title: \"Clinical Pharmacology\" },\n { field: \"description\", code: \"34089-3\", title: \"Description\" },\n { field: \"how_supplied\", code: \"34069-5\", title: \"How Supplied\" },\n];\n\n/* --------------------------- US: openFDA --------------------------- */\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/** Fetch the most recent FDA label for a drug; prefers single-ingredient products. */\nasync function fetchOpenFdaBest(drug: string): Promise<OpenFdaLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];\n}\n\nfunction usMeta(r: OpenFdaLabel, fallbackName: string) {\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? fallbackName;\n return {\n setId,\n brand,\n generic,\n productName: brand ? `${brand} (${generic})` : generic,\n dailymedUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n };\n}\n\nfunction matchesUsFilter(entry: { field: string; code: string; title: string }, requested: string[]): boolean {\n return requested.some((req) => {\n const r = req.toLowerCase().trim();\n return (\n entry.code === req ||\n entry.title.toLowerCase().includes(r) ||\n String(entry.field).replace(/_/g, \" \").includes(r) ||\n r.includes(entry.title.toLowerCase())\n );\n });\n}\n\n/**\n * General US label access — returns every available section (or the requested\n * subset) as LabelSection[]. Powers PubCrawl's get_uspi.\n */\nexport async function getUsLabel(drug: string, requested?: string[]): Promise<UsLabelResult | null> {\n const r = await fetchOpenFdaBest(drug);\n if (!r) return null;\n const meta = usMeta(r, drug.trim());\n\n const sections: LabelSection[] = [];\n for (const entry of US_SECTION_MAP) {\n if (requested?.length && !matchesUsFilter(entry, requested)) continue;\n const content = cleanSection(r[entry.field]);\n if (content) sections.push({ code: entry.code, title: entry.title, content });\n }\n if (sections.length === 0) return null;\n\n return {\n drugName: meta.productName,\n setId: meta.setId,\n publishedDate: formatFdaDate(r.effective_time),\n sections,\n dailymedUrl: meta.dailymedUrl,\n };\n}\n\n/** 5-field convenience view of the US label (Pharmonym). */\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n const r = await fetchOpenFdaBest(drug);\n if (!r) return null;\n const meta = usMeta(r, drug.trim());\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: meta.productName,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: meta.dailymedUrl,\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmcAll(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\nasync function searchEmcBest(drug: string): Promise<EmcMatch | null> {\n const matches = await searchEmcAll(drug);\n if (matches.length === 0) return null;\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n return matches[0];\n}\n\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n \"6.1\": \"List of excipients\",\n \"6.2\": \"Incompatibilities\",\n \"6.3\": \"Shelf life\",\n \"6.4\": \"Special precautions for storage\",\n \"6.5\": \"Nature and contents of container\",\n \"6.6\": \"Special precautions for disposal\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\nfunction matchesSmpcFilter(code: string, title: string, requested: string[]): boolean {\n return requested.some((req) => {\n const r = req.toLowerCase().trim();\n if (code === req || code === r) return true;\n if (title.toLowerCase().includes(r)) return true;\n if (r.includes(\"indication\")) return code === \"4.1\";\n if (r.includes(\"dosage\") || r.includes(\"dosing\") || r.includes(\"posology\")) return code === \"4.2\";\n if (r.includes(\"contraindication\")) return code === \"4.3\";\n if (r.includes(\"warning\") || r.includes(\"precaution\")) return code === \"4.4\";\n if (r.includes(\"interaction\")) return code === \"4.5\";\n if (r.includes(\"pregnancy\") || r.includes(\"fertility\")) return code === \"4.6\";\n if (r.includes(\"adverse\") || r.includes(\"undesirable\") || r.includes(\"side effect\")) return code === \"4.8\";\n if (r.includes(\"overdose\") || r.includes(\"overdosage\")) return code === \"4.9\";\n if (r.includes(\"pharmacodynamic\")) return code === \"5.1\";\n if (r.includes(\"pharmacokinetic\")) return code === \"5.2\";\n return false;\n });\n}\n\n/**\n * Parse SmPC sections from eMC HTML. Returns every detected numbered section as\n * LabelSection[] (or the requested subset). Detect numbered headings, collect\n * sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string, requested?: string[]): LabelSection[] {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const out: LabelSection[] = [];\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n\n const cleaned = cleanSection(content);\n if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });\n }\n\n if (requested?.length) {\n return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));\n }\n return out;\n}\n\n/**\n * General UK/EU SmPC access — returns every available section (or the requested\n * subset) as LabelSection[]. Powers PubCrawl's get_smpc.\n */\nexport async function getUkSmpc(drug: string, requested?: string[]): Promise<UkSmpcResult | null> {\n if (!drug) return null;\n try {\n const best = await searchEmcBest(drug);\n if (!best) return null;\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const sections = parseSmpcSections(await res.text(), requested);\n if (sections.length === 0) return null;\n return {\n drugName: best.name,\n productId: best.product_id,\n sections,\n url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n };\n } catch {\n return null;\n }\n}\n\n/** 5-field convenience view of the UK/EU SmPC (Pharmonym). */\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const best = await searchEmcBest(drug);\n if (!best) return null;\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const all = parseSmpcSections(await res.text());\n const pick = (code: string) => all.find((s) => s.code === code)?.content ?? null;\n\n const indications = pick(\"4.1\");\n const dosage = pick(\"4.2\");\n const contraindications = pick(\"4.3\");\n const warnings = pick(\"4.4\");\n const adverse = pick(\"4.8\");\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications,\n dosage,\n contraindications,\n warnings,\n adverse,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both 5-field labels for a drug. Each side is null if unavailable. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AAEjB,IAAM,aAAa;AAE1B,eAAsB,WACpB,KACA,OAAoB,CAAC,GACrB,YAAY,oBACO;AACnB,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,MAAI;AACF,WAAO,MAAM,MAAM,KAAK;AAAA,MACtB,GAAG;AAAA,MACH,QAAQ,WAAW;AAAA,MACnB,SAAS,EAAE,cAAc,YAAY,GAAK,KAAK,WAAsC,CAAC,EAAG;AAAA,IAC3F,CAAC;AAAA,EACH,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAsB,QACpB,KACA,YAAY,oBACO;AACnB,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,KAAK,CAAC,GAAG,SAAS;AAC/C,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,aAAa,OAAgB,WAAW,iBAAyB;AAC/E,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,OAAO,KAAK;AAEnE,SAAO,KACJ,QAAQ,OAAO,EAAE,EACjB,QAAQ,WAAW,GAAG,EACtB,QAAQ,WAAW,MAAM,EACzB,KAAK;AAGR,SAAO,KAAK,QAAQ,0CAA0C,EAAE,EAAE,KAAK;AAEvE,MAAI,KAAK,SAAS,UAAU;AAC1B,UAAM,QAAQ,KAAK,MAAM,GAAG,QAAQ;AACpC,UAAM,WAAW,KAAK,IAAI,MAAM,YAAY,IAAI,GAAG,MAAM,YAAY,IAAI,CAAC;AAC1E,YAAQ,WAAW,WAAW,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,OAAO,KAAK,IAAI;AAAA,EACrF;AACA,SAAO;AACT;AAEO,SAAS,cAAc,UAA2B;AACvD,MAAI,CAAC,YAAY,CAAC,UAAU,KAAK,QAAQ,EAAG,QAAO;AACnD,QAAM,IAAI,oBAAI,KAAK,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE;AAC5F,MAAI,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AAC/B,SAAO,EAAE,mBAAmB,SAAS,EAAE,MAAM,WAAW,OAAO,SAAS,KAAK,UAAU,CAAC;AAC1F;AAEO,SAAS,UAAU,GAAmB;AAC3C,SAAO,OAAO,KAAK,EAAE,EAAE,QAAQ,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC;AACtG;AAEO,SAAS,KAAQ,KAAe;AACrC,SAAO,CAAC,GAAG,IAAI,IAAI,IAAI,OAAO,OAAO,CAAC,CAAC;AACzC;;;ACtEA,IAAM,QAAQ;AACd,IAAM,UAAU;AAChB,IAAM,aAAa;AAEnB,IAAM,cAAc;AAIpB,eAAe,UAAU,MAAsC;AAC7D,QAAM,QAAQ,MAAM,QAAa,GAAG,KAAK,oBAAoB,mBAAmB,IAAI,CAAC,IAAI,UAAU;AACnG,QAAM,KAAK,OAAO,SAAS,WAAW,CAAC;AACvC,MAAI,GAAI,QAAO;AAEf,QAAM,SAAS,MAAM;AAAA,IACnB,GAAG,KAAK,8BAA8B,mBAAmB,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,kBAAkB,YAAY,CAAC;AACpD,SAAO,OAAO,KAAK,QAAQ;AAC7B;AAEA,SAAS,aAAa,YAAiB,KAAuB;AAC5D,QAAM,SAAS,YAAY,iBAAiB,gBAAgB,CAAC;AAC7D,QAAM,IAAI,OAAO,KAAK,CAAC,MAAW,EAAE,QAAQ,GAAG;AAC/C,MAAI,CAAC,KAAK,CAAC,EAAE,kBAAmB,QAAO,CAAC;AACxC,SAAO,EAAE,kBAAkB,IAAI,CAAC,MAAW,EAAE,IAAI;AACnD;AAOA,eAAe,0BAA0B,iBAA4C;AACnF,QAAM,OAAO,MAAM,QAAa,GAAG,KAAK,UAAU,eAAe,0BAA0B,UAAU;AACrG,QAAM,SAAS,MAAM,cAAc,gBAAgB,CAAC;AACpD,QAAM,IAAI,OAAO,KAAK,CAAC,MAAW,EAAE,QAAQ,MAAM;AAClD,MAAI,CAAC,KAAK,CAAC,EAAE,kBAAmB,QAAO,CAAC;AACxC,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,EAAE,mBAAmB;AACnC,UAAM,IAAK,EAAE,KAAgB,MAAM,kBAAkB;AACrD,QAAI,CAAC,EAAG;AACR,UAAM,SAAU,EAAE,KAAgB,MAAM,GAAI,EAAE,KAAgB,YAAY,GAAG,CAAC;AAC9E,QAAI,OAAO,SAAS,GAAG,EAAG;AAC1B,WAAO,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC;AAAA,EACzB;AACA,SAAO,KAAK,MAAM,EAAE,KAAK;AAC3B;AAEA,eAAe,oBACb,OACuD;AACvD,QAAM,OAAO,MAAM;AAAA,IACjB,GAAG,KAAK,qCAAqC,KAAK;AAAA,IAClD;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,qBAAqB,mBAAmB,CAAC;AAE7D,QAAM,SAAS,CAAC,MAA6B;AAC3C,UAAM,MAAM,MAAM,KAAK,CAAC,MAAW,EAAE,sBAAsB,cAAc,CAAC;AAC1E,WAAO,MAAM,IAAI,sBAAsB,YAAY;AAAA,EACrD;AAEA,QAAM,YAAY,OAAO,KAAK,KAAK,OAAO,KAAK,KAAK,OAAO,MAAM;AAEjE,QAAM,OAAO;AAAA,IACX,MACG,OAAO,CAAC,MAAW,EAAE,SAAS,WAAW,EACzC,IAAI,CAAC,MAAW,EAAE,sBAAsB,SAAmB;AAAA,EAChE,EAAE,MAAM,GAAG,CAAC;AAEZ,SAAO,EAAE,WAAW,KAAK;AAC3B;AAEA,eAAe,iBAAiB,MAA6C;AAC3E,QAAM,QAAQ,MAAM,UAAU,IAAI;AAClC,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,OAAO,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC5C,QAAa,GAAG,KAAK,UAAU,KAAK,oBAAoB,UAAU;AAAA,IAClE,QAAa,GAAG,KAAK,UAAU,KAAK,oBAAoB,UAAU;AAAA,EACpE,CAAC;AAED,QAAM,MAA0B,OAAO,YAAY;AACnD,QAAM,gBAAwB,OAAO,YAAY,QAAQ;AACzD,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,UAAU,aAAa,YAAY,IAAI;AAC7C,QAAM,WAAW,aAAa,YAAY,KAAK;AAE/C,QAAM,UAAU,QAAQ,QAAQ,QAAQ,SAAS,QAAQ;AACzD,QAAM,YAAiC,UAAU,UAAU;AAG3D,MAAI,aAA8B;AAClC,MAAI,CAAC,SAAS;AACZ,UAAM,SAAS,MAAM,0BAA0B,KAAK;AACpD,UAAM,WAAW,KAAK,CAAC,GAAG,aAAa,YAAY,IAAI,GAAG,GAAG,aAAa,YAAY,MAAM,CAAC,CAAC,EAAE,KAAK;AACrG,iBAAa,OAAO,SAAS,SAAS;AAAA,EACxC;AASA,MAAI,cAA6B;AACjC,MAAI,QAAQ,QAAQ,QAAQ,OAAO;AACjC,kBAAc,UAAU,aAAa;AAAA,EACvC,WAAW,SAAS,QAAQ;AAC1B,kBAAc,UAAU,SAAS,CAAC,CAAC;AAAA,EACrC,WAAW,QAAQ,WAAW,GAAG;AAC/B,kBAAc,UAAU,QAAQ,CAAC,CAAC;AAAA,EACpC,WAAW,QAAQ,SAAS,GAAG;AAC7B,kBAAc,UAAU,KAAK,OAAO,EAAE,KAAK,KAAK,CAAC;AAAA,EACnD,OAAO;AACL,kBAAc,UAAU,aAAa;AAAA,EACvC;AAGA,QAAM,aAAa,CAAC,UAChB,SACC,MAAM;AACL,UAAM,WAAW,YAAY,iBAAiB,gBAAgB,CAAC,GAAG,KAAK,CAAC,MAAW,EAAE,QAAQ,IAAI;AACjG,WAAO,SAAS,oBAAoB,CAAC,GAAG,SAAS;AAAA,EACnD,GAAG;AACP,QAAM,EAAE,WAAW,KAAK,IAAI,MAAM,oBAAoB,UAAU;AAEhE,SAAO;AAAA,IACL;AAAA,IACA,WAAW,UAAU,aAAa;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAIA,eAAe,kBAAkB,MAA6C;AAC5E,QAAM,IAAI,KAAK,QAAQ,MAAM,EAAE;AAC/B,QAAM,SAAS,wBAAwB,CAAC,8BAA8B,CAAC;AACvE,QAAM,MAAM,GAAG,OAAO,WAAW,mBAAmB,MAAM,EAAE,QAAQ,QAAQ,GAAG,CAAC;AAChF,QAAM,OAAO,MAAM,QAAa,KAAK,UAAU;AAC/C,QAAM,MAAM,MAAM,UAAU,CAAC,GAAG;AAChC,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK;AAC3C,QAAM,WAAW,IAAI,gBAAgB,CAAC,GAAG,CAAC,KAAK;AAC/C,MAAI,CAAC,SAAS,CAAC,QAAS,QAAO;AAE/B,QAAM,UAAU,MAAM,YAAY,MAAM,KAAK,YAAY;AACzD,SAAO;AAAA,IACL,WAAW,UAAU,UAAU;AAAA,IAC/B,WAAW,UAAU,IAAI;AAAA,IACzB,aAAa,UAAU,UAAU,OAAO,IAAI;AAAA,IAC5C,YAAY,UAAU,OAAO,KAAa,IAAI,cAAc,CAAC,CAAC,EAAE,KAAK;AAAA,IACrE,YAAY,IAAI,mBAAmB,CAAC,GAAG,CAAC,MAAM,IAAI,mBAAmB,CAAC,GAAG,CAAC,KAAK;AAAA,IAC/E,MAAM,CAAC;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,YAAY;AAAA,EACd;AACF;AAQA,eAAsB,YAAY,MAA6C;AAC7E,MAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAG,QAAO;AAClC,QAAM,QAAQ,KAAK,KAAK;AACxB,QAAM,QAAQ,MAAM,iBAAiB,KAAK,EAAE,MAAM,MAAM,IAAI;AAC5D,MAAI,UAAU,MAAM,eAAgB,MAAM,cAAc,MAAM,WAAW,SAAU;AACjF,WAAO;AAAA,EACT;AACA,SAAO,MAAM,kBAAkB,KAAK,EAAE,MAAM,MAAM,IAAI;AACxD;;;ACnLA,cAAyB;AAIzB,IAAM,cAAc;AACpB,IAAM,WAAW;AA6BjB,IAAM,iBAAoF;AAAA,EACxF,EAAE,OAAO,iBAAiB,MAAM,WAAW,OAAO,gBAAgB;AAAA,EAClE,EAAE,OAAO,yBAAyB,MAAM,WAAW,OAAO,wBAAwB;AAAA,EAClF,EAAE,OAAO,6BAA6B,MAAM,WAAW,OAAO,4BAA4B;AAAA,EAC1F,EAAE,OAAO,8BAA8B,MAAM,WAAW,OAAO,6BAA6B;AAAA,EAC5F,EAAE,OAAO,qBAAqB,MAAM,WAAW,OAAO,oBAAoB;AAAA,EAC1E,EAAE,OAAO,yBAAyB,MAAM,WAAW,OAAO,2BAA2B;AAAA,EACrF,EAAE,OAAO,YAAY,MAAM,WAAW,OAAO,WAAW;AAAA,EACxD,EAAE,OAAO,qBAAqB,MAAM,WAAW,OAAO,oBAAoB;AAAA,EAC1E,EAAE,OAAO,qBAAqB,MAAM,WAAW,OAAO,oBAAoB;AAAA,EAC1E,EAAE,OAAO,+BAA+B,MAAM,WAAW,OAAO,8BAA8B;AAAA,EAC9F,EAAE,OAAO,cAAc,MAAM,WAAW,OAAO,aAAa;AAAA,EAC5D,EAAE,OAAO,yBAAyB,MAAM,WAAW,OAAO,wBAAwB;AAAA,EAClF,EAAE,OAAO,eAAe,MAAM,WAAW,OAAO,cAAc;AAAA,EAC9D,EAAE,OAAO,gBAAgB,MAAM,WAAW,OAAO,eAAe;AAClE;AAIA,SAAS,MAAM,GAAiB,GAAmB;AACjD,QAAM,KAAK,EAAE,SAAS,gBAAgB,CAAC,GAAG,KAAK,IAAI,EAAE,YAAY;AACjE,MAAI,IAAI;AACR,MAAI,KAAK,EAAE,MAAM,SAAS,EAAE,WAAW,EAAG,MAAK;AAC/C,MAAI,EAAE,SAAS,EAAE,YAAY,CAAC,EAAG,MAAK;AACtC,SAAO;AACT;AAGA,eAAe,iBAAiB,MAA4C;AAC1E,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAI,KAAK,KAAK,EAAE,QAAQ,MAAM,EAAE;AACtC,QAAM,SAAS,0BAA0B,CAAC,4BAA4B,CAAC;AACvE,QAAM,MAAM,GAAG,WAAW,WAAW,mBAAmB,MAAM,EAAE,QAAQ,QAAQ,GAAG,CAAC;AACpF,QAAM,OAAO,MAAM,QAAyB,GAAG;AAC/C,QAAM,UAAU,MAAM,WAAW,CAAC;AAClC,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,QAAQ,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;AACpE;AAEA,SAAS,OAAO,GAAiB,cAAsB;AACrD,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,UAAU,EAAE,SAAS,eAAe,CAAC,KAAK;AAChD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,QAAQ,GAAG,KAAK,KAAK,OAAO,MAAM;AAAA,IAC/C,aAAa,QACT,4DAA4D,KAAK,KACjE;AAAA,EACN;AACF;AAEA,SAAS,gBAAgB,OAAuD,WAA8B;AAC5G,SAAO,UAAU,KAAK,CAAC,QAAQ;AAC7B,UAAM,IAAI,IAAI,YAAY,EAAE,KAAK;AACjC,WACE,MAAM,SAAS,OACf,MAAM,MAAM,YAAY,EAAE,SAAS,CAAC,KACpC,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,EAAE,SAAS,CAAC,KACjD,EAAE,SAAS,MAAM,MAAM,YAAY,CAAC;AAAA,EAExC,CAAC;AACH;AAMA,eAAsB,WAAW,MAAc,WAAqD;AAClG,QAAM,IAAI,MAAM,iBAAiB,IAAI;AACrC,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,OAAO,OAAO,GAAG,KAAK,KAAK,CAAC;AAElC,QAAM,WAA2B,CAAC;AAClC,aAAW,SAAS,gBAAgB;AAClC,QAAI,WAAW,UAAU,CAAC,gBAAgB,OAAO,SAAS,EAAG;AAC7D,UAAM,UAAU,aAAa,EAAE,MAAM,KAAK,CAAC;AAC3C,QAAI,QAAS,UAAS,KAAK,EAAE,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO,QAAQ,CAAC;AAAA,EAC9E;AACA,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,SAAO;AAAA,IACL,UAAU,KAAK;AAAA,IACf,OAAO,KAAK;AAAA,IACZ,eAAe,cAAc,EAAE,cAAc;AAAA,IAC7C;AAAA,IACA,aAAa,KAAK;AAAA,EACpB;AACF;AAGA,eAAsB,QAAQ,MAA2C;AACvE,QAAM,IAAI,MAAM,iBAAiB,IAAI;AACrC,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,OAAO,OAAO,GAAG,KAAK,KAAK,CAAC;AAElC,QAAM,SAAS,aAAa,EAAE,yBAAyB;AACvD,QAAM,QAAQ,aAAa,EAAE,aAAa;AAC1C,QAAM,OAAO,aAAa,EAAE,yBAAyB,EAAE,QAAQ;AAC/D,QAAM,WAAW,CAAC,QAAQ,wBAAmB,KAAK,KAAK,IAAI,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,MAAM;AAC5F,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,QAAM,cAAc,aAAa,EAAE,qBAAqB;AACxD,QAAM,oBAAoB,aAAa,EAAE,iBAAiB;AAE1D,MAAI,CAAC,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,eAAe,CAAC,kBAAmB,QAAO;AAEnF,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,aAAa,eAAe;AAAA,IAC5B,QAAQ,UAAU;AAAA,IAClB,mBAAmB,qBAAqB;AAAA,IACxC,UAAU,YAAY;AAAA,IACtB,SAAS,WAAW;AAAA,IACpB,SAAS,cAAc,EAAE,cAAc;AAAA,IACvC,WAAW,KAAK;AAAA,IAChB,aAAa;AAAA,EACf;AACF;AASA,eAAe,aAAa,MAAmC;AAC7D,QAAM,MAAM,GAAG,QAAQ,eAAe,IAAI,gBAAgB,EAAE,GAAG,KAAK,CAAC,CAAC;AACtE,QAAM,MAAM,MAAM,WAAW,GAAG;AAChC,MAAI,CAAC,IAAI,GAAI,QAAO,CAAC;AACrB,QAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,MAAkB,CAAC;AACzB,IAAE,0BAA0B,EAAE,KAAK,CAAC,GAAG,OAAO;AAC5C,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,MAAM,KAAK;AACnC,UAAM,IAAI,KAAK,MAAM,6BAA6B;AAClD,QAAI,CAAC,EAAG;AACR,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC/B,QAAI,CAAC,QAAQ,KAAK,YAAY,EAAE,SAAS,qBAAqB,EAAG;AACjE,QAAI,IAAI,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC,EAAG;AAC5C,QAAI,KAAK,EAAE,YAAY,EAAE,CAAC,GAAG,KAAK,CAAC;AAAA,EACrC,CAAC;AACD,SAAO;AACT;AAEA,eAAe,cAAc,MAAwC;AACnE,QAAM,UAAU,MAAM,aAAa,IAAI;AACvC,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,IAAI,KAAK,YAAY;AAC3B,UAAQ;AAAA,IACN,CAAC,GAAG,OACD,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI;AAAA,EACzF;AACA,SAAO,QAAQ,CAAC;AAClB;AAEA,IAAM,qBAA6C;AAAA,EACjD,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACT;AAEA,SAAS,WAAW,MAAsB;AACxC,QAAM,IAAY,aAAK,IAAI;AAC3B,IAAE,IAAI,EAAE,YAAY,IAAI;AACxB,IAAE,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO;AACtB,MAAE,EAAE,EAAE,QAAQ,IAAI;AAClB,MAAE,EAAE,EAAE,OAAO,IAAI;AAAA,EACnB,CAAC;AACD,IAAE,oCAAoC,EAAE,KAAK,CAAC,GAAG,OAAO;AACtD,MAAE,EAAE,EAAE,OAAO,IAAI;AAAA,EACnB,CAAC;AACD,SAAO,EAAE,KAAK,EAAE,QAAQ,WAAW,MAAM,EAAE,KAAK;AAClD;AAEA,SAAS,kBAAkB,MAAc,OAAe,WAA8B;AACpF,SAAO,UAAU,KAAK,CAAC,QAAQ;AAC7B,UAAM,IAAI,IAAI,YAAY,EAAE,KAAK;AACjC,QAAI,SAAS,OAAO,SAAS,EAAG,QAAO;AACvC,QAAI,MAAM,YAAY,EAAE,SAAS,CAAC,EAAG,QAAO;AAC5C,QAAI,EAAE,SAAS,YAAY,EAAG,QAAO,SAAS;AAC9C,QAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,QAAQ,KAAK,EAAE,SAAS,UAAU,EAAG,QAAO,SAAS;AAC5F,QAAI,EAAE,SAAS,kBAAkB,EAAG,QAAO,SAAS;AACpD,QAAI,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,YAAY,EAAG,QAAO,SAAS;AACvE,QAAI,EAAE,SAAS,aAAa,EAAG,QAAO,SAAS;AAC/C,QAAI,EAAE,SAAS,WAAW,KAAK,EAAE,SAAS,WAAW,EAAG,QAAO,SAAS;AACxE,QAAI,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,aAAa,KAAK,EAAE,SAAS,aAAa,EAAG,QAAO,SAAS;AACrG,QAAI,EAAE,SAAS,UAAU,KAAK,EAAE,SAAS,YAAY,EAAG,QAAO,SAAS;AACxE,QAAI,EAAE,SAAS,iBAAiB,EAAG,QAAO,SAAS;AACnD,QAAI,EAAE,SAAS,iBAAiB,EAAG,QAAO,SAAS;AACnD,WAAO;AAAA,EACT,CAAC;AACH;AAOA,SAAS,kBAAkB,MAAc,WAAsC;AAC7E,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,kBAAwE,CAAC;AAE/E,IAAE,+DAA+D,EAAE,KAAK,CAAC,GAAG,OAAO;AACjF,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC/B,UAAM,QAAQ,KAAK,MAAM,qBAAqB;AAC9C,QAAI,MAAO,iBAAgB,KAAK,EAAE,MAAM,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,SAAS,EAAE,EAAE,EAAE,CAAC;AAAA,EAC5F,CAAC;AAED,MAAI,gBAAgB,WAAW,GAAG;AAChC,MAAE,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO;AACrB,YAAM,MAAM,EAAE,EAAE;AAChB,UAAI,IAAI,SAAS,EAAE,SAAS,KAAK,CAAC,IAAI,GAAG,2BAA2B,EAAG;AACvE,YAAM,OAAO,IAAI,KAAK,EAAE,KAAK;AAC7B,YAAM,QAAQ,KAAK,MAAM,qBAAqB;AAC9C,UAAI,SAAS,mBAAmB,MAAM,CAAC,CAAC,GAAG;AACzC,wBAAgB,KAAK,EAAE,MAAM,MAAM,CAAC,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,SAAS,IAAI,CAAC;AAAA,MAC/E;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,MAAsB,CAAC;AAC7B,WAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;AAC/C,UAAM,UAAU,gBAAgB,CAAC;AACjC,UAAM,OAAO,gBAAgB,IAAI,CAAC;AAElC,QAAI,UAAU;AACd,QAAI,OAAO,QAAQ,QAAQ,KAAK;AAChC,WAAO,KAAK,SAAS,GAAG;AACtB,UAAI,QAAQ,KAAK,GAAG,KAAK,OAAO,EAAG;AACnC,YAAM,WAAW,EAAE,KAAK,IAAI;AAC5B,UAAI,SAAU,YAAW,WAAW,QAAQ,IAAI;AAChD,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,QAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,YAAM,aAAa,EAAE,KAAK,QAAQ,QAAQ,OAAO,CAAC;AAClD,UAAI,WAAY,WAAU,WAAW,UAAU,EAAE,QAAQ,QAAQ,QAAQ,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK;AAAA,IACnG;AAEA,UAAM,UAAU,aAAa,OAAO;AACpC,QAAI,QAAS,KAAI,KAAK,EAAE,MAAM,QAAQ,MAAM,OAAO,QAAQ,OAAO,SAAS,QAAQ,CAAC;AAAA,EACtF;AAEA,MAAI,WAAW,QAAQ;AACrB,WAAO,IAAI,OAAO,CAAC,MAAM,kBAAkB,EAAE,MAAM,EAAE,OAAO,SAAS,CAAC;AAAA,EACxE;AACA,SAAO;AACT;AAMA,eAAsB,UAAU,MAAc,WAAoD;AAChG,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,OAAO,MAAM,cAAc,IAAI;AACrC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,MAAM,MAAM,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU,OAAO;AAC9E,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,WAAW,kBAAkB,MAAM,IAAI,KAAK,GAAG,SAAS;AAC9D,QAAI,SAAS,WAAW,EAAG,QAAO;AAClC,WAAO;AAAA,MACL,UAAU,KAAK;AAAA,MACf,WAAW,KAAK;AAAA,MAChB;AAAA,MACA,KAAK,GAAG,QAAQ,gBAAgB,KAAK,UAAU;AAAA,IACjD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,QAAQ,MAA2C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,OAAO,MAAM,cAAc,IAAI;AACrC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,MAAM,MAAM,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU,OAAO;AAC9E,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,MAAM,kBAAkB,MAAM,IAAI,KAAK,CAAC;AAC9C,UAAM,OAAO,CAAC,SAAiB,IAAI,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,WAAW;AAE5E,UAAM,cAAc,KAAK,KAAK;AAC9B,UAAM,SAAS,KAAK,KAAK;AACzB,UAAM,oBAAoB,KAAK,KAAK;AACpC,UAAM,WAAW,KAAK,KAAK;AAC3B,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,eAAe,CAAC,kBAAmB,QAAO;AAEnF,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,aAAa,KAAK;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU;AAAA,MACrD,aAAa;AAAA,IACf;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,UAAU,MAAoC;AAClE,QAAM,CAAC,IAAI,EAAE,IAAI,MAAM,QAAQ,IAAI;AAAA,IACjC,QAAQ,IAAI,EAAE,MAAM,MAAM,IAAI;AAAA,IAC9B,QAAQ,IAAI,EAAE,MAAM,MAAM,IAAI;AAAA,EAChC,CAAC;AACD,SAAO,EAAE,IAAI,GAAG;AAClB;;;ACxXO,IAAM,cAA2D;AAAA,EACtE,EAAE,OAAO,eAAe,OAAO,cAAc;AAAA,EAC7C,EAAE,OAAO,UAAU,OAAO,SAAS;AAAA,EACnC,EAAE,OAAO,qBAAqB,OAAO,oBAAoB;AAAA,EACzD,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,qBAAqB,OAAO,UAAU;AACjD;AAEA,eAAsB,cAAc,MAAsC;AACxE,QAAM,EAAE,IAAI,GAAG,IAAI,MAAM,UAAU,IAAI;AAEvC,QAAM,cAAiC,YAAY,IAAI,CAAC,EAAE,OAAO,MAAM,OAAO;AAAA,IAC5E;AAAA,IACA,IAAI,KAAK,GAAG,KAAK,IAAI;AAAA,IACrB,IAAI,KAAK,GAAG,KAAK,IAAI;AAAA,EACvB,EAAE,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;AAE9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,IAAI,aAAa;AAAA,IAC3B,UAAU,IAAI,aAAa;AAAA,EAC7B;AACF;","names":[]}
package/dist/index.d.cts CHANGED
@@ -18,6 +18,27 @@ interface LabelResult {
18
18
  us: RegionLabel | null;
19
19
  uk: RegionLabel | null;
20
20
  }
21
+ /** One label section in general (all-section) access. */
22
+ interface LabelSection {
23
+ code: string;
24
+ title: string;
25
+ content: string;
26
+ }
27
+ /** General US label result (openFDA), all available sections. */
28
+ interface UsLabelResult {
29
+ drugName: string;
30
+ setId: string;
31
+ publishedDate: string;
32
+ sections: LabelSection[];
33
+ dailymedUrl: string;
34
+ }
35
+ /** General UK/EU SmPC result (eMC), all available sections. */
36
+ interface UkSmpcResult {
37
+ drugName: string;
38
+ productId: string;
39
+ sections: LabelSection[];
40
+ url: string;
41
+ }
21
42
  /** Deterministic brand <-> generic resolution result. */
22
43
  interface ResolveResult {
23
44
  inputType: "brand" | "generic";
@@ -61,13 +82,31 @@ declare function resolveName(name: string): Promise<ResolveResult | null>;
61
82
  * - US: FDA label via the openFDA API -> DailyMed citation
62
83
  * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation
63
84
  *
64
- * Every network call is best-effort: any failure returns null so callers never
65
- * break. Pure fetch + parse caching belongs to the consumer.
85
+ * Two layers:
86
+ * - getUsLabel / getUkSmpc general, all-section access (LabelSection[]),
87
+ * used by PubCrawl's get_uspi / get_smpc / compare_labels tools.
88
+ * - getUspi / getSmpc / getLabels — a 5-field convenience view (RegionLabel)
89
+ * used by Pharmonym. Built on the same fetch + parse internals.
90
+ *
91
+ * Every network call is best-effort: failures return null. Pure fetch + parse;
92
+ * caching belongs to the consumer.
66
93
  */
67
94
 
95
+ /**
96
+ * General US label access — returns every available section (or the requested
97
+ * subset) as LabelSection[]. Powers PubCrawl's get_uspi.
98
+ */
99
+ declare function getUsLabel(drug: string, requested?: string[]): Promise<UsLabelResult | null>;
100
+ /** 5-field convenience view of the US label (Pharmonym). */
68
101
  declare function getUspi(drug: string): Promise<RegionLabel | null>;
102
+ /**
103
+ * General UK/EU SmPC access — returns every available section (or the requested
104
+ * subset) as LabelSection[]. Powers PubCrawl's get_smpc.
105
+ */
106
+ declare function getUkSmpc(drug: string, requested?: string[]): Promise<UkSmpcResult | null>;
107
+ /** 5-field convenience view of the UK/EU SmPC (Pharmonym). */
69
108
  declare function getSmpc(drug: string): Promise<RegionLabel | null>;
70
- /** Look up both labels for a drug. Each side is null if unavailable. Never throws. */
109
+ /** Look up both 5-field labels for a drug. Each side is null if unavailable. */
71
110
  declare function getLabels(drug: string): Promise<LabelResult>;
72
111
 
73
112
  /**
@@ -89,4 +128,4 @@ declare function compareLabels(drug: string): Promise<CompareResult>;
89
128
  declare function cleanSection(value: unknown, maxChars?: number): string;
90
129
  declare function formatFdaDate(yyyymmdd?: string): string;
91
130
 
92
- export { type CompareResult, type LabelComparison, type LabelResult, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUspi, resolveName };
131
+ export { type CompareResult, type LabelComparison, type LabelResult, type LabelSection, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, type UkSmpcResult, type UsLabelResult, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUkSmpc, getUsLabel, getUspi, resolveName };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,27 @@ interface LabelResult {
18
18
  us: RegionLabel | null;
19
19
  uk: RegionLabel | null;
20
20
  }
21
+ /** One label section in general (all-section) access. */
22
+ interface LabelSection {
23
+ code: string;
24
+ title: string;
25
+ content: string;
26
+ }
27
+ /** General US label result (openFDA), all available sections. */
28
+ interface UsLabelResult {
29
+ drugName: string;
30
+ setId: string;
31
+ publishedDate: string;
32
+ sections: LabelSection[];
33
+ dailymedUrl: string;
34
+ }
35
+ /** General UK/EU SmPC result (eMC), all available sections. */
36
+ interface UkSmpcResult {
37
+ drugName: string;
38
+ productId: string;
39
+ sections: LabelSection[];
40
+ url: string;
41
+ }
21
42
  /** Deterministic brand <-> generic resolution result. */
22
43
  interface ResolveResult {
23
44
  inputType: "brand" | "generic";
@@ -61,13 +82,31 @@ declare function resolveName(name: string): Promise<ResolveResult | null>;
61
82
  * - US: FDA label via the openFDA API -> DailyMed citation
62
83
  * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation
63
84
  *
64
- * Every network call is best-effort: any failure returns null so callers never
65
- * break. Pure fetch + parse caching belongs to the consumer.
85
+ * Two layers:
86
+ * - getUsLabel / getUkSmpc general, all-section access (LabelSection[]),
87
+ * used by PubCrawl's get_uspi / get_smpc / compare_labels tools.
88
+ * - getUspi / getSmpc / getLabels — a 5-field convenience view (RegionLabel)
89
+ * used by Pharmonym. Built on the same fetch + parse internals.
90
+ *
91
+ * Every network call is best-effort: failures return null. Pure fetch + parse;
92
+ * caching belongs to the consumer.
66
93
  */
67
94
 
95
+ /**
96
+ * General US label access — returns every available section (or the requested
97
+ * subset) as LabelSection[]. Powers PubCrawl's get_uspi.
98
+ */
99
+ declare function getUsLabel(drug: string, requested?: string[]): Promise<UsLabelResult | null>;
100
+ /** 5-field convenience view of the US label (Pharmonym). */
68
101
  declare function getUspi(drug: string): Promise<RegionLabel | null>;
102
+ /**
103
+ * General UK/EU SmPC access — returns every available section (or the requested
104
+ * subset) as LabelSection[]. Powers PubCrawl's get_smpc.
105
+ */
106
+ declare function getUkSmpc(drug: string, requested?: string[]): Promise<UkSmpcResult | null>;
107
+ /** 5-field convenience view of the UK/EU SmPC (Pharmonym). */
69
108
  declare function getSmpc(drug: string): Promise<RegionLabel | null>;
70
- /** Look up both labels for a drug. Each side is null if unavailable. Never throws. */
109
+ /** Look up both 5-field labels for a drug. Each side is null if unavailable. */
71
110
  declare function getLabels(drug: string): Promise<LabelResult>;
72
111
 
73
112
  /**
@@ -89,4 +128,4 @@ declare function compareLabels(drug: string): Promise<CompareResult>;
89
128
  declare function cleanSection(value: unknown, maxChars?: number): string;
90
129
  declare function formatFdaDate(yyyymmdd?: string): string;
91
130
 
92
- export { type CompareResult, type LabelComparison, type LabelResult, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUspi, resolveName };
131
+ export { type CompareResult, type LabelComparison, type LabelResult, type LabelSection, type Region, type RegionLabel, type ResolveResult, SECTION_MAP, type UkSmpcResult, type UsLabelResult, cleanSection, compareLabels, formatFdaDate, getLabels, getSmpc, getUkSmpc, getUsLabel, getUspi, resolveName };
package/dist/index.js CHANGED
@@ -123,9 +123,17 @@ async function resolveViaRxNorm(name) {
123
123
  brandNames = single.length ? single : fallback;
124
124
  }
125
125
  let genericName = null;
126
- if (minNames.length) genericName = titleCase(minNames[0]);
127
- else if (inNames.length === 1) genericName = titleCase(inNames[0]);
128
- else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(" / "));
126
+ if (tty === "IN" || tty === "MIN") {
127
+ genericName = titleCase(canonicalName);
128
+ } else if (minNames.length) {
129
+ genericName = titleCase(minNames[0]);
130
+ } else if (inNames.length === 1) {
131
+ genericName = titleCase(inNames[0]);
132
+ } else if (inNames.length > 1) {
133
+ genericName = titleCase(uniq(inNames).join(" / "));
134
+ } else {
135
+ genericName = titleCase(canonicalName);
136
+ }
129
137
  const classRxcui = !isBrand ? rxcui : (() => {
130
138
  const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x) => x.tty === "IN");
131
139
  return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;
@@ -180,7 +188,30 @@ async function resolveName(name) {
180
188
  import * as cheerio from "cheerio";
181
189
  var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
182
190
  var EMC_BASE = "https://www.medicines.org.uk";
183
- async function getUspi(drug) {
191
+ var US_SECTION_MAP = [
192
+ { field: "boxed_warning", code: "34066-1", title: "Boxed Warning" },
193
+ { field: "indications_and_usage", code: "34067-9", title: "Indications and Usage" },
194
+ { field: "dosage_and_administration", code: "34068-7", title: "Dosage and Administration" },
195
+ { field: "dosage_forms_and_strengths", code: "43678-2", title: "Dosage Forms and Strengths" },
196
+ { field: "contraindications", code: "34070-3", title: "Contraindications" },
197
+ { field: "warnings_and_cautions", code: "43685-7", title: "Warnings and Precautions" },
198
+ { field: "warnings", code: "34071-1", title: "Warnings" },
199
+ { field: "adverse_reactions", code: "34084-4", title: "Adverse Reactions" },
200
+ { field: "drug_interactions", code: "34073-7", title: "Drug Interactions" },
201
+ { field: "use_in_specific_populations", code: "42228-7", title: "Use in Specific Populations" },
202
+ { field: "overdosage", code: "34088-5", title: "Overdosage" },
203
+ { field: "clinical_pharmacology", code: "34090-1", title: "Clinical Pharmacology" },
204
+ { field: "description", code: "34089-3", title: "Description" },
205
+ { field: "how_supplied", code: "34069-5", title: "How Supplied" }
206
+ ];
207
+ function score(r, q) {
208
+ const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
209
+ let s = 0;
210
+ if (g && g.split(/,| and /).length === 1) s += 2;
211
+ if (g.includes(q.toLowerCase())) s += 1;
212
+ return s;
213
+ }
214
+ async function fetchOpenFdaBest(drug) {
184
215
  if (!drug) return null;
185
216
  const q = drug.trim().replace(/"/g, "");
186
217
  const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
@@ -188,8 +219,49 @@ async function getUspi(drug) {
188
219
  const data = await getJson(url);
189
220
  const results = data?.results ?? [];
190
221
  if (results.length === 0) return null;
191
- const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));
192
- const r = ranked[0];
222
+ return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];
223
+ }
224
+ function usMeta(r, fallbackName) {
225
+ const setId = r.openfda?.spl_set_id?.[0] ?? "";
226
+ const brand = r.openfda?.brand_name?.[0] ?? "";
227
+ const generic = r.openfda?.generic_name?.[0] ?? fallbackName;
228
+ return {
229
+ setId,
230
+ brand,
231
+ generic,
232
+ productName: brand ? `${brand} (${generic})` : generic,
233
+ dailymedUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/"
234
+ };
235
+ }
236
+ function matchesUsFilter(entry, requested) {
237
+ return requested.some((req) => {
238
+ const r = req.toLowerCase().trim();
239
+ return entry.code === req || entry.title.toLowerCase().includes(r) || String(entry.field).replace(/_/g, " ").includes(r) || r.includes(entry.title.toLowerCase());
240
+ });
241
+ }
242
+ async function getUsLabel(drug, requested) {
243
+ const r = await fetchOpenFdaBest(drug);
244
+ if (!r) return null;
245
+ const meta = usMeta(r, drug.trim());
246
+ const sections = [];
247
+ for (const entry of US_SECTION_MAP) {
248
+ if (requested?.length && !matchesUsFilter(entry, requested)) continue;
249
+ const content = cleanSection(r[entry.field]);
250
+ if (content) sections.push({ code: entry.code, title: entry.title, content });
251
+ }
252
+ if (sections.length === 0) return null;
253
+ return {
254
+ drugName: meta.productName,
255
+ setId: meta.setId,
256
+ publishedDate: formatFdaDate(r.effective_time),
257
+ sections,
258
+ dailymedUrl: meta.dailymedUrl
259
+ };
260
+ }
261
+ async function getUspi(drug) {
262
+ const r = await fetchOpenFdaBest(drug);
263
+ if (!r) return null;
264
+ const meta = usMeta(r, drug.trim());
193
265
  const dosage = cleanSection(r.dosage_and_administration);
194
266
  const boxed = cleanSection(r.boxed_warning);
195
267
  const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
@@ -198,31 +270,21 @@ async function getUspi(drug) {
198
270
  const indications = cleanSection(r.indications_and_usage);
199
271
  const contraindications = cleanSection(r.contraindications);
200
272
  if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
201
- const setId = r.openfda?.spl_set_id?.[0] ?? "";
202
- const brand = r.openfda?.brand_name?.[0] ?? "";
203
- const generic = r.openfda?.generic_name?.[0] ?? q;
204
273
  return {
205
274
  region: "US",
206
275
  authority: "FDA Prescribing Information",
207
- productName: brand ? `${brand} (${generic})` : generic,
276
+ productName: meta.productName,
208
277
  indications: indications || null,
209
278
  dosage: dosage || null,
210
279
  contraindications: contraindications || null,
211
280
  warnings: warnings || null,
212
281
  adverse: adverse || null,
213
282
  updated: formatFdaDate(r.effective_time),
214
- sourceUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/",
283
+ sourceUrl: meta.dailymedUrl,
215
284
  sourceLabel: "DailyMed (U.S. National Library of Medicine)"
216
285
  };
217
286
  }
218
- function score(r, q) {
219
- const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
220
- let s = 0;
221
- if (g && g.split(/,| and /).length === 1) s += 2;
222
- if (g.includes(q.toLowerCase())) s += 1;
223
- return s;
224
- }
225
- async function searchEmc(drug) {
287
+ async function searchEmcAll(drug) {
226
288
  const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
227
289
  const res = await timedFetch(url);
228
290
  if (!res.ok) return [];
@@ -240,13 +302,15 @@ async function searchEmc(drug) {
240
302
  });
241
303
  return out;
242
304
  }
243
- var SMPC_WANT = {
244
- "4.1": "indications",
245
- "4.2": "dosage",
246
- "4.3": "contraindications",
247
- "4.4": "warnings",
248
- "4.8": "adverse"
249
- };
305
+ async function searchEmcBest(drug) {
306
+ const matches = await searchEmcAll(drug);
307
+ if (matches.length === 0) return null;
308
+ const q = drug.toLowerCase();
309
+ matches.sort(
310
+ (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
311
+ );
312
+ return matches[0];
313
+ }
250
314
  var SMPC_SECTION_NAMES = {
251
315
  "4.1": "Therapeutic indications",
252
316
  "4.2": "Posology and method of administration",
@@ -259,7 +323,13 @@ var SMPC_SECTION_NAMES = {
259
323
  "4.9": "Overdose",
260
324
  "5.1": "Pharmacodynamic properties",
261
325
  "5.2": "Pharmacokinetic properties",
262
- "5.3": "Preclinical safety data"
326
+ "5.3": "Preclinical safety data",
327
+ "6.1": "List of excipients",
328
+ "6.2": "Incompatibilities",
329
+ "6.3": "Shelf life",
330
+ "6.4": "Special precautions for storage",
331
+ "6.5": "Nature and contents of container",
332
+ "6.6": "Special precautions for disposal"
263
333
  };
264
334
  function htmlToText(html) {
265
335
  const $ = cheerio.load(html);
@@ -273,7 +343,25 @@ function htmlToText(html) {
273
343
  });
274
344
  return $.text().replace(/\n{3,}/g, "\n\n").trim();
275
345
  }
276
- function parseSmpcSections(html) {
346
+ function matchesSmpcFilter(code, title, requested) {
347
+ return requested.some((req) => {
348
+ const r = req.toLowerCase().trim();
349
+ if (code === req || code === r) return true;
350
+ if (title.toLowerCase().includes(r)) return true;
351
+ if (r.includes("indication")) return code === "4.1";
352
+ if (r.includes("dosage") || r.includes("dosing") || r.includes("posology")) return code === "4.2";
353
+ if (r.includes("contraindication")) return code === "4.3";
354
+ if (r.includes("warning") || r.includes("precaution")) return code === "4.4";
355
+ if (r.includes("interaction")) return code === "4.5";
356
+ if (r.includes("pregnancy") || r.includes("fertility")) return code === "4.6";
357
+ if (r.includes("adverse") || r.includes("undesirable") || r.includes("side effect")) return code === "4.8";
358
+ if (r.includes("overdose") || r.includes("overdosage")) return code === "4.9";
359
+ if (r.includes("pharmacodynamic")) return code === "5.1";
360
+ if (r.includes("pharmacokinetic")) return code === "5.2";
361
+ return false;
362
+ });
363
+ }
364
+ function parseSmpcSections(html, requested) {
277
365
  const $ = cheerio.load(html);
278
366
  const sectionElements = [];
279
367
  $("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
@@ -292,11 +380,9 @@ function parseSmpcSections(html) {
292
380
  }
293
381
  });
294
382
  }
295
- const fields = {};
383
+ const out = [];
296
384
  for (let i = 0; i < sectionElements.length; i++) {
297
385
  const current = sectionElements[i];
298
- const field = SMPC_WANT[current.code];
299
- if (!field) continue;
300
386
  const next = sectionElements[i + 1];
301
387
  let content = "";
302
388
  let node = current.element.next();
@@ -308,39 +394,59 @@ function parseSmpcSections(html) {
308
394
  }
309
395
  if (!content.trim()) {
310
396
  const parentHtml = $.html(current.element.parent());
311
- if (parentHtml) {
312
- content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
313
- }
397
+ if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
314
398
  }
315
- fields[field] = cleanSection(content);
399
+ const cleaned = cleanSection(content);
400
+ if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });
401
+ }
402
+ if (requested?.length) {
403
+ return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));
404
+ }
405
+ return out;
406
+ }
407
+ async function getUkSmpc(drug, requested) {
408
+ if (!drug) return null;
409
+ try {
410
+ const best = await searchEmcBest(drug);
411
+ if (!best) return null;
412
+ const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
413
+ if (!res.ok) return null;
414
+ const sections = parseSmpcSections(await res.text(), requested);
415
+ if (sections.length === 0) return null;
416
+ return {
417
+ drugName: best.name,
418
+ productId: best.product_id,
419
+ sections,
420
+ url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`
421
+ };
422
+ } catch {
423
+ return null;
316
424
  }
317
- return fields;
318
425
  }
319
426
  async function getSmpc(drug) {
320
427
  if (!drug) return null;
321
428
  try {
322
- const matches = await searchEmc(drug);
323
- if (matches.length === 0) return null;
324
- const q = drug.toLowerCase();
325
- matches.sort(
326
- (a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
327
- );
328
- const best = matches[0];
429
+ const best = await searchEmcBest(drug);
430
+ if (!best) return null;
329
431
  const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
330
432
  if (!res.ok) return null;
331
- const html = await res.text();
332
- const sec = parseSmpcSections(html);
333
- if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)
334
- return null;
433
+ const all = parseSmpcSections(await res.text());
434
+ const pick = (code) => all.find((s) => s.code === code)?.content ?? null;
435
+ const indications = pick("4.1");
436
+ const dosage = pick("4.2");
437
+ const contraindications = pick("4.3");
438
+ const warnings = pick("4.4");
439
+ const adverse = pick("4.8");
440
+ if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
335
441
  return {
336
442
  region: "UK/EU",
337
443
  authority: "Summary of Product Characteristics (SmPC)",
338
444
  productName: best.name,
339
- indications: sec.indications || null,
340
- dosage: sec.dosage || null,
341
- contraindications: sec.contraindications || null,
342
- warnings: sec.warnings || null,
343
- adverse: sec.adverse || null,
445
+ indications,
446
+ dosage,
447
+ contraindications,
448
+ warnings,
449
+ adverse,
344
450
  updated: "",
345
451
  sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
346
452
  sourceLabel: "electronic medicines compendium (emc), UK"
@@ -386,6 +492,8 @@ export {
386
492
  formatFdaDate,
387
493
  getLabels,
388
494
  getSmpc,
495
+ getUkSmpc,
496
+ getUsLabel,
389
497
  getUspi,
390
498
  resolveName
391
499
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic display name: prefer the canonical combo string, else single, else joined.\n let genericName: string | null = null;\n if (minNames.length) genericName = titleCase(minNames[0]);\n else if (inNames.length === 1) genericName = titleCase(inNames[0]);\n else if (inNames.length > 1) genericName = titleCase(uniq(inNames).join(\" / \"));\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Every network call is best-effort: any failure returns null so callers never\n * break. Pure fetch + parse — caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n dosage_and_administration?: string[];\n boxed_warning?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n indications_and_usage?: string[];\n contraindications?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n/* --------------------------- US: openFDA --------------------------- */\n\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n\n // Prefer a single-ingredient product over combination labels.\n const ranked = results.slice().sort((a, b) => score(b, q) - score(a, q));\n const r = ranked[0];\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? q;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: brand ? `${brand} (${generic})` : generic,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmc(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\n// SmPC section number -> the field we store it under\nconst SMPC_WANT: Record<string, keyof Pick<RegionLabel, \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\">> = {\n \"4.1\": \"indications\",\n \"4.2\": \"dosage\",\n \"4.3\": \"contraindications\",\n \"4.4\": \"warnings\",\n \"4.8\": \"adverse\",\n};\n\n// Canonical SmPC section titles — used as a fallback heading detector\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\n/**\n * Parse the wanted SmPC sections from eMC HTML. Detect numbered headings, then\n * collect sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string): Record<string, string> {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n // Strategy 1: headings whose text starts with a section number\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n // Strategy 2: fall back to scanning leaf nodes for known section headers\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const fields: Record<string, string> = {};\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const field = SMPC_WANT[current.code];\n if (!field) continue;\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) {\n content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n }\n\n fields[field] = cleanSection(content);\n }\n return fields;\n}\n\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const matches = await searchEmc(drug);\n if (matches.length === 0) return null;\n\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n const best = matches[0];\n\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const html = await res.text();\n const sec = parseSmpcSections(html);\n\n if (!sec.dosage && !sec.warnings && !sec.adverse && !sec.indications && !sec.contraindications)\n return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications: sec.indications || null,\n dosage: sec.dosage || null,\n contraindications: sec.contraindications || null,\n warnings: sec.warnings || null,\n adverse: sec.adverse || null,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both labels for a drug. Each side is null if unavailable. Never throws. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fetch.ts","../src/names.ts","../src/labels.ts","../src/compare.ts"],"sourcesContent":["/**\n * Shared HTTP + text helpers. Uses the runtime's global `fetch` (Node >=18),\n * so the package has no node-fetch dependency.\n */\n\nconst DEFAULT_TIMEOUT_MS = 12000;\nconst MAX_FIELD_CHARS = 1400;\n\nexport const USER_AGENT = \"drug-data/0.1 (pharmatools.ai; nick@pharmatools.ai)\";\n\nexport async function timedFetch(\n url: string,\n opts: RequestInit = {},\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n ...opts,\n signal: controller.signal,\n headers: { \"User-Agent\": USER_AGENT, ...((opts.headers as Record<string, string>) || {}) },\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function getJson<T = unknown>(\n url: string,\n timeoutMs = DEFAULT_TIMEOUT_MS\n): Promise<T | null> {\n try {\n const res = await timedFetch(url, {}, timeoutMs);\n if (!res.ok) return null;\n return (await res.json()) as T;\n } catch {\n return null;\n }\n}\n\n/**\n * Tidy a raw label section: join arrays, strip a leading numbered ALL-CAPS\n * heading, collapse whitespace, and truncate on a sentence boundary.\n */\nexport function cleanSection(value: unknown, maxChars = MAX_FIELD_CHARS): string {\n if (!value) return \"\";\n let text = Array.isArray(value) ? value.join(\"\\n\\n\") : String(value);\n\n text = text\n .replace(/\\r/g, \"\")\n .replace(/[ \\t]+/g, \" \")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n\n // Drop a leading section header line like \"5 WARNINGS AND PRECAUTIONS\"\n text = text.replace(/^\\s*\\d+(\\.\\d+)*\\s+[A-Z][A-Z \\-/,&]+\\n+/, \"\").trim();\n\n if (text.length > maxChars) {\n const slice = text.slice(0, maxChars);\n const lastStop = Math.max(slice.lastIndexOf(\". \"), slice.lastIndexOf(\"\\n\"));\n text = (lastStop > maxChars * 0.6 ? slice.slice(0, lastStop + 1) : slice).trim() + \" …\";\n }\n return text;\n}\n\nexport function formatFdaDate(yyyymmdd?: string): string {\n if (!yyyymmdd || !/^\\d{8}$/.test(yyyymmdd)) return \"\";\n const d = new Date(`${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`);\n if (isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(\"en-US\", { year: \"numeric\", month: \"short\", day: \"numeric\" });\n}\n\nexport function titleCase(s: string): string {\n return String(s || \"\").replace(/\\w\\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());\n}\n\nexport function uniq<T>(arr: T[]): T[] {\n return [...new Set(arr.filter(Boolean))];\n}\n","/**\n * Deterministic brand <-> generic resolution via RxNorm/RxNav, with an openFDA\n * fallback. No AI, no hallucinated names. Brand names are US-registered;\n * international brand names are intentionally not invented.\n */\n\nimport { getJson, titleCase, uniq } from \"./fetch.js\";\nimport type { ResolveResult } from \"./types.js\";\n\nconst RXNAV = \"https://rxnav.nlm.nih.gov/REST\";\nconst OPENFDA = \"https://api.fda.gov/drug/label.json\";\nconst TIMEOUT_MS = 10000;\n\nconst REGION_NOTE = \"Brand names shown are those registered in the United States.\";\n\n/* ----------------------- RxNorm resolution ----------------------- */\n\nasync function findRxcui(name: string): Promise<string | null> {\n const exact = await getJson<any>(`${RXNAV}/rxcui.json?name=${encodeURIComponent(name)}`, TIMEOUT_MS);\n const id = exact?.idGroup?.rxnormId?.[0];\n if (id) return id;\n\n const approx = await getJson<any>(\n `${RXNAV}/approximateTerm.json?term=${encodeURIComponent(name)}&maxEntries=1`,\n TIMEOUT_MS\n );\n const cand = approx?.approximateGroup?.candidate?.[0];\n return cand ? cand.rxcui : null;\n}\n\nfunction collectNames(allRelated: any, tty: string): string[] {\n const groups = allRelated?.allRelatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === tty);\n if (!g || !g.conceptProperties) return [];\n return g.conceptProperties.map((c: any) => c.name);\n}\n\n/**\n * Single-ingredient brand names for a generic ingredient, via SBDF concepts\n * whose name is \"<ingredient> <dose form> [Brand]\". Combination products read\n * \"<ing A> / <ing B> ... [Brand]\", so any \"/\" before the bracket is excluded.\n */\nasync function getSingleIngredientBrands(ingredientRxcui: string): Promise<string[]> {\n const data = await getJson<any>(`${RXNAV}/rxcui/${ingredientRxcui}/related.json?tty=SBDF`, TIMEOUT_MS);\n const groups = data?.relatedGroup?.conceptGroup ?? [];\n const g = groups.find((x: any) => x.tty === \"SBDF\");\n if (!g || !g.conceptProperties) return [];\n const brands: string[] = [];\n for (const c of g.conceptProperties) {\n const m = (c.name as string).match(/\\[([^\\]]+)\\]\\s*$/);\n if (!m) continue;\n const prefix = (c.name as string).slice(0, (c.name as string).lastIndexOf(\"[\"));\n if (prefix.includes(\"/\")) continue; // combination product — skip\n brands.push(m[1].trim());\n }\n return uniq(brands).sort();\n}\n\nasync function getDrugClassAndUses(\n rxcui: string\n): Promise<{ drugClass: string | null; uses: string[] }> {\n const data = await getJson<any>(\n `${RXNAV}/rxclass/class/byRxcui.json?rxcui=${rxcui}&relaSource=MEDRT`,\n TIMEOUT_MS\n );\n const items = data?.rxclassDrugInfoList?.rxclassDrugInfo ?? [];\n\n const byType = (t: string): string | null => {\n const hit = items.find((i: any) => i.rxclassMinConceptItem.classType === t);\n return hit ? hit.rxclassMinConceptItem.className : null;\n };\n // Prefer FDA Established Pharmacologic Class, then mechanism, then chemical.\n const drugClass = byType(\"EPC\") || byType(\"MOA\") || byType(\"CHEM\");\n\n const uses = uniq<string>(\n items\n .filter((i: any) => i.rela === \"may_treat\")\n .map((i: any) => i.rxclassMinConceptItem.className as string)\n ).slice(0, 6);\n\n return { drugClass, uses };\n}\n\nasync function resolveViaRxNorm(name: string): Promise<ResolveResult | null> {\n const rxcui = await findRxcui(name);\n if (!rxcui) return null;\n\n const [props, allRelated] = await Promise.all([\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/properties.json`, TIMEOUT_MS),\n getJson<any>(`${RXNAV}/rxcui/${rxcui}/allrelated.json`, TIMEOUT_MS),\n ]);\n\n const tty: string | undefined = props?.properties?.tty;\n const canonicalName: string = props?.properties?.name ?? name;\n if (!tty) return null;\n\n const inNames = collectNames(allRelated, \"IN\");\n const minNames = collectNames(allRelated, \"MIN\");\n\n const isBrand = tty === \"BN\" || tty === \"SBD\" || tty === \"BPCK\";\n const inputType: \"brand\" | \"generic\" = isBrand ? \"brand\" : \"generic\";\n\n // For a generic, list only brands where it is the sole active ingredient.\n let brandNames: string[] | null = null;\n if (!isBrand) {\n const single = await getSingleIngredientBrands(rxcui);\n const fallback = uniq([...collectNames(allRelated, \"BN\"), ...collectNames(allRelated, \"BPCK\")]).sort();\n brandNames = single.length ? single : fallback;\n }\n\n // Generic name must reflect the concept's OWN ingredient(s). A single-ingredient\n // lookup (e.g. \"atorvastatin\", tty IN) relates to every combination it appears\n // in (Atorvastatin / Ezetimibe, ...), so the previous MIN-first logic\n // mis-resolved single generics to arbitrary combinations and broke downstream\n // label lookups. When the input concept is itself an ingredient (IN) or a\n // multi-ingredient concept (MIN), its own canonical name is authoritative;\n // only for brands/products do we read the related MIN as the combo generic.\n let genericName: string | null = null;\n if (tty === \"IN\" || tty === \"MIN\") {\n genericName = titleCase(canonicalName);\n } else if (minNames.length) {\n genericName = titleCase(minNames[0]);\n } else if (inNames.length === 1) {\n genericName = titleCase(inNames[0]);\n } else if (inNames.length > 1) {\n genericName = titleCase(uniq(inNames).join(\" / \"));\n } else {\n genericName = titleCase(canonicalName);\n }\n\n // Class + indications from the ingredient rxcui where possible.\n const classRxcui = !isBrand\n ? rxcui\n : (() => {\n const inGroup = (allRelated?.allRelatedGroup?.conceptGroup ?? []).find((x: any) => x.tty === \"IN\");\n return inGroup?.conceptProperties?.[0]?.rxcui ?? rxcui;\n })();\n const { drugClass, uses } = await getDrugClassAndUses(classRxcui);\n\n return {\n inputType,\n inputName: titleCase(canonicalName),\n genericName,\n brandNames,\n drugClass,\n uses,\n rxcui,\n source: \"RxNorm (NLM)\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* ----------------------- openFDA fallback ------------------------ */\n\nasync function resolveViaOpenFda(name: string): Promise<ResolveResult | null> {\n const q = name.replace(/\"/g, \"\");\n const search = `(openfda.brand_name:\"${q}\"+OR+openfda.generic_name:\"${q}\")`;\n const url = `${OPENFDA}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&limit=1`;\n const data = await getJson<any>(url, TIMEOUT_MS);\n const fda = data?.results?.[0]?.openfda;\n if (!fda) return null;\n\n const brand = (fda.brand_name ?? [])[0] ?? \"\";\n const generic = (fda.generic_name ?? [])[0] ?? \"\";\n if (!brand && !generic) return null;\n\n const isBrand = brand.toLowerCase() === name.toLowerCase();\n return {\n inputType: isBrand ? \"brand\" : \"generic\",\n inputName: titleCase(name),\n genericName: generic ? titleCase(generic) : null,\n brandNames: isBrand ? null : uniq<string>(fda.brand_name ?? []).sort(),\n drugClass: (fda.pharm_class_epc ?? [])[0] ?? (fda.pharm_class_moa ?? [])[0] ?? null,\n uses: [],\n rxcui: null,\n source: \"openFDA\",\n regionNote: REGION_NOTE,\n };\n}\n\n/* --------------------------- public ----------------------------- */\n\n/**\n * Resolve a drug name deterministically. Tries RxNorm, then openFDA. Returns\n * null if neither source recognises the name.\n */\nexport async function resolveName(name: string): Promise<ResolveResult | null> {\n if (!name || !name.trim()) return null;\n const clean = name.trim();\n const viaRx = await resolveViaRxNorm(clean).catch(() => null);\n if (viaRx && (viaRx.genericName || (viaRx.brandNames && viaRx.brandNames.length))) {\n return viaRx;\n }\n return await resolveViaOpenFda(clean).catch(() => null);\n}\n","/**\n * Official drug-label lookups.\n * - US: FDA label via the openFDA API -> DailyMed citation\n * - UK/EU: SmPC via the eMC (medicines.org.uk) -> eMC citation\n *\n * Two layers:\n * - getUsLabel / getUkSmpc — general, all-section access (LabelSection[]),\n * used by PubCrawl's get_uspi / get_smpc / compare_labels tools.\n * - getUspi / getSmpc / getLabels — a 5-field convenience view (RegionLabel)\n * used by Pharmonym. Built on the same fetch + parse internals.\n *\n * Every network call is best-effort: failures return null. Pure fetch + parse;\n * caching belongs to the consumer.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { timedFetch, getJson, cleanSection, formatFdaDate } from \"./fetch.js\";\nimport type { RegionLabel, LabelResult, LabelSection, UsLabelResult, UkSmpcResult } from \"./types.js\";\n\nconst OPENFDA_URL = \"https://api.fda.gov/drug/label.json\";\nconst EMC_BASE = \"https://www.medicines.org.uk\";\n\ninterface OpenFdaLabel {\n effective_time?: string;\n boxed_warning?: string[];\n indications_and_usage?: string[];\n dosage_and_administration?: string[];\n dosage_forms_and_strengths?: string[];\n contraindications?: string[];\n warnings_and_cautions?: string[];\n warnings?: string[];\n adverse_reactions?: string[];\n drug_interactions?: string[];\n use_in_specific_populations?: string[];\n overdosage?: string[];\n clinical_pharmacology?: string[];\n description?: string[];\n how_supplied?: string[];\n openfda?: {\n generic_name?: string[];\n brand_name?: string[];\n spl_set_id?: string[];\n };\n}\ninterface OpenFdaResponse {\n results?: OpenFdaLabel[];\n}\n\n// openFDA field -> { LOINC code, display title }. Order = label reading order.\nconst US_SECTION_MAP: Array<{ field: keyof OpenFdaLabel; code: string; title: string }> = [\n { field: \"boxed_warning\", code: \"34066-1\", title: \"Boxed Warning\" },\n { field: \"indications_and_usage\", code: \"34067-9\", title: \"Indications and Usage\" },\n { field: \"dosage_and_administration\", code: \"34068-7\", title: \"Dosage and Administration\" },\n { field: \"dosage_forms_and_strengths\", code: \"43678-2\", title: \"Dosage Forms and Strengths\" },\n { field: \"contraindications\", code: \"34070-3\", title: \"Contraindications\" },\n { field: \"warnings_and_cautions\", code: \"43685-7\", title: \"Warnings and Precautions\" },\n { field: \"warnings\", code: \"34071-1\", title: \"Warnings\" },\n { field: \"adverse_reactions\", code: \"34084-4\", title: \"Adverse Reactions\" },\n { field: \"drug_interactions\", code: \"34073-7\", title: \"Drug Interactions\" },\n { field: \"use_in_specific_populations\", code: \"42228-7\", title: \"Use in Specific Populations\" },\n { field: \"overdosage\", code: \"34088-5\", title: \"Overdosage\" },\n { field: \"clinical_pharmacology\", code: \"34090-1\", title: \"Clinical Pharmacology\" },\n { field: \"description\", code: \"34089-3\", title: \"Description\" },\n { field: \"how_supplied\", code: \"34069-5\", title: \"How Supplied\" },\n];\n\n/* --------------------------- US: openFDA --------------------------- */\n\nfunction score(r: OpenFdaLabel, q: string): number {\n const g = (r.openfda?.generic_name ?? []).join(\", \").toLowerCase();\n let s = 0;\n if (g && g.split(/,| and /).length === 1) s += 2; // single ingredient\n if (g.includes(q.toLowerCase())) s += 1;\n return s;\n}\n\n/** Fetch the most recent FDA label for a drug; prefers single-ingredient products. */\nasync function fetchOpenFdaBest(drug: string): Promise<OpenFdaLabel | null> {\n if (!drug) return null;\n const q = drug.trim().replace(/\"/g, \"\");\n const search = `(openfda.generic_name:\"${q}\"+OR+openfda.brand_name:\"${q}\")`;\n const url = `${OPENFDA_URL}?search=${encodeURIComponent(search).replace(/%2B/g, \"+\")}&sort=effective_time:desc&limit=5`;\n const data = await getJson<OpenFdaResponse>(url);\n const results = data?.results ?? [];\n if (results.length === 0) return null;\n return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];\n}\n\nfunction usMeta(r: OpenFdaLabel, fallbackName: string) {\n const setId = r.openfda?.spl_set_id?.[0] ?? \"\";\n const brand = r.openfda?.brand_name?.[0] ?? \"\";\n const generic = r.openfda?.generic_name?.[0] ?? fallbackName;\n return {\n setId,\n brand,\n generic,\n productName: brand ? `${brand} (${generic})` : generic,\n dailymedUrl: setId\n ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}`\n : \"https://dailymed.nlm.nih.gov/dailymed/\",\n };\n}\n\nfunction matchesUsFilter(entry: { field: string; code: string; title: string }, requested: string[]): boolean {\n return requested.some((req) => {\n const r = req.toLowerCase().trim();\n return (\n entry.code === req ||\n entry.title.toLowerCase().includes(r) ||\n String(entry.field).replace(/_/g, \" \").includes(r) ||\n r.includes(entry.title.toLowerCase())\n );\n });\n}\n\n/**\n * General US label access — returns every available section (or the requested\n * subset) as LabelSection[]. Powers PubCrawl's get_uspi.\n */\nexport async function getUsLabel(drug: string, requested?: string[]): Promise<UsLabelResult | null> {\n const r = await fetchOpenFdaBest(drug);\n if (!r) return null;\n const meta = usMeta(r, drug.trim());\n\n const sections: LabelSection[] = [];\n for (const entry of US_SECTION_MAP) {\n if (requested?.length && !matchesUsFilter(entry, requested)) continue;\n const content = cleanSection(r[entry.field]);\n if (content) sections.push({ code: entry.code, title: entry.title, content });\n }\n if (sections.length === 0) return null;\n\n return {\n drugName: meta.productName,\n setId: meta.setId,\n publishedDate: formatFdaDate(r.effective_time),\n sections,\n dailymedUrl: meta.dailymedUrl,\n };\n}\n\n/** 5-field convenience view of the US label (Pharmonym). */\nexport async function getUspi(drug: string): Promise<RegionLabel | null> {\n const r = await fetchOpenFdaBest(drug);\n if (!r) return null;\n const meta = usMeta(r, drug.trim());\n\n const dosage = cleanSection(r.dosage_and_administration);\n const boxed = cleanSection(r.boxed_warning);\n const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);\n const warnings = [boxed ? `BOXED WARNING — ${boxed}` : \"\", warn].filter(Boolean).join(\"\\n\\n\");\n const adverse = cleanSection(r.adverse_reactions);\n const indications = cleanSection(r.indications_and_usage);\n const contraindications = cleanSection(r.contraindications);\n\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n return {\n region: \"US\",\n authority: \"FDA Prescribing Information\",\n productName: meta.productName,\n indications: indications || null,\n dosage: dosage || null,\n contraindications: contraindications || null,\n warnings: warnings || null,\n adverse: adverse || null,\n updated: formatFdaDate(r.effective_time),\n sourceUrl: meta.dailymedUrl,\n sourceLabel: \"DailyMed (U.S. National Library of Medicine)\",\n };\n}\n\n/* --------------------------- UK/EU: eMC ---------------------------- */\n\ninterface EmcMatch {\n product_id: string;\n name: string;\n}\n\nasync function searchEmcAll(drug: string): Promise<EmcMatch[]> {\n const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;\n const res = await timedFetch(url);\n if (!res.ok) return [];\n const html = await res.text();\n const $ = cheerio.load(html);\n const out: EmcMatch[] = [];\n $(\"a[href*='/emc/product/']\").each((_, el) => {\n const href = $(el).attr(\"href\") || \"\";\n const m = href.match(/\\/emc\\/product\\/(\\d+)\\/smpc/);\n if (!m) return;\n const name = $(el).text().trim();\n if (!name || name.toLowerCase().includes(\"health professional\")) return;\n if (out.some((x) => x.product_id === m[1])) return;\n out.push({ product_id: m[1], name });\n });\n return out;\n}\n\nasync function searchEmcBest(drug: string): Promise<EmcMatch | null> {\n const matches = await searchEmcAll(drug);\n if (matches.length === 0) return null;\n const q = drug.toLowerCase();\n matches.sort(\n (a, b) =>\n (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)\n );\n return matches[0];\n}\n\nconst SMPC_SECTION_NAMES: Record<string, string> = {\n \"4.1\": \"Therapeutic indications\",\n \"4.2\": \"Posology and method of administration\",\n \"4.3\": \"Contraindications\",\n \"4.4\": \"Special warnings and precautions for use\",\n \"4.5\": \"Interaction with other medicinal products\",\n \"4.6\": \"Fertility, pregnancy and lactation\",\n \"4.7\": \"Effects on ability to drive and use machines\",\n \"4.8\": \"Undesirable effects\",\n \"4.9\": \"Overdose\",\n \"5.1\": \"Pharmacodynamic properties\",\n \"5.2\": \"Pharmacokinetic properties\",\n \"5.3\": \"Preclinical safety data\",\n \"6.1\": \"List of excipients\",\n \"6.2\": \"Incompatibilities\",\n \"6.3\": \"Shelf life\",\n \"6.4\": \"Special precautions for storage\",\n \"6.5\": \"Nature and contents of container\",\n \"6.6\": \"Special precautions for disposal\",\n};\n\nfunction htmlToText(html: string): string {\n const $ = cheerio.load(html);\n $(\"br\").replaceWith(\"\\n\");\n $(\"li\").each((_, el) => {\n $(el).prepend(\"- \");\n $(el).append(\"\\n\");\n });\n $(\"p, div, tr, h1, h2, h3, h4, h5, h6\").each((_, el) => {\n $(el).append(\"\\n\");\n });\n return $.text().replace(/\\n{3,}/g, \"\\n\\n\").trim();\n}\n\nfunction matchesSmpcFilter(code: string, title: string, requested: string[]): boolean {\n return requested.some((req) => {\n const r = req.toLowerCase().trim();\n if (code === req || code === r) return true;\n if (title.toLowerCase().includes(r)) return true;\n if (r.includes(\"indication\")) return code === \"4.1\";\n if (r.includes(\"dosage\") || r.includes(\"dosing\") || r.includes(\"posology\")) return code === \"4.2\";\n if (r.includes(\"contraindication\")) return code === \"4.3\";\n if (r.includes(\"warning\") || r.includes(\"precaution\")) return code === \"4.4\";\n if (r.includes(\"interaction\")) return code === \"4.5\";\n if (r.includes(\"pregnancy\") || r.includes(\"fertility\")) return code === \"4.6\";\n if (r.includes(\"adverse\") || r.includes(\"undesirable\") || r.includes(\"side effect\")) return code === \"4.8\";\n if (r.includes(\"overdose\") || r.includes(\"overdosage\")) return code === \"4.9\";\n if (r.includes(\"pharmacodynamic\")) return code === \"5.1\";\n if (r.includes(\"pharmacokinetic\")) return code === \"5.2\";\n return false;\n });\n}\n\n/**\n * Parse SmPC sections from eMC HTML. Returns every detected numbered section as\n * LabelSection[] (or the requested subset). Detect numbered headings, collect\n * sibling content up to the next heading, with a parent-text fallback.\n */\nfunction parseSmpcSections(html: string, requested?: string[]): LabelSection[] {\n const $ = cheerio.load(html);\n const sectionElements: Array<{ code: string; title: string; element: any }> = [];\n\n $(\"[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4\").each((_, el) => {\n const text = $(el).text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match) sectionElements.push({ code: match[1], title: match[2].trim(), element: $(el) });\n });\n\n if (sectionElements.length === 0) {\n $(\"*\").each((_, el) => {\n const $el = $(el);\n if ($el.children().length > 0 && !$el.is(\"a, span, strong, em, b, i\")) return;\n const text = $el.text().trim();\n const match = text.match(/^(\\d+\\.?\\d*)\\s+(.+)/);\n if (match && SMPC_SECTION_NAMES[match[1]]) {\n sectionElements.push({ code: match[1], title: match[2].trim(), element: $el });\n }\n });\n }\n\n const out: LabelSection[] = [];\n for (let i = 0; i < sectionElements.length; i++) {\n const current = sectionElements[i];\n const next = sectionElements[i + 1];\n\n let content = \"\";\n let node = current.element.next();\n while (node.length > 0) {\n if (next && node.is(next.element)) break;\n const nodeHtml = $.html(node);\n if (nodeHtml) content += htmlToText(nodeHtml) + \"\\n\";\n node = node.next();\n }\n if (!content.trim()) {\n const parentHtml = $.html(current.element.parent());\n if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), \"\").trim();\n }\n\n const cleaned = cleanSection(content);\n if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });\n }\n\n if (requested?.length) {\n return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));\n }\n return out;\n}\n\n/**\n * General UK/EU SmPC access — returns every available section (or the requested\n * subset) as LabelSection[]. Powers PubCrawl's get_smpc.\n */\nexport async function getUkSmpc(drug: string, requested?: string[]): Promise<UkSmpcResult | null> {\n if (!drug) return null;\n try {\n const best = await searchEmcBest(drug);\n if (!best) return null;\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const sections = parseSmpcSections(await res.text(), requested);\n if (sections.length === 0) return null;\n return {\n drugName: best.name,\n productId: best.product_id,\n sections,\n url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n };\n } catch {\n return null;\n }\n}\n\n/** 5-field convenience view of the UK/EU SmPC (Pharmonym). */\nexport async function getSmpc(drug: string): Promise<RegionLabel | null> {\n if (!drug) return null;\n try {\n const best = await searchEmcBest(drug);\n if (!best) return null;\n const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);\n if (!res.ok) return null;\n const all = parseSmpcSections(await res.text());\n const pick = (code: string) => all.find((s) => s.code === code)?.content ?? null;\n\n const indications = pick(\"4.1\");\n const dosage = pick(\"4.2\");\n const contraindications = pick(\"4.3\");\n const warnings = pick(\"4.4\");\n const adverse = pick(\"4.8\");\n if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;\n\n return {\n region: \"UK/EU\",\n authority: \"Summary of Product Characteristics (SmPC)\",\n productName: best.name,\n indications,\n dosage,\n contraindications,\n warnings,\n adverse,\n updated: \"\",\n sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,\n sourceLabel: \"electronic medicines compendium (emc), UK\",\n };\n } catch {\n return null;\n }\n}\n\n/* --------------------------- public API ---------------------------- */\n\n/** Look up both 5-field labels for a drug. Each side is null if unavailable. */\nexport async function getLabels(drug: string): Promise<LabelResult> {\n const [us, uk] = await Promise.all([\n getUspi(drug).catch(() => null),\n getSmpc(drug).catch(() => null),\n ]);\n return { us, uk };\n}\n","/**\n * US (FDA) vs UK/EU (SmPC) side-by-side label comparison across mapped topics.\n */\n\nimport { getLabels } from \"./labels.js\";\nimport type { CompareResult, LabelComparison } from \"./types.js\";\n\ntype LabelField = \"indications\" | \"dosage\" | \"contraindications\" | \"warnings\" | \"adverse\";\n\n/** Topics compared across US and UK labels, mapped to the fields that hold them. */\nexport const SECTION_MAP: Array<{ topic: string; field: LabelField }> = [\n { topic: \"Indications\", field: \"indications\" },\n { topic: \"Dosing\", field: \"dosage\" },\n { topic: \"Contraindications\", field: \"contraindications\" },\n { topic: \"Warnings\", field: \"warnings\" },\n { topic: \"Adverse Reactions\", field: \"adverse\" },\n];\n\nexport async function compareLabels(drug: string): Promise<CompareResult> {\n const { us, uk } = await getLabels(drug);\n\n const comparisons: LabelComparison[] = SECTION_MAP.map(({ topic, field }) => ({\n topic,\n us: us ? us[field] : null,\n uk: uk ? uk[field] : null,\n })).filter((c) => c.us || c.uk);\n\n return {\n drug,\n comparisons,\n usSource: us?.sourceUrl ?? null,\n ukSource: uk?.sourceUrl ?? null,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pharmatools/drug-data",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Deterministic drug-name resolution (RxNorm) and official label data (FDA openFDA + UK eMC SmPC), with US/UK comparison. The shared engine behind Pharmonym and PubCrawl.",
5
5
  "type": "module",
6
6
  "exports": {