@pharmatools/drug-data 0.1.0 → 0.2.0
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 +16 -0
- package/dist/index.cjs +151 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -4
- package/dist/index.d.ts +43 -4
- package/dist/index.js +149 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
});
|
|
@@ -223,7 +225,30 @@ async function resolveName(name) {
|
|
|
223
225
|
var cheerio = __toESM(require("cheerio"), 1);
|
|
224
226
|
var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
|
|
225
227
|
var EMC_BASE = "https://www.medicines.org.uk";
|
|
226
|
-
|
|
228
|
+
var US_SECTION_MAP = [
|
|
229
|
+
{ field: "boxed_warning", code: "34066-1", title: "Boxed Warning" },
|
|
230
|
+
{ field: "indications_and_usage", code: "34067-9", title: "Indications and Usage" },
|
|
231
|
+
{ field: "dosage_and_administration", code: "34068-7", title: "Dosage and Administration" },
|
|
232
|
+
{ field: "dosage_forms_and_strengths", code: "43678-2", title: "Dosage Forms and Strengths" },
|
|
233
|
+
{ field: "contraindications", code: "34070-3", title: "Contraindications" },
|
|
234
|
+
{ field: "warnings_and_cautions", code: "43685-7", title: "Warnings and Precautions" },
|
|
235
|
+
{ field: "warnings", code: "34071-1", title: "Warnings" },
|
|
236
|
+
{ field: "adverse_reactions", code: "34084-4", title: "Adverse Reactions" },
|
|
237
|
+
{ field: "drug_interactions", code: "34073-7", title: "Drug Interactions" },
|
|
238
|
+
{ field: "use_in_specific_populations", code: "42228-7", title: "Use in Specific Populations" },
|
|
239
|
+
{ field: "overdosage", code: "34088-5", title: "Overdosage" },
|
|
240
|
+
{ field: "clinical_pharmacology", code: "34090-1", title: "Clinical Pharmacology" },
|
|
241
|
+
{ field: "description", code: "34089-3", title: "Description" },
|
|
242
|
+
{ field: "how_supplied", code: "34069-5", title: "How Supplied" }
|
|
243
|
+
];
|
|
244
|
+
function score(r, q) {
|
|
245
|
+
const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
|
|
246
|
+
let s = 0;
|
|
247
|
+
if (g && g.split(/,| and /).length === 1) s += 2;
|
|
248
|
+
if (g.includes(q.toLowerCase())) s += 1;
|
|
249
|
+
return s;
|
|
250
|
+
}
|
|
251
|
+
async function fetchOpenFdaBest(drug) {
|
|
227
252
|
if (!drug) return null;
|
|
228
253
|
const q = drug.trim().replace(/"/g, "");
|
|
229
254
|
const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
|
|
@@ -231,8 +256,49 @@ async function getUspi(drug) {
|
|
|
231
256
|
const data = await getJson(url);
|
|
232
257
|
const results = data?.results ?? [];
|
|
233
258
|
if (results.length === 0) return null;
|
|
234
|
-
|
|
235
|
-
|
|
259
|
+
return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];
|
|
260
|
+
}
|
|
261
|
+
function usMeta(r, fallbackName) {
|
|
262
|
+
const setId = r.openfda?.spl_set_id?.[0] ?? "";
|
|
263
|
+
const brand = r.openfda?.brand_name?.[0] ?? "";
|
|
264
|
+
const generic = r.openfda?.generic_name?.[0] ?? fallbackName;
|
|
265
|
+
return {
|
|
266
|
+
setId,
|
|
267
|
+
brand,
|
|
268
|
+
generic,
|
|
269
|
+
productName: brand ? `${brand} (${generic})` : generic,
|
|
270
|
+
dailymedUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/"
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function matchesUsFilter(entry, requested) {
|
|
274
|
+
return requested.some((req) => {
|
|
275
|
+
const r = req.toLowerCase().trim();
|
|
276
|
+
return entry.code === req || entry.title.toLowerCase().includes(r) || String(entry.field).replace(/_/g, " ").includes(r) || r.includes(entry.title.toLowerCase());
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async function getUsLabel(drug, requested) {
|
|
280
|
+
const r = await fetchOpenFdaBest(drug);
|
|
281
|
+
if (!r) return null;
|
|
282
|
+
const meta = usMeta(r, drug.trim());
|
|
283
|
+
const sections = [];
|
|
284
|
+
for (const entry of US_SECTION_MAP) {
|
|
285
|
+
if (requested?.length && !matchesUsFilter(entry, requested)) continue;
|
|
286
|
+
const content = cleanSection(r[entry.field]);
|
|
287
|
+
if (content) sections.push({ code: entry.code, title: entry.title, content });
|
|
288
|
+
}
|
|
289
|
+
if (sections.length === 0) return null;
|
|
290
|
+
return {
|
|
291
|
+
drugName: meta.productName,
|
|
292
|
+
setId: meta.setId,
|
|
293
|
+
publishedDate: formatFdaDate(r.effective_time),
|
|
294
|
+
sections,
|
|
295
|
+
dailymedUrl: meta.dailymedUrl
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function getUspi(drug) {
|
|
299
|
+
const r = await fetchOpenFdaBest(drug);
|
|
300
|
+
if (!r) return null;
|
|
301
|
+
const meta = usMeta(r, drug.trim());
|
|
236
302
|
const dosage = cleanSection(r.dosage_and_administration);
|
|
237
303
|
const boxed = cleanSection(r.boxed_warning);
|
|
238
304
|
const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
|
|
@@ -241,31 +307,21 @@ async function getUspi(drug) {
|
|
|
241
307
|
const indications = cleanSection(r.indications_and_usage);
|
|
242
308
|
const contraindications = cleanSection(r.contraindications);
|
|
243
309
|
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
310
|
return {
|
|
248
311
|
region: "US",
|
|
249
312
|
authority: "FDA Prescribing Information",
|
|
250
|
-
productName:
|
|
313
|
+
productName: meta.productName,
|
|
251
314
|
indications: indications || null,
|
|
252
315
|
dosage: dosage || null,
|
|
253
316
|
contraindications: contraindications || null,
|
|
254
317
|
warnings: warnings || null,
|
|
255
318
|
adverse: adverse || null,
|
|
256
319
|
updated: formatFdaDate(r.effective_time),
|
|
257
|
-
sourceUrl:
|
|
320
|
+
sourceUrl: meta.dailymedUrl,
|
|
258
321
|
sourceLabel: "DailyMed (U.S. National Library of Medicine)"
|
|
259
322
|
};
|
|
260
323
|
}
|
|
261
|
-
function
|
|
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) {
|
|
324
|
+
async function searchEmcAll(drug) {
|
|
269
325
|
const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
|
|
270
326
|
const res = await timedFetch(url);
|
|
271
327
|
if (!res.ok) return [];
|
|
@@ -283,13 +339,15 @@ async function searchEmc(drug) {
|
|
|
283
339
|
});
|
|
284
340
|
return out;
|
|
285
341
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
342
|
+
async function searchEmcBest(drug) {
|
|
343
|
+
const matches = await searchEmcAll(drug);
|
|
344
|
+
if (matches.length === 0) return null;
|
|
345
|
+
const q = drug.toLowerCase();
|
|
346
|
+
matches.sort(
|
|
347
|
+
(a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
|
|
348
|
+
);
|
|
349
|
+
return matches[0];
|
|
350
|
+
}
|
|
293
351
|
var SMPC_SECTION_NAMES = {
|
|
294
352
|
"4.1": "Therapeutic indications",
|
|
295
353
|
"4.2": "Posology and method of administration",
|
|
@@ -302,7 +360,13 @@ var SMPC_SECTION_NAMES = {
|
|
|
302
360
|
"4.9": "Overdose",
|
|
303
361
|
"5.1": "Pharmacodynamic properties",
|
|
304
362
|
"5.2": "Pharmacokinetic properties",
|
|
305
|
-
"5.3": "Preclinical safety data"
|
|
363
|
+
"5.3": "Preclinical safety data",
|
|
364
|
+
"6.1": "List of excipients",
|
|
365
|
+
"6.2": "Incompatibilities",
|
|
366
|
+
"6.3": "Shelf life",
|
|
367
|
+
"6.4": "Special precautions for storage",
|
|
368
|
+
"6.5": "Nature and contents of container",
|
|
369
|
+
"6.6": "Special precautions for disposal"
|
|
306
370
|
};
|
|
307
371
|
function htmlToText(html) {
|
|
308
372
|
const $ = cheerio.load(html);
|
|
@@ -316,7 +380,25 @@ function htmlToText(html) {
|
|
|
316
380
|
});
|
|
317
381
|
return $.text().replace(/\n{3,}/g, "\n\n").trim();
|
|
318
382
|
}
|
|
319
|
-
function
|
|
383
|
+
function matchesSmpcFilter(code, title, requested) {
|
|
384
|
+
return requested.some((req) => {
|
|
385
|
+
const r = req.toLowerCase().trim();
|
|
386
|
+
if (code === req || code === r) return true;
|
|
387
|
+
if (title.toLowerCase().includes(r)) return true;
|
|
388
|
+
if (r.includes("indication")) return code === "4.1";
|
|
389
|
+
if (r.includes("dosage") || r.includes("dosing") || r.includes("posology")) return code === "4.2";
|
|
390
|
+
if (r.includes("contraindication")) return code === "4.3";
|
|
391
|
+
if (r.includes("warning") || r.includes("precaution")) return code === "4.4";
|
|
392
|
+
if (r.includes("interaction")) return code === "4.5";
|
|
393
|
+
if (r.includes("pregnancy") || r.includes("fertility")) return code === "4.6";
|
|
394
|
+
if (r.includes("adverse") || r.includes("undesirable") || r.includes("side effect")) return code === "4.8";
|
|
395
|
+
if (r.includes("overdose") || r.includes("overdosage")) return code === "4.9";
|
|
396
|
+
if (r.includes("pharmacodynamic")) return code === "5.1";
|
|
397
|
+
if (r.includes("pharmacokinetic")) return code === "5.2";
|
|
398
|
+
return false;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function parseSmpcSections(html, requested) {
|
|
320
402
|
const $ = cheerio.load(html);
|
|
321
403
|
const sectionElements = [];
|
|
322
404
|
$("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
|
|
@@ -335,11 +417,9 @@ function parseSmpcSections(html) {
|
|
|
335
417
|
}
|
|
336
418
|
});
|
|
337
419
|
}
|
|
338
|
-
const
|
|
420
|
+
const out = [];
|
|
339
421
|
for (let i = 0; i < sectionElements.length; i++) {
|
|
340
422
|
const current = sectionElements[i];
|
|
341
|
-
const field = SMPC_WANT[current.code];
|
|
342
|
-
if (!field) continue;
|
|
343
423
|
const next = sectionElements[i + 1];
|
|
344
424
|
let content = "";
|
|
345
425
|
let node = current.element.next();
|
|
@@ -351,39 +431,59 @@ function parseSmpcSections(html) {
|
|
|
351
431
|
}
|
|
352
432
|
if (!content.trim()) {
|
|
353
433
|
const parentHtml = $.html(current.element.parent());
|
|
354
|
-
if (parentHtml)
|
|
355
|
-
content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
|
|
356
|
-
}
|
|
434
|
+
if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
|
|
357
435
|
}
|
|
358
|
-
|
|
436
|
+
const cleaned = cleanSection(content);
|
|
437
|
+
if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });
|
|
438
|
+
}
|
|
439
|
+
if (requested?.length) {
|
|
440
|
+
return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));
|
|
441
|
+
}
|
|
442
|
+
return out;
|
|
443
|
+
}
|
|
444
|
+
async function getUkSmpc(drug, requested) {
|
|
445
|
+
if (!drug) return null;
|
|
446
|
+
try {
|
|
447
|
+
const best = await searchEmcBest(drug);
|
|
448
|
+
if (!best) return null;
|
|
449
|
+
const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
|
|
450
|
+
if (!res.ok) return null;
|
|
451
|
+
const sections = parseSmpcSections(await res.text(), requested);
|
|
452
|
+
if (sections.length === 0) return null;
|
|
453
|
+
return {
|
|
454
|
+
drugName: best.name,
|
|
455
|
+
productId: best.product_id,
|
|
456
|
+
sections,
|
|
457
|
+
url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`
|
|
458
|
+
};
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
359
461
|
}
|
|
360
|
-
return fields;
|
|
361
462
|
}
|
|
362
463
|
async function getSmpc(drug) {
|
|
363
464
|
if (!drug) return null;
|
|
364
465
|
try {
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
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];
|
|
466
|
+
const best = await searchEmcBest(drug);
|
|
467
|
+
if (!best) return null;
|
|
372
468
|
const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
|
|
373
469
|
if (!res.ok) return null;
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
470
|
+
const all = parseSmpcSections(await res.text());
|
|
471
|
+
const pick = (code) => all.find((s) => s.code === code)?.content ?? null;
|
|
472
|
+
const indications = pick("4.1");
|
|
473
|
+
const dosage = pick("4.2");
|
|
474
|
+
const contraindications = pick("4.3");
|
|
475
|
+
const warnings = pick("4.4");
|
|
476
|
+
const adverse = pick("4.8");
|
|
477
|
+
if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
|
|
378
478
|
return {
|
|
379
479
|
region: "UK/EU",
|
|
380
480
|
authority: "Summary of Product Characteristics (SmPC)",
|
|
381
481
|
productName: best.name,
|
|
382
|
-
indications
|
|
383
|
-
dosage
|
|
384
|
-
contraindications
|
|
385
|
-
warnings
|
|
386
|
-
adverse
|
|
482
|
+
indications,
|
|
483
|
+
dosage,
|
|
484
|
+
contraindications,
|
|
485
|
+
warnings,
|
|
486
|
+
adverse,
|
|
387
487
|
updated: "",
|
|
388
488
|
sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
|
|
389
489
|
sourceLabel: "electronic medicines compendium (emc), UK"
|
|
@@ -430,6 +530,8 @@ async function compareLabels(drug) {
|
|
|
430
530
|
formatFdaDate,
|
|
431
531
|
getLabels,
|
|
432
532
|
getSmpc,
|
|
533
|
+
getUkSmpc,
|
|
534
|
+
getUsLabel,
|
|
433
535
|
getUspi,
|
|
434
536
|
resolveName
|
|
435
537
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAGA,MAAI,cAA6B;AACjC,MAAI,SAAS,OAAQ,eAAc,UAAU,SAAS,CAAC,CAAC;AAAA,WAC/C,QAAQ,WAAW,EAAG,eAAc,UAAU,QAAQ,CAAC,CAAC;AAAA,WACxD,QAAQ,SAAS,EAAG,eAAc,UAAU,KAAK,OAAO,EAAE,KAAK,KAAK,CAAC;AAG9E,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;;;AC3KA,cAAyB;AAIzB,IAAM,cAAc;AACpB,IAAM,WAAW;AAuBjB,eAAsB,QAAQ,MAA2C;AACvE,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;AAEpF,QAAM,OAAO,MAAM,QAAyB,GAAG;AAC/C,QAAM,UAAU,MAAM,WAAW,CAAC;AAClC,MAAI,QAAQ,WAAW,EAAG,QAAO;AAGjC,QAAM,SAAS,QAAQ,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;AACvE,QAAM,IAAI,OAAO,CAAC;AAElB,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,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,QAAQ,EAAE,SAAS,aAAa,CAAC,KAAK;AAC5C,QAAM,UAAU,EAAE,SAAS,eAAe,CAAC,KAAK;AAEhD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,aAAa,QAAQ,GAAG,KAAK,KAAK,OAAO,MAAM;AAAA,IAC/C,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,QACP,4DAA4D,KAAK,KACjE;AAAA,IACJ,aAAa;AAAA,EACf;AACF;AAEA,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;AASA,eAAe,UAAU,MAAmC;AAC1D,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;AAGA,IAAM,YAA8H;AAAA,EAClI,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACT;AAGA,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;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;AAMA,SAAS,kBAAkB,MAAsC;AAC/D,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,kBAAwE,CAAC;AAG/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;AAGD,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,SAAiC,CAAC;AACxC,WAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;AAC/C,UAAM,UAAU,gBAAgB,CAAC;AACjC,UAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,QAAI,CAAC,MAAO;AACZ,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;AAEA,QAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,YAAM,aAAa,EAAE,KAAK,QAAQ,QAAQ,OAAO,CAAC;AAClD,UAAI,YAAY;AACd,kBAAU,WAAW,UAAU,EAAE,QAAQ,QAAQ,QAAQ,KAAK,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK;AAAA,MACnF;AAAA,IACF;AAEA,WAAO,KAAK,IAAI,aAAa,OAAO;AAAA,EACtC;AACA,SAAO;AACT;AAEA,eAAsB,QAAQ,MAA2C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,UAAU,MAAM,UAAU,IAAI;AACpC,QAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,UAAM,IAAI,KAAK,YAAY;AAC3B,YAAQ;AAAA,MACN,CAAC,GAAG,OACD,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,IAAI,IAAI;AAAA,IACzF;AACA,UAAM,OAAO,QAAQ,CAAC;AAEtB,UAAM,MAAM,MAAM,WAAW,GAAG,QAAQ,gBAAgB,KAAK,UAAU,OAAO;AAC9E,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,MAAM,kBAAkB,IAAI;AAElC,QAAI,CAAC,IAAI,UAAU,CAAC,IAAI,YAAY,CAAC,IAAI,WAAW,CAAC,IAAI,eAAe,CAAC,IAAI;AAC3E,aAAO;AAET,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,aAAa,IAAI,eAAe;AAAA,MAChC,QAAQ,IAAI,UAAU;AAAA,MACtB,mBAAmB,IAAI,qBAAqB;AAAA,MAC5C,UAAU,IAAI,YAAY;AAAA,MAC1B,SAAS,IAAI,WAAW;AAAA,MACxB,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;;;ACxPO,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":[]}
|
|
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 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 * 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/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
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
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
|
@@ -180,7 +180,30 @@ async function resolveName(name) {
|
|
|
180
180
|
import * as cheerio from "cheerio";
|
|
181
181
|
var OPENFDA_URL = "https://api.fda.gov/drug/label.json";
|
|
182
182
|
var EMC_BASE = "https://www.medicines.org.uk";
|
|
183
|
-
|
|
183
|
+
var US_SECTION_MAP = [
|
|
184
|
+
{ field: "boxed_warning", code: "34066-1", title: "Boxed Warning" },
|
|
185
|
+
{ field: "indications_and_usage", code: "34067-9", title: "Indications and Usage" },
|
|
186
|
+
{ field: "dosage_and_administration", code: "34068-7", title: "Dosage and Administration" },
|
|
187
|
+
{ field: "dosage_forms_and_strengths", code: "43678-2", title: "Dosage Forms and Strengths" },
|
|
188
|
+
{ field: "contraindications", code: "34070-3", title: "Contraindications" },
|
|
189
|
+
{ field: "warnings_and_cautions", code: "43685-7", title: "Warnings and Precautions" },
|
|
190
|
+
{ field: "warnings", code: "34071-1", title: "Warnings" },
|
|
191
|
+
{ field: "adverse_reactions", code: "34084-4", title: "Adverse Reactions" },
|
|
192
|
+
{ field: "drug_interactions", code: "34073-7", title: "Drug Interactions" },
|
|
193
|
+
{ field: "use_in_specific_populations", code: "42228-7", title: "Use in Specific Populations" },
|
|
194
|
+
{ field: "overdosage", code: "34088-5", title: "Overdosage" },
|
|
195
|
+
{ field: "clinical_pharmacology", code: "34090-1", title: "Clinical Pharmacology" },
|
|
196
|
+
{ field: "description", code: "34089-3", title: "Description" },
|
|
197
|
+
{ field: "how_supplied", code: "34069-5", title: "How Supplied" }
|
|
198
|
+
];
|
|
199
|
+
function score(r, q) {
|
|
200
|
+
const g = (r.openfda?.generic_name ?? []).join(", ").toLowerCase();
|
|
201
|
+
let s = 0;
|
|
202
|
+
if (g && g.split(/,| and /).length === 1) s += 2;
|
|
203
|
+
if (g.includes(q.toLowerCase())) s += 1;
|
|
204
|
+
return s;
|
|
205
|
+
}
|
|
206
|
+
async function fetchOpenFdaBest(drug) {
|
|
184
207
|
if (!drug) return null;
|
|
185
208
|
const q = drug.trim().replace(/"/g, "");
|
|
186
209
|
const search = `(openfda.generic_name:"${q}"+OR+openfda.brand_name:"${q}")`;
|
|
@@ -188,8 +211,49 @@ async function getUspi(drug) {
|
|
|
188
211
|
const data = await getJson(url);
|
|
189
212
|
const results = data?.results ?? [];
|
|
190
213
|
if (results.length === 0) return null;
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
return results.slice().sort((a, b) => score(b, q) - score(a, q))[0];
|
|
215
|
+
}
|
|
216
|
+
function usMeta(r, fallbackName) {
|
|
217
|
+
const setId = r.openfda?.spl_set_id?.[0] ?? "";
|
|
218
|
+
const brand = r.openfda?.brand_name?.[0] ?? "";
|
|
219
|
+
const generic = r.openfda?.generic_name?.[0] ?? fallbackName;
|
|
220
|
+
return {
|
|
221
|
+
setId,
|
|
222
|
+
brand,
|
|
223
|
+
generic,
|
|
224
|
+
productName: brand ? `${brand} (${generic})` : generic,
|
|
225
|
+
dailymedUrl: setId ? `https://dailymed.nlm.nih.gov/dailymed/drugInfo.cfm?setid=${setId}` : "https://dailymed.nlm.nih.gov/dailymed/"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function matchesUsFilter(entry, requested) {
|
|
229
|
+
return requested.some((req) => {
|
|
230
|
+
const r = req.toLowerCase().trim();
|
|
231
|
+
return entry.code === req || entry.title.toLowerCase().includes(r) || String(entry.field).replace(/_/g, " ").includes(r) || r.includes(entry.title.toLowerCase());
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function getUsLabel(drug, requested) {
|
|
235
|
+
const r = await fetchOpenFdaBest(drug);
|
|
236
|
+
if (!r) return null;
|
|
237
|
+
const meta = usMeta(r, drug.trim());
|
|
238
|
+
const sections = [];
|
|
239
|
+
for (const entry of US_SECTION_MAP) {
|
|
240
|
+
if (requested?.length && !matchesUsFilter(entry, requested)) continue;
|
|
241
|
+
const content = cleanSection(r[entry.field]);
|
|
242
|
+
if (content) sections.push({ code: entry.code, title: entry.title, content });
|
|
243
|
+
}
|
|
244
|
+
if (sections.length === 0) return null;
|
|
245
|
+
return {
|
|
246
|
+
drugName: meta.productName,
|
|
247
|
+
setId: meta.setId,
|
|
248
|
+
publishedDate: formatFdaDate(r.effective_time),
|
|
249
|
+
sections,
|
|
250
|
+
dailymedUrl: meta.dailymedUrl
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function getUspi(drug) {
|
|
254
|
+
const r = await fetchOpenFdaBest(drug);
|
|
255
|
+
if (!r) return null;
|
|
256
|
+
const meta = usMeta(r, drug.trim());
|
|
193
257
|
const dosage = cleanSection(r.dosage_and_administration);
|
|
194
258
|
const boxed = cleanSection(r.boxed_warning);
|
|
195
259
|
const warn = cleanSection(r.warnings_and_cautions ?? r.warnings);
|
|
@@ -198,31 +262,21 @@ async function getUspi(drug) {
|
|
|
198
262
|
const indications = cleanSection(r.indications_and_usage);
|
|
199
263
|
const contraindications = cleanSection(r.contraindications);
|
|
200
264
|
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
265
|
return {
|
|
205
266
|
region: "US",
|
|
206
267
|
authority: "FDA Prescribing Information",
|
|
207
|
-
productName:
|
|
268
|
+
productName: meta.productName,
|
|
208
269
|
indications: indications || null,
|
|
209
270
|
dosage: dosage || null,
|
|
210
271
|
contraindications: contraindications || null,
|
|
211
272
|
warnings: warnings || null,
|
|
212
273
|
adverse: adverse || null,
|
|
213
274
|
updated: formatFdaDate(r.effective_time),
|
|
214
|
-
sourceUrl:
|
|
275
|
+
sourceUrl: meta.dailymedUrl,
|
|
215
276
|
sourceLabel: "DailyMed (U.S. National Library of Medicine)"
|
|
216
277
|
};
|
|
217
278
|
}
|
|
218
|
-
function
|
|
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) {
|
|
279
|
+
async function searchEmcAll(drug) {
|
|
226
280
|
const url = `${EMC_BASE}/emc/search?${new URLSearchParams({ q: drug })}`;
|
|
227
281
|
const res = await timedFetch(url);
|
|
228
282
|
if (!res.ok) return [];
|
|
@@ -240,13 +294,15 @@ async function searchEmc(drug) {
|
|
|
240
294
|
});
|
|
241
295
|
return out;
|
|
242
296
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
297
|
+
async function searchEmcBest(drug) {
|
|
298
|
+
const matches = await searchEmcAll(drug);
|
|
299
|
+
if (matches.length === 0) return null;
|
|
300
|
+
const q = drug.toLowerCase();
|
|
301
|
+
matches.sort(
|
|
302
|
+
(a, b) => (b.name.toLowerCase().includes(q) ? 1 : 0) - (a.name.toLowerCase().includes(q) ? 1 : 0)
|
|
303
|
+
);
|
|
304
|
+
return matches[0];
|
|
305
|
+
}
|
|
250
306
|
var SMPC_SECTION_NAMES = {
|
|
251
307
|
"4.1": "Therapeutic indications",
|
|
252
308
|
"4.2": "Posology and method of administration",
|
|
@@ -259,7 +315,13 @@ var SMPC_SECTION_NAMES = {
|
|
|
259
315
|
"4.9": "Overdose",
|
|
260
316
|
"5.1": "Pharmacodynamic properties",
|
|
261
317
|
"5.2": "Pharmacokinetic properties",
|
|
262
|
-
"5.3": "Preclinical safety data"
|
|
318
|
+
"5.3": "Preclinical safety data",
|
|
319
|
+
"6.1": "List of excipients",
|
|
320
|
+
"6.2": "Incompatibilities",
|
|
321
|
+
"6.3": "Shelf life",
|
|
322
|
+
"6.4": "Special precautions for storage",
|
|
323
|
+
"6.5": "Nature and contents of container",
|
|
324
|
+
"6.6": "Special precautions for disposal"
|
|
263
325
|
};
|
|
264
326
|
function htmlToText(html) {
|
|
265
327
|
const $ = cheerio.load(html);
|
|
@@ -273,7 +335,25 @@ function htmlToText(html) {
|
|
|
273
335
|
});
|
|
274
336
|
return $.text().replace(/\n{3,}/g, "\n\n").trim();
|
|
275
337
|
}
|
|
276
|
-
function
|
|
338
|
+
function matchesSmpcFilter(code, title, requested) {
|
|
339
|
+
return requested.some((req) => {
|
|
340
|
+
const r = req.toLowerCase().trim();
|
|
341
|
+
if (code === req || code === r) return true;
|
|
342
|
+
if (title.toLowerCase().includes(r)) return true;
|
|
343
|
+
if (r.includes("indication")) return code === "4.1";
|
|
344
|
+
if (r.includes("dosage") || r.includes("dosing") || r.includes("posology")) return code === "4.2";
|
|
345
|
+
if (r.includes("contraindication")) return code === "4.3";
|
|
346
|
+
if (r.includes("warning") || r.includes("precaution")) return code === "4.4";
|
|
347
|
+
if (r.includes("interaction")) return code === "4.5";
|
|
348
|
+
if (r.includes("pregnancy") || r.includes("fertility")) return code === "4.6";
|
|
349
|
+
if (r.includes("adverse") || r.includes("undesirable") || r.includes("side effect")) return code === "4.8";
|
|
350
|
+
if (r.includes("overdose") || r.includes("overdosage")) return code === "4.9";
|
|
351
|
+
if (r.includes("pharmacodynamic")) return code === "5.1";
|
|
352
|
+
if (r.includes("pharmacokinetic")) return code === "5.2";
|
|
353
|
+
return false;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function parseSmpcSections(html, requested) {
|
|
277
357
|
const $ = cheerio.load(html);
|
|
278
358
|
const sectionElements = [];
|
|
279
359
|
$("[id*='SECTION'], [id*='section'], .sectionHeading, h2, h3, h4").each((_, el) => {
|
|
@@ -292,11 +372,9 @@ function parseSmpcSections(html) {
|
|
|
292
372
|
}
|
|
293
373
|
});
|
|
294
374
|
}
|
|
295
|
-
const
|
|
375
|
+
const out = [];
|
|
296
376
|
for (let i = 0; i < sectionElements.length; i++) {
|
|
297
377
|
const current = sectionElements[i];
|
|
298
|
-
const field = SMPC_WANT[current.code];
|
|
299
|
-
if (!field) continue;
|
|
300
378
|
const next = sectionElements[i + 1];
|
|
301
379
|
let content = "";
|
|
302
380
|
let node = current.element.next();
|
|
@@ -308,39 +386,59 @@ function parseSmpcSections(html) {
|
|
|
308
386
|
}
|
|
309
387
|
if (!content.trim()) {
|
|
310
388
|
const parentHtml = $.html(current.element.parent());
|
|
311
|
-
if (parentHtml)
|
|
312
|
-
content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
|
|
313
|
-
}
|
|
389
|
+
if (parentHtml) content = htmlToText(parentHtml).replace(current.element.text().trim(), "").trim();
|
|
314
390
|
}
|
|
315
|
-
|
|
391
|
+
const cleaned = cleanSection(content);
|
|
392
|
+
if (cleaned) out.push({ code: current.code, title: current.title, content: cleaned });
|
|
393
|
+
}
|
|
394
|
+
if (requested?.length) {
|
|
395
|
+
return out.filter((s) => matchesSmpcFilter(s.code, s.title, requested));
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
async function getUkSmpc(drug, requested) {
|
|
400
|
+
if (!drug) return null;
|
|
401
|
+
try {
|
|
402
|
+
const best = await searchEmcBest(drug);
|
|
403
|
+
if (!best) return null;
|
|
404
|
+
const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
|
|
405
|
+
if (!res.ok) return null;
|
|
406
|
+
const sections = parseSmpcSections(await res.text(), requested);
|
|
407
|
+
if (sections.length === 0) return null;
|
|
408
|
+
return {
|
|
409
|
+
drugName: best.name,
|
|
410
|
+
productId: best.product_id,
|
|
411
|
+
sections,
|
|
412
|
+
url: `${EMC_BASE}/emc/product/${best.product_id}/smpc`
|
|
413
|
+
};
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
316
416
|
}
|
|
317
|
-
return fields;
|
|
318
417
|
}
|
|
319
418
|
async function getSmpc(drug) {
|
|
320
419
|
if (!drug) return null;
|
|
321
420
|
try {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
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];
|
|
421
|
+
const best = await searchEmcBest(drug);
|
|
422
|
+
if (!best) return null;
|
|
329
423
|
const res = await timedFetch(`${EMC_BASE}/emc/product/${best.product_id}/smpc`);
|
|
330
424
|
if (!res.ok) return null;
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
425
|
+
const all = parseSmpcSections(await res.text());
|
|
426
|
+
const pick = (code) => all.find((s) => s.code === code)?.content ?? null;
|
|
427
|
+
const indications = pick("4.1");
|
|
428
|
+
const dosage = pick("4.2");
|
|
429
|
+
const contraindications = pick("4.3");
|
|
430
|
+
const warnings = pick("4.4");
|
|
431
|
+
const adverse = pick("4.8");
|
|
432
|
+
if (!dosage && !warnings && !adverse && !indications && !contraindications) return null;
|
|
335
433
|
return {
|
|
336
434
|
region: "UK/EU",
|
|
337
435
|
authority: "Summary of Product Characteristics (SmPC)",
|
|
338
436
|
productName: best.name,
|
|
339
|
-
indications
|
|
340
|
-
dosage
|
|
341
|
-
contraindications
|
|
342
|
-
warnings
|
|
343
|
-
adverse
|
|
437
|
+
indications,
|
|
438
|
+
dosage,
|
|
439
|
+
contraindications,
|
|
440
|
+
warnings,
|
|
441
|
+
adverse,
|
|
344
442
|
updated: "",
|
|
345
443
|
sourceUrl: `${EMC_BASE}/emc/product/${best.product_id}/smpc`,
|
|
346
444
|
sourceLabel: "electronic medicines compendium (emc), UK"
|
|
@@ -386,6 +484,8 @@ export {
|
|
|
386
484
|
formatFdaDate,
|
|
387
485
|
getLabels,
|
|
388
486
|
getSmpc,
|
|
487
|
+
getUkSmpc,
|
|
488
|
+
getUsLabel,
|
|
389
489
|
getUspi,
|
|
390
490
|
resolveName
|
|
391
491
|
};
|
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 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 * 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.
|
|
3
|
+
"version": "0.2.0",
|
|
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": {
|