Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

134 lines
5.1 KiB
JavaScript

// dblp author — resolve an author name to a PID and list their publications
// newest first.
//
// Two-step lookup against dblp's public API:
// 1. `search/author/api?q=<name>` returns candidate authors (each with a
// stable PID URL like `https://dblp.org/pid/56/953`).
// 2. `pid/<pid>.xml` returns every publication under that PID, ordered
// newest first.
//
// We auto-resolve to the top hit. dblp's author search returns a single
// best-match for unique names; for ambiguous names (e.g. "Wei Wang") it
// returns multiple PIDs — we pick the highest-scored one and surface the
// resolved name + PID in the row metadata so the caller can refine.
//
// To bypass author search entirely, pass `--pid <prefix>/<id>` (e.g.
// `--pid 56/953`). This is the canonical disambiguator.
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import {
DBLP_ORIGIN,
decodeXmlEntities,
dblpFetchJson,
dblpFetchXml,
extractRecordKey,
recordXmlToRow,
requireBoundedInt,
requireQuery,
} from './utils.js';
const PID_PATTERN = /^[0-9a-z]+(?:\/[0-9a-z-]+)+$/i;
function extractPidFromAuthorHit(hit) {
const url = String(hit?.info?.url ?? '').trim();
const m = url.match(/\/pid\/([^/]+(?:\/[^/]+)+)$/);
return m ? m[1] : '';
}
function pickTopAuthor(hits) {
// dblp returns hits sorted by score desc; we just take the head.
return hits[0];
}
function splitRecords(xml) {
// Each publication is wrapped in <r>…</r> directly under <dblpperson>.
const out = [];
const re = /<r>\s*([\s\S]*?)\s*<\/r>/g;
let m;
while ((m = re.exec(String(xml || ''))) !== null) {
const inner = m[1];
// Skip cross-references that have no concrete record (rare).
if (/^<crossref/.test(inner)) continue;
out.push(inner);
}
return out;
}
cli({
site: 'dblp',
name: 'author',
access: 'read',
description: 'List dblp publications by a given author (newest first; resolves to top PID match)',
domain: 'dblp.org',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'author', positional: true, required: false, help: 'Author name (e.g. "Yoshua Bengio"). Optional when --pid is given.' },
{ name: 'pid', help: 'Canonical dblp PID (e.g. "56/953"). Bypasses author search.' },
{ name: 'limit', type: 'int', default: 20, help: 'Max publications (1-200)' },
],
columns: ['rank', 'key', 'title', 'authors', 'venue', 'year', 'type', 'doi', 'pid', 'url'],
func: async (args) => {
const limit = requireBoundedInt(args.limit, 20, 200);
const pidArg = args.pid != null ? String(args.pid).trim() : '';
let pid = '';
let resolvedName = '';
if (pidArg) {
if (!PID_PATTERN.test(pidArg)) {
throw new ArgumentError(
`dblp pid "${pidArg}" is not a valid PID`,
'Expected something like "56/953" — visit the author page on dblp.org to find it.',
);
}
pid = pidArg;
}
else {
const name = requireQuery(args.author, 'author');
const json = await dblpFetchJson(
`/search/author/api?q=${encodeURIComponent(name)}&format=json&h=20`,
'dblp author search',
);
const raw = json?.result?.hits?.hit;
const hits = Array.isArray(raw) ? raw : (raw ? [raw] : []);
if (!hits.length) {
throw new EmptyResultError(
'dblp author',
`No dblp author matched "${name}". Try a different spelling, or pass --pid to bypass author search.`,
);
}
const top = pickTopAuthor(hits);
pid = extractPidFromAuthorHit(top);
if (!pid) {
throw new CommandExecutionError(
`dblp author search for "${name}" returned a hit without a PID URL`,
'dblp may have changed its author-search response shape; retry or pass --pid manually.',
);
}
resolvedName = decodeXmlEntities(String(top?.info?.author ?? '')).trim();
}
const xml = await dblpFetchXml(`/pid/${pid}.xml`, `dblp pid ${pid}`);
const records = splitRecords(xml);
if (!records.length) {
throw new EmptyResultError(
'dblp author',
`dblp PID ${pid}${resolvedName ? ` (${resolvedName})` : ''} has no publications.`,
);
}
return records.slice(0, limit).map((recordXml, i) => {
const row = recordXmlToRow(`<root>${recordXml}</root>`);
return {
rank: i + 1,
key: row.key || extractRecordKey(recordXml),
title: row.title,
authors: row.authors,
venue: row.venue,
year: row.year,
type: row.type,
doi: row.doi,
pid,
url: row.open_access_url || row.dblp_url,
};
});
},
});