@ncukondo/search-hub 0.23.1 → 0.24.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/dist/cli/commands/fulltext/check.d.ts +4 -0
- package/dist/cli/commands/fulltext/check.d.ts.map +1 -1
- package/dist/cli/commands/fulltext/check.js +41 -4
- package/dist/cli/commands/fulltext/check.js.map +1 -1
- package/dist/cli/commands/fulltext/index.d.ts.map +1 -1
- package/dist/cli/commands/fulltext/index.js +10 -0
- package/dist/cli/commands/fulltext/index.js.map +1 -1
- package/dist/cli/commands/fulltext/verify-pmcid.d.ts +28 -0
- package/dist/cli/commands/fulltext/verify-pmcid.d.ts.map +1 -0
- package/dist/cli/commands/fulltext/verify-pmcid.js +51 -0
- package/dist/cli/commands/fulltext/verify-pmcid.js.map +1 -0
- package/dist/cli/commands/register.d.ts +4 -0
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +62 -13
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/upgrade.d.ts +28 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +96 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +11 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/argv.d.ts +34 -0
- package/dist/cli/utils/argv.d.ts.map +1 -0
- package/dist/cli/utils/argv.js +63 -0
- package/dist/cli/utils/argv.js.map +1 -0
- package/dist/config/paths.js +4 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/integration/csl-json.d.ts +10 -0
- package/dist/integration/csl-json.d.ts.map +1 -1
- package/dist/integration/csl-json.js +12 -0
- package/dist/integration/csl-json.js.map +1 -1
- package/dist/integration/register.d.ts.map +1 -1
- package/dist/integration/register.js +1 -1
- package/dist/integration/register.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js +25 -7
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/convert/jats-parser.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js +67 -25
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/index.js.map +1 -1
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js +3 -3
- package/dist/node_modules/@ncukondo/academic-fulltext/dist/discovery/pmc.js.map +1 -1
- package/dist/package.json.js +1 -1
- package/dist/upgrade/apply-binary.d.ts +37 -0
- package/dist/upgrade/apply-binary.d.ts.map +1 -0
- package/dist/upgrade/apply-binary.js +200 -0
- package/dist/upgrade/apply-binary.js.map +1 -0
- package/dist/upgrade/apply-npm.d.ts +23 -0
- package/dist/upgrade/apply-npm.d.ts.map +1 -0
- package/dist/upgrade/apply-npm.js +96 -0
- package/dist/upgrade/apply-npm.js.map +1 -0
- package/dist/upgrade/check.d.ts +21 -0
- package/dist/upgrade/check.d.ts.map +1 -0
- package/dist/upgrade/check.js +91 -0
- package/dist/upgrade/check.js.map +1 -0
- package/dist/upgrade/detect.d.ts +16 -0
- package/dist/upgrade/detect.d.ts.map +1 -0
- package/dist/upgrade/detect.js +65 -0
- package/dist/upgrade/detect.js.map +1 -0
- package/dist/upgrade/notifier.d.ts +26 -0
- package/dist/upgrade/notifier.d.ts.map +1 -0
- package/dist/upgrade/notifier.js +111 -0
- package/dist/upgrade/notifier.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the subcommand name from argv (best-effort).
|
|
4
|
+
*
|
|
5
|
+
* Skips global options that take a value so things like `--config upgrade`
|
|
6
|
+
* are not misread as the `upgrade` subcommand. The set of value-taking
|
|
7
|
+
* options is derived from the Commander program itself, so it stays in sync
|
|
8
|
+
* with the option definitions.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractCommandName(argv: string[], program: Command): string;
|
|
11
|
+
/**
|
|
12
|
+
* Best-effort detection of `--no-update-check` in argv, without relying on
|
|
13
|
+
* Commander having parsed yet (the async check starts before parseAsync).
|
|
14
|
+
*/
|
|
15
|
+
export declare function hasNoUpdateCheckFlag(argv: string[]): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Best-effort detection of the global `--quiet` flag in argv, without relying
|
|
18
|
+
* on Commander having parsed yet (the async check starts before parseAsync).
|
|
19
|
+
* `--quiet` promises "suppress all output except errors", which includes the
|
|
20
|
+
* update notice.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hasQuietFlag(argv: string[]): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Rewrite `search-hub upgrade --version <tag>` to `--version=<tag>`.
|
|
25
|
+
*
|
|
26
|
+
* The program has `.version(VERSION)` which registers a no-arg `--version`
|
|
27
|
+
* flag at the root. Commander's parser consumes the root `--version` before
|
|
28
|
+
* subcommand options, so the space-separated form is caught by the root
|
|
29
|
+
* (prints + exits) instead of reaching `upgrade`'s `--version <tag>`. The
|
|
30
|
+
* `=` form binds the value to the subcommand's option and sidesteps the
|
|
31
|
+
* root flag.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rewriteUpgradeVersionFlag(argv: string[], program: Command): string[];
|
|
34
|
+
//# sourceMappingURL=argv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"argv.d.ts","sourceRoot":"","sources":["../../../src/cli/utils/argv.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBzC;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAkB3E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAK5D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAKpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAiBpF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function collectValueTakingFlags(program) {
|
|
2
|
+
const flags = /* @__PURE__ */ new Set();
|
|
3
|
+
for (const opt of program.options) {
|
|
4
|
+
if (!opt.required && !opt.optional) continue;
|
|
5
|
+
if (opt.long) flags.add(opt.long);
|
|
6
|
+
if (opt.short) flags.add(opt.short);
|
|
7
|
+
}
|
|
8
|
+
return flags;
|
|
9
|
+
}
|
|
10
|
+
function extractCommandName(argv, program) {
|
|
11
|
+
const valueTakingFlags = collectValueTakingFlags(program);
|
|
12
|
+
let skipNext = false;
|
|
13
|
+
for (let i = 2; i < argv.length; i++) {
|
|
14
|
+
const token = argv[i];
|
|
15
|
+
if (!token) continue;
|
|
16
|
+
if (skipNext) {
|
|
17
|
+
skipNext = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (token.startsWith("-")) {
|
|
21
|
+
if (!token.includes("=") && valueTakingFlags.has(token)) skipNext = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
function hasNoUpdateCheckFlag(argv) {
|
|
29
|
+
for (let i = 2; i < argv.length; i++) {
|
|
30
|
+
if (argv[i] === "--no-update-check") return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
function hasQuietFlag(argv) {
|
|
35
|
+
for (let i = 2; i < argv.length; i++) {
|
|
36
|
+
if (argv[i] === "--quiet") return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function rewriteUpgradeVersionFlag(argv, program) {
|
|
41
|
+
if (extractCommandName(argv, program) !== "upgrade") return argv;
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const token = argv[i];
|
|
45
|
+
if (token === "--version" && i + 1 < argv.length) {
|
|
46
|
+
const next = argv[i + 1];
|
|
47
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
48
|
+
out.push(`--version=${next}`);
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (token !== void 0) out.push(token);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
extractCommandName,
|
|
59
|
+
hasNoUpdateCheckFlag,
|
|
60
|
+
hasQuietFlag,
|
|
61
|
+
rewriteUpgradeVersionFlag
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=argv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"argv.js","sources":["../../../src/cli/utils/argv.ts"],"sourcesContent":["/**\n * Argv helpers for the update-check notifier and the `upgrade` command.\n *\n * These run before Commander parses argv (the async update check starts\n * before command dispatch), so they are best-effort token scanners.\n */\nimport type { Command } from 'commander';\n\n/**\n * Collect flags (both `--long` and `-s`) for global options that take a value.\n * Used by {@link extractCommandName} so the subcommand parser can skip the\n * value after options like `--config <path>`.\n */\nfunction collectValueTakingFlags(program: Command): Set<string> {\n const flags = new Set<string>();\n for (const opt of program.options) {\n if (!opt.required && !opt.optional) continue;\n if (opt.long) flags.add(opt.long);\n if (opt.short) flags.add(opt.short);\n }\n return flags;\n}\n\n/**\n * Extract the subcommand name from argv (best-effort).\n *\n * Skips global options that take a value so things like `--config upgrade`\n * are not misread as the `upgrade` subcommand. The set of value-taking\n * options is derived from the Commander program itself, so it stays in sync\n * with the option definitions.\n */\nexport function extractCommandName(argv: string[], program: Command): string {\n const valueTakingFlags = collectValueTakingFlags(program);\n let skipNext = false;\n for (let i = 2; i < argv.length; i++) {\n const token = argv[i];\n if (!token) continue;\n if (skipNext) {\n skipNext = false;\n continue;\n }\n if (token.startsWith('-')) {\n // `--opt=value` bundles its value, no skip needed.\n if (!token.includes('=') && valueTakingFlags.has(token)) skipNext = true;\n continue;\n }\n return token;\n }\n return '';\n}\n\n/**\n * Best-effort detection of `--no-update-check` in argv, without relying on\n * Commander having parsed yet (the async check starts before parseAsync).\n */\nexport function hasNoUpdateCheckFlag(argv: string[]): boolean {\n for (let i = 2; i < argv.length; i++) {\n if (argv[i] === '--no-update-check') return true;\n }\n return false;\n}\n\n/**\n * Best-effort detection of the global `--quiet` flag in argv, without relying\n * on Commander having parsed yet (the async check starts before parseAsync).\n * `--quiet` promises \"suppress all output except errors\", which includes the\n * update notice.\n */\nexport function hasQuietFlag(argv: string[]): boolean {\n for (let i = 2; i < argv.length; i++) {\n if (argv[i] === '--quiet') return true;\n }\n return false;\n}\n\n/**\n * Rewrite `search-hub upgrade --version <tag>` to `--version=<tag>`.\n *\n * The program has `.version(VERSION)` which registers a no-arg `--version`\n * flag at the root. Commander's parser consumes the root `--version` before\n * subcommand options, so the space-separated form is caught by the root\n * (prints + exits) instead of reaching `upgrade`'s `--version <tag>`. The\n * `=` form binds the value to the subcommand's option and sidesteps the\n * root flag.\n */\nexport function rewriteUpgradeVersionFlag(argv: string[], program: Command): string[] {\n if (extractCommandName(argv, program) !== 'upgrade') return argv;\n\n const out: string[] = [];\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i];\n if (token === '--version' && i + 1 < argv.length) {\n const next = argv[i + 1];\n if (next !== undefined && !next.startsWith('-')) {\n out.push(`--version=${next}`);\n i++;\n continue;\n }\n }\n if (token !== undefined) out.push(token);\n }\n return out;\n}\n"],"names":[],"mappings":"AAaA,SAAS,wBAAwB,SAA+B;AAC9D,QAAM,4BAAY,IAAA;AAClB,aAAW,OAAO,QAAQ,SAAS;AACjC,QAAI,CAAC,IAAI,YAAY,CAAC,IAAI,SAAU;AACpC,QAAI,IAAI,KAAM,OAAM,IAAI,IAAI,IAAI;AAChC,QAAI,IAAI,MAAO,OAAM,IAAI,IAAI,KAAK;AAAA,EACpC;AACA,SAAO;AACT;AAUO,SAAS,mBAAmB,MAAgB,SAA0B;AAC3E,QAAM,mBAAmB,wBAAwB,OAAO;AACxD,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,CAAC,MAAO;AACZ,QAAI,UAAU;AACZ,iBAAW;AACX;AAAA,IACF;AACA,QAAI,MAAM,WAAW,GAAG,GAAG;AAEzB,UAAI,CAAC,MAAM,SAAS,GAAG,KAAK,iBAAiB,IAAI,KAAK,EAAG,YAAW;AACpE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAMO,SAAS,qBAAqB,MAAyB;AAC5D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,CAAC,MAAM,oBAAqB,QAAO;AAAA,EAC9C;AACA,SAAO;AACT;AAQO,SAAS,aAAa,MAAyB;AACpD,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,CAAC,MAAM,UAAW,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAYO,SAAS,0BAA0B,MAAgB,SAA4B;AACpF,MAAI,mBAAmB,MAAM,OAAO,MAAM,UAAW,QAAO;AAE5D,QAAM,MAAgB,CAAA;AACtB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,eAAe,IAAI,IAAI,KAAK,QAAQ;AAChD,YAAM,OAAO,KAAK,IAAI,CAAC;AACvB,UAAI,SAAS,UAAa,CAAC,KAAK,WAAW,GAAG,GAAG;AAC/C,YAAI,KAAK,aAAa,IAAI,EAAE;AAC5B;AACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,UAAU,OAAW,KAAI,KAAK,KAAK;AAAA,EACzC;AACA,SAAO;AACT;"}
|
package/dist/config/paths.js
CHANGED
|
@@ -5,6 +5,9 @@ const paths = envPaths("search-hub", { suffix: "" });
|
|
|
5
5
|
function getConfigDir() {
|
|
6
6
|
return paths.config;
|
|
7
7
|
}
|
|
8
|
+
function getDataDir() {
|
|
9
|
+
return paths.data;
|
|
10
|
+
}
|
|
8
11
|
function getDefaultConfigPath() {
|
|
9
12
|
return join(paths.config, "config.toml");
|
|
10
13
|
}
|
|
@@ -34,6 +37,7 @@ async function isInsideProject(baseDir) {
|
|
|
34
37
|
}
|
|
35
38
|
export {
|
|
36
39
|
getConfigDir,
|
|
40
|
+
getDataDir,
|
|
37
41
|
getDefaultConfigPath,
|
|
38
42
|
getDefaultSessionsDir,
|
|
39
43
|
getLocalConfigPath,
|
package/dist/config/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sources":["../../src/config/paths.ts"],"sourcesContent":["/**\n * Platform-specific paths using XDG Base Directory spec on Linux,\n * ~/Library on macOS, and AppData on Windows.\n */\nimport envPaths from 'env-paths';\nimport { join } from 'node:path';\nimport { stat } from 'node:fs/promises';\n\n// Use empty suffix to get clean 'search-hub' directory names\nconst paths = envPaths('search-hub', { suffix: '' });\n\n/**\n * Get the config directory for search-hub.\n * - Linux: ~/.config/search-hub\n * - macOS: ~/Library/Preferences/search-hub\n * - Windows: %APPDATA%\\search-hub\\Config\n */\nexport function getConfigDir(): string {\n return paths.config;\n}\n\n/**\n * Get the data directory for search-hub.\n * - Linux: ~/.local/share/search-hub\n * - macOS: ~/Library/Application Support/search-hub\n * - Windows: %LOCALAPPDATA%\\search-hub\\Data\n */\nexport function getDataDir(): string {\n return paths.data;\n}\n\n/**\n * Get the default config file path.\n */\nexport function getDefaultConfigPath(): string {\n return join(paths.config, 'config.toml');\n}\n\n/**\n * Get the default sessions directory.\n */\nexport function getDefaultSessionsDir(): string {\n return join(paths.data, 'sessions');\n}\n\n/** Name of the project-local directory. */\nconst PROJECT_DIR_NAME = '.search-hub';\n\n/**\n * Get the project directory path (.search-hub/) relative to a base directory.\n * Defaults to cwd.\n */\nexport function getProjectDir(baseDir?: string): string {\n return join(baseDir ?? process.cwd(), PROJECT_DIR_NAME);\n}\n\n/**\n * Get the local config file path (.search-hub/config.toml).\n */\nexport function getLocalConfigPath(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'config.toml');\n}\n\n/**\n * Get the local sessions directory (.search-hub/sessions/).\n */\nexport function getLocalSessionsDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'sessions');\n}\n\n/**\n * Get the local queries directory (.search-hub/queries/).\n */\nexport function getQueriesDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'queries');\n}\n\n/**\n * Check if the given directory (default: cwd) contains a .search-hub/ project directory.\n */\nexport async function isInsideProject(baseDir?: string): Promise<boolean> {\n try {\n const stats = await stat(getProjectDir(baseDir));\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n"],"names":[],"mappings":";;;AASA,MAAM,QAAQ,SAAS,cAAc,EAAE,QAAQ,IAAI;AAQ5C,SAAS,eAAuB;AACrC,SAAO,MAAM;AACf;
|
|
1
|
+
{"version":3,"file":"paths.js","sources":["../../src/config/paths.ts"],"sourcesContent":["/**\n * Platform-specific paths using XDG Base Directory spec on Linux,\n * ~/Library on macOS, and AppData on Windows.\n */\nimport envPaths from 'env-paths';\nimport { join } from 'node:path';\nimport { stat } from 'node:fs/promises';\n\n// Use empty suffix to get clean 'search-hub' directory names\nconst paths = envPaths('search-hub', { suffix: '' });\n\n/**\n * Get the config directory for search-hub.\n * - Linux: ~/.config/search-hub\n * - macOS: ~/Library/Preferences/search-hub\n * - Windows: %APPDATA%\\search-hub\\Config\n */\nexport function getConfigDir(): string {\n return paths.config;\n}\n\n/**\n * Get the data directory for search-hub.\n * - Linux: ~/.local/share/search-hub\n * - macOS: ~/Library/Application Support/search-hub\n * - Windows: %LOCALAPPDATA%\\search-hub\\Data\n */\nexport function getDataDir(): string {\n return paths.data;\n}\n\n/**\n * Get the default config file path.\n */\nexport function getDefaultConfigPath(): string {\n return join(paths.config, 'config.toml');\n}\n\n/**\n * Get the default sessions directory.\n */\nexport function getDefaultSessionsDir(): string {\n return join(paths.data, 'sessions');\n}\n\n/** Name of the project-local directory. */\nconst PROJECT_DIR_NAME = '.search-hub';\n\n/**\n * Get the project directory path (.search-hub/) relative to a base directory.\n * Defaults to cwd.\n */\nexport function getProjectDir(baseDir?: string): string {\n return join(baseDir ?? process.cwd(), PROJECT_DIR_NAME);\n}\n\n/**\n * Get the local config file path (.search-hub/config.toml).\n */\nexport function getLocalConfigPath(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'config.toml');\n}\n\n/**\n * Get the local sessions directory (.search-hub/sessions/).\n */\nexport function getLocalSessionsDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'sessions');\n}\n\n/**\n * Get the local queries directory (.search-hub/queries/).\n */\nexport function getQueriesDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'queries');\n}\n\n/**\n * Check if the given directory (default: cwd) contains a .search-hub/ project directory.\n */\nexport async function isInsideProject(baseDir?: string): Promise<boolean> {\n try {\n const stats = await stat(getProjectDir(baseDir));\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n"],"names":[],"mappings":";;;AASA,MAAM,QAAQ,SAAS,cAAc,EAAE,QAAQ,IAAI;AAQ5C,SAAS,eAAuB;AACrC,SAAO,MAAM;AACf;AAQO,SAAS,aAAqB;AACnC,SAAO,MAAM;AACf;AAKO,SAAS,uBAA+B;AAC7C,SAAO,KAAK,MAAM,QAAQ,aAAa;AACzC;AAKO,SAAS,wBAAgC;AAC9C,SAAO,KAAK,MAAM,MAAM,UAAU;AACpC;AAGA,MAAM,mBAAmB;AAMlB,SAAS,cAAc,SAA0B;AACtD,SAAO,KAAK,WAAW,QAAQ,IAAA,GAAO,gBAAgB;AACxD;AAKO,SAAS,mBAAmB,SAA0B;AAC3D,SAAO,KAAK,cAAc,OAAO,GAAG,aAAa;AACnD;AAKO,SAAS,oBAAoB,SAA0B;AAC5D,SAAO,KAAK,cAAc,OAAO,GAAG,UAAU;AAChD;AAKO,SAAS,cAAc,SAA0B;AACtD,SAAO,KAAK,cAAc,OAAO,GAAG,SAAS;AAC/C;AAKA,eAAsB,gBAAgB,SAAoC;AACxE,MAAI;AACF,UAAM,QAAQ,MAAM,KAAK,cAAc,OAAO,CAAC;AAC/C,WAAO,MAAM,YAAA;AAAA,EACf,QAAQ;AACN,WAAO;AAAA,EACT;AACF;"}
|
|
@@ -10,6 +10,7 @@ export interface CslJsonItem {
|
|
|
10
10
|
}>;
|
|
11
11
|
DOI?: string;
|
|
12
12
|
PMID?: string;
|
|
13
|
+
URL?: string;
|
|
13
14
|
abstract?: string;
|
|
14
15
|
issued?: {
|
|
15
16
|
'date-parts': number[][];
|
|
@@ -18,6 +19,15 @@ export interface CslJsonItem {
|
|
|
18
19
|
volume?: string;
|
|
19
20
|
issue?: string;
|
|
20
21
|
page?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Alternative identifiers. Key names match what reference-manager reads:
|
|
24
|
+
* its duplicate detector and fulltext discovery use `custom.arxiv_id`.
|
|
25
|
+
*/
|
|
26
|
+
custom?: {
|
|
27
|
+
arxiv_id?: string;
|
|
28
|
+
eric_id?: string;
|
|
29
|
+
scopus_id?: string;
|
|
30
|
+
};
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
33
|
* Generate a human-readable CSL ID from an article (author-year format).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csl-json.d.ts","sourceRoot":"","sources":["../../src/integration/csl-json.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,+CAA+C;AAC/C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAA;KAAE,CAAC;IACtC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"csl-json.d.ts","sourceRoot":"","sources":["../../src/integration/csl-json.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,+CAA+C;AAC/C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAA;KAAE,CAAC;IACtC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAStD;AAYD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,MAAM,EAAE,EAAE,GAAG,SAAS,CAOxB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,WAAW,CAyC1E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,WAAW,EAAE,CAqBpE"}
|
|
@@ -27,6 +27,18 @@ function articleToCslJson(article, id) {
|
|
|
27
27
|
};
|
|
28
28
|
if (article.doi) item.DOI = article.doi;
|
|
29
29
|
if (article.pmid) item.PMID = article.pmid;
|
|
30
|
+
const custom = {};
|
|
31
|
+
if (article.arxivId) custom.arxiv_id = article.arxivId;
|
|
32
|
+
if (article.ericId) custom.eric_id = article.ericId;
|
|
33
|
+
if (article.scopusId) custom.scopus_id = article.scopusId;
|
|
34
|
+
if (Object.keys(custom).length > 0) item.custom = custom;
|
|
35
|
+
if (!article.doi) {
|
|
36
|
+
if (article.arxivId) {
|
|
37
|
+
item.URL = `https://arxiv.org/abs/${article.arxivId}`;
|
|
38
|
+
} else if (article.ericId) {
|
|
39
|
+
item.URL = `https://eric.ed.gov/?id=${article.ericId}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
if (article.abstract) item.abstract = article.abstract;
|
|
31
43
|
const dateParts = parseDateParts(article.publicationDate);
|
|
32
44
|
if (dateParts) item.issued = { "date-parts": dateParts };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csl-json.js","sources":["../../src/integration/csl-json.ts"],"sourcesContent":["/**\n * CSL-JSON conversion module.\n * Converts Article objects to CSL-JSON format for bulk import via ref add -i json.\n */\n\nimport type { Article } from '../providers/base/types.js';\n\n/** CSL-JSON item type used for bulk import. */\nexport interface CslJsonItem {\n id: string;\n type: string;\n title: string;\n author: Array<{ family: string; given?: string }>;\n DOI?: string;\n PMID?: string;\n abstract?: string;\n issued?: { 'date-parts': number[][] };\n 'container-title'?: string;\n volume?: string;\n issue?: string;\n page?: string;\n}\n\n/**\n * Generate a human-readable CSL ID from an article (author-year format).\n *\n * Format: `{first-author-family}-{year}`\n * - No author: `anon-{year}`\n * - No year: `{author}-nd`\n * - Duplicate resolution is handled by `articlesToCslJson()` at the batch level.\n */\nexport function generateCslId(article: Article): string {\n const authorPart =\n article.authors.length > 0\n ? article.authors[0]!.family.toLowerCase()\n : 'anon';\n\n const yearPart = extractYear(article.publicationDate);\n\n return `${authorPart}-${yearPart}`;\n}\n\n/**\n * Extract year from a publication date string.\n * Handles formats: \"2024-01-15\", \"2024-01\", \"2024\"\n */\nfunction extractYear(date: string | undefined): string {\n if (!date) return 'nd';\n const match = date.match(/^(\\d{4})/);\n return match ? match[1]! : 'nd';\n}\n\n/**\n * Parse a publication date string into CSL date-parts format.\n *\n * - `\"2024-01-15\"` → `[[2024, 1, 15]]`\n * - `\"2024-01\"` → `[[2024, 1]]`\n * - `\"2024\"` → `[[2024]]`\n * - `undefined` or `\"\"` → `undefined`\n */\nexport function parseDateParts(\n date: string | undefined\n): number[][] | undefined {\n if (!date) return undefined;\n\n const parts = date.split('-').map(Number);\n if (parts.length === 0 || Number.isNaN(parts[0])) return undefined;\n\n return [parts];\n}\n\n/**\n * Convert a single Article to a CSL-JSON item.\n * The `id` parameter must be pre-generated (with duplicate resolution already applied).\n */\nexport function articleToCslJson(article: Article, id: string): CslJsonItem {\n const item: CslJsonItem = {\n id,\n type: 'article-journal',\n title: article.title,\n author: article.authors.map((a) => {\n const entry: { family: string; given?: string } = { family: a.family };\n if (a.given) entry.given = a.given;\n return entry;\n }),\n };\n\n if (article.doi) item.DOI = article.doi;\n if (article.pmid) item.PMID = article.pmid;\n if (article.abstract) item.abstract = article.abstract;\n\n const dateParts = parseDateParts(article.publicationDate);\n if (dateParts) item.issued = { 'date-parts': dateParts };\n\n if (article.journal) item['container-title'] = article.journal;\n if (article.volume) item.volume = article.volume;\n if (article.issue) item.issue = article.issue;\n if (article.pages) item.page = article.pages;\n\n return item;\n}\n\n/**\n * Convert an array of Articles to CSL-JSON items with duplicate ID resolution.\n *\n * Duplicates within the batch are resolved by appending suffixes:\n * `smith-2024`, `smith-2024a`, `smith-2024b`, ...\n */\nexport function articlesToCslJson(articles: Article[]): CslJsonItem[] {\n const idCounts = new Map<string, number>();\n const result: CslJsonItem[] = [];\n\n for (const article of articles) {\n const baseId = generateCslId(article);\n const count = idCounts.get(baseId) ?? 0;\n idCounts.set(baseId, count + 1);\n\n let resolvedId: string;\n if (count === 0) {\n resolvedId = baseId;\n } else {\n // a=97 in ASCII, so suffix is 'a', 'b', 'c', ...\n resolvedId = `${baseId}${String.fromCharCode(96 + count)}`;\n }\n\n result.push(articleToCslJson(article, resolvedId));\n }\n\n return result;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"csl-json.js","sources":["../../src/integration/csl-json.ts"],"sourcesContent":["/**\n * CSL-JSON conversion module.\n * Converts Article objects to CSL-JSON format for bulk import via ref add -i json.\n */\n\nimport type { Article } from '../providers/base/types.js';\n\n/** CSL-JSON item type used for bulk import. */\nexport interface CslJsonItem {\n id: string;\n type: string;\n title: string;\n author: Array<{ family: string; given?: string }>;\n DOI?: string;\n PMID?: string;\n URL?: string;\n abstract?: string;\n issued?: { 'date-parts': number[][] };\n 'container-title'?: string;\n volume?: string;\n issue?: string;\n page?: string;\n /**\n * Alternative identifiers. Key names match what reference-manager reads:\n * its duplicate detector and fulltext discovery use `custom.arxiv_id`.\n */\n custom?: {\n arxiv_id?: string;\n eric_id?: string;\n scopus_id?: string;\n };\n}\n\n/**\n * Generate a human-readable CSL ID from an article (author-year format).\n *\n * Format: `{first-author-family}-{year}`\n * - No author: `anon-{year}`\n * - No year: `{author}-nd`\n * - Duplicate resolution is handled by `articlesToCslJson()` at the batch level.\n */\nexport function generateCslId(article: Article): string {\n const authorPart =\n article.authors.length > 0\n ? article.authors[0]!.family.toLowerCase()\n : 'anon';\n\n const yearPart = extractYear(article.publicationDate);\n\n return `${authorPart}-${yearPart}`;\n}\n\n/**\n * Extract year from a publication date string.\n * Handles formats: \"2024-01-15\", \"2024-01\", \"2024\"\n */\nfunction extractYear(date: string | undefined): string {\n if (!date) return 'nd';\n const match = date.match(/^(\\d{4})/);\n return match ? match[1]! : 'nd';\n}\n\n/**\n * Parse a publication date string into CSL date-parts format.\n *\n * - `\"2024-01-15\"` → `[[2024, 1, 15]]`\n * - `\"2024-01\"` → `[[2024, 1]]`\n * - `\"2024\"` → `[[2024]]`\n * - `undefined` or `\"\"` → `undefined`\n */\nexport function parseDateParts(\n date: string | undefined\n): number[][] | undefined {\n if (!date) return undefined;\n\n const parts = date.split('-').map(Number);\n if (parts.length === 0 || Number.isNaN(parts[0])) return undefined;\n\n return [parts];\n}\n\n/**\n * Convert a single Article to a CSL-JSON item.\n * The `id` parameter must be pre-generated (with duplicate resolution already applied).\n */\nexport function articleToCslJson(article: Article, id: string): CslJsonItem {\n const item: CslJsonItem = {\n id,\n type: 'article-journal',\n title: article.title,\n author: article.authors.map((a) => {\n const entry: { family: string; given?: string } = { family: a.family };\n if (a.given) entry.given = a.given;\n return entry;\n }),\n };\n\n if (article.doi) item.DOI = article.doi;\n if (article.pmid) item.PMID = article.pmid;\n\n const custom: NonNullable<CslJsonItem['custom']> = {};\n if (article.arxivId) custom.arxiv_id = article.arxivId;\n if (article.ericId) custom.eric_id = article.ericId;\n if (article.scopusId) custom.scopus_id = article.scopusId;\n if (Object.keys(custom).length > 0) item.custom = custom;\n\n // Without a DOI, give resolvers a landing page for the alternative identifier\n if (!article.doi) {\n if (article.arxivId) {\n item.URL = `https://arxiv.org/abs/${article.arxivId}`;\n } else if (article.ericId) {\n item.URL = `https://eric.ed.gov/?id=${article.ericId}`;\n }\n }\n\n if (article.abstract) item.abstract = article.abstract;\n\n const dateParts = parseDateParts(article.publicationDate);\n if (dateParts) item.issued = { 'date-parts': dateParts };\n\n if (article.journal) item['container-title'] = article.journal;\n if (article.volume) item.volume = article.volume;\n if (article.issue) item.issue = article.issue;\n if (article.pages) item.page = article.pages;\n\n return item;\n}\n\n/**\n * Convert an array of Articles to CSL-JSON items with duplicate ID resolution.\n *\n * Duplicates within the batch are resolved by appending suffixes:\n * `smith-2024`, `smith-2024a`, `smith-2024b`, ...\n */\nexport function articlesToCslJson(articles: Article[]): CslJsonItem[] {\n const idCounts = new Map<string, number>();\n const result: CslJsonItem[] = [];\n\n for (const article of articles) {\n const baseId = generateCslId(article);\n const count = idCounts.get(baseId) ?? 0;\n idCounts.set(baseId, count + 1);\n\n let resolvedId: string;\n if (count === 0) {\n resolvedId = baseId;\n } else {\n // a=97 in ASCII, so suffix is 'a', 'b', 'c', ...\n resolvedId = `${baseId}${String.fromCharCode(96 + count)}`;\n }\n\n result.push(articleToCslJson(article, resolvedId));\n }\n\n return result;\n}\n"],"names":[],"mappings":"AAyCO,SAAS,cAAc,SAA0B;AACtD,QAAM,aACJ,QAAQ,QAAQ,SAAS,IACrB,QAAQ,QAAQ,CAAC,EAAG,OAAO,YAAA,IAC3B;AAEN,QAAM,WAAW,YAAY,QAAQ,eAAe;AAEpD,SAAO,GAAG,UAAU,IAAI,QAAQ;AAClC;AAMA,SAAS,YAAY,MAAkC;AACrD,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,MAAM,UAAU;AACnC,SAAO,QAAQ,MAAM,CAAC,IAAK;AAC7B;AAUO,SAAS,eACd,MACwB;AACxB,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,IAAI,MAAM;AACxC,MAAI,MAAM,WAAW,KAAK,OAAO,MAAM,MAAM,CAAC,CAAC,EAAG,QAAO;AAEzD,SAAO,CAAC,KAAK;AACf;AAMO,SAAS,iBAAiB,SAAkB,IAAyB;AAC1E,QAAM,OAAoB;AAAA,IACxB;AAAA,IACA,MAAM;AAAA,IACN,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ,QAAQ,IAAI,CAAC,MAAM;AACjC,YAAM,QAA4C,EAAE,QAAQ,EAAE,OAAA;AAC9D,UAAI,EAAE,MAAO,OAAM,QAAQ,EAAE;AAC7B,aAAO;AAAA,IACT,CAAC;AAAA,EAAA;AAGH,MAAI,QAAQ,IAAK,MAAK,MAAM,QAAQ;AACpC,MAAI,QAAQ,KAAM,MAAK,OAAO,QAAQ;AAEtC,QAAM,SAA6C,CAAA;AACnD,MAAI,QAAQ,QAAS,QAAO,WAAW,QAAQ;AAC/C,MAAI,QAAQ,OAAQ,QAAO,UAAU,QAAQ;AAC7C,MAAI,QAAQ,SAAU,QAAO,YAAY,QAAQ;AACjD,MAAI,OAAO,KAAK,MAAM,EAAE,SAAS,QAAQ,SAAS;AAGlD,MAAI,CAAC,QAAQ,KAAK;AAChB,QAAI,QAAQ,SAAS;AACnB,WAAK,MAAM,yBAAyB,QAAQ,OAAO;AAAA,IACrD,WAAW,QAAQ,QAAQ;AACzB,WAAK,MAAM,2BAA2B,QAAQ,MAAM;AAAA,IACtD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAU,MAAK,WAAW,QAAQ;AAE9C,QAAM,YAAY,eAAe,QAAQ,eAAe;AACxD,MAAI,UAAW,MAAK,SAAS,EAAE,cAAc,UAAA;AAE7C,MAAI,QAAQ,QAAS,MAAK,iBAAiB,IAAI,QAAQ;AACvD,MAAI,QAAQ,OAAQ,MAAK,SAAS,QAAQ;AAC1C,MAAI,QAAQ,MAAO,MAAK,QAAQ,QAAQ;AACxC,MAAI,QAAQ,MAAO,MAAK,OAAO,QAAQ;AAEvC,SAAO;AACT;AAQO,SAAS,kBAAkB,UAAoC;AACpE,QAAM,+BAAe,IAAA;AACrB,QAAM,SAAwB,CAAA;AAE9B,aAAW,WAAW,UAAU;AAC9B,UAAM,SAAS,cAAc,OAAO;AACpC,UAAM,QAAQ,SAAS,IAAI,MAAM,KAAK;AACtC,aAAS,IAAI,QAAQ,QAAQ,CAAC;AAE9B,QAAI;AACJ,QAAI,UAAU,GAAG;AACf,mBAAa;AAAA,IACf,OAAO;AAEL,mBAAa,GAAG,MAAM,GAAG,OAAO,aAAa,KAAK,KAAK,CAAC;AAAA,IAC1D;AAEA,WAAO,KAAK,iBAAiB,SAAS,UAAU,CAAC;AAAA,EACnD;AAEA,SAAO;AACT;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/integration/register.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQ/E;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAcpC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/integration/register.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQ/E;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAcpC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7D;AAiBD;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA+H7B"}
|
|
@@ -12,7 +12,7 @@ async function saveRegistrationRecord(sessionDir, record) {
|
|
|
12
12
|
await fs.writeFile(filePath, JSON.stringify(record, null, 2));
|
|
13
13
|
}
|
|
14
14
|
function hasIdentifier(article) {
|
|
15
|
-
return !!(article.pmid || article.doi);
|
|
15
|
+
return !!(article.pmid || article.doi || article.arxivId || article.scopusId || article.ericId);
|
|
16
16
|
}
|
|
17
17
|
async function registerArticles(articles, options) {
|
|
18
18
|
const { sessionId, sessionDir, withAbstracts, noAttachFulltext, onProgress, onAttachProgress } = options;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sources":["../../src/integration/register.ts"],"sourcesContent":["/**\n * Registration logic for reference-manager integration.\n * Uses bulk CSL-JSON import for performance.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Article } from '../providers/base/types.js';\nimport { RegistrationRecordSchema, type RegistrationRecord } from './types.js';\nimport { refAddBulk } from './ref-cli.js';\nimport { articlesToCslJson } from './csl-json.js';\nimport { attachFulltexts } from './fulltext-attach.js';\n\nconst REGISTRATION_FILE = 'registration.json';\nconst BULK_IMPORT_FILE = '_bulk_import.json';\n\n/**\n * Save registration record to session directory.\n */\nexport async function saveRegistrationRecord(\n sessionDir: string,\n record: RegistrationRecord\n): Promise<void> {\n await fs.mkdir(sessionDir, { recursive: true });\n const filePath = path.join(sessionDir, REGISTRATION_FILE);\n await fs.writeFile(filePath, JSON.stringify(record, null, 2));\n}\n\n/**\n * Load registration record from session directory.\n * Returns null if file does not exist.\n * Throws if file exists but is invalid.\n */\nexport async function loadRegistrationRecord(\n sessionDir: string\n): Promise<RegistrationRecord | null> {\n const filePath = path.join(sessionDir, REGISTRATION_FILE);\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n const data = JSON.parse(content);\n return RegistrationRecordSchema.parse(data);\n } catch (error) {\n // File not found - return null\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n return null;\n }\n // Parse or validation error - re-throw\n throw error;\n }\n}\n\n/**\n * Options for registerArticles function.\n */\nexport interface RegisterOptions {\n sessionId: string;\n sessionDir: string;\n withAbstracts?: boolean;\n /** Skip fulltext attach step */\n noAttachFulltext?: boolean;\n onProgress?: (current: number, total: number) => void;\n onAttachProgress?: (current: number, total: number) => void;\n}\n\n/**\n * Check if an article has an identifier suitable for registration.\n *
|
|
1
|
+
{"version":3,"file":"register.js","sources":["../../src/integration/register.ts"],"sourcesContent":["/**\n * Registration logic for reference-manager integration.\n * Uses bulk CSL-JSON import for performance.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { Article } from '../providers/base/types.js';\nimport { RegistrationRecordSchema, type RegistrationRecord } from './types.js';\nimport { refAddBulk } from './ref-cli.js';\nimport { articlesToCslJson } from './csl-json.js';\nimport { attachFulltexts } from './fulltext-attach.js';\n\nconst REGISTRATION_FILE = 'registration.json';\nconst BULK_IMPORT_FILE = '_bulk_import.json';\n\n/**\n * Save registration record to session directory.\n */\nexport async function saveRegistrationRecord(\n sessionDir: string,\n record: RegistrationRecord\n): Promise<void> {\n await fs.mkdir(sessionDir, { recursive: true });\n const filePath = path.join(sessionDir, REGISTRATION_FILE);\n await fs.writeFile(filePath, JSON.stringify(record, null, 2));\n}\n\n/**\n * Load registration record from session directory.\n * Returns null if file does not exist.\n * Throws if file exists but is invalid.\n */\nexport async function loadRegistrationRecord(\n sessionDir: string\n): Promise<RegistrationRecord | null> {\n const filePath = path.join(sessionDir, REGISTRATION_FILE);\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n const data = JSON.parse(content);\n return RegistrationRecordSchema.parse(data);\n } catch (error) {\n // File not found - return null\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {\n return null;\n }\n // Parse or validation error - re-throw\n throw error;\n }\n}\n\n/**\n * Options for registerArticles function.\n */\nexport interface RegisterOptions {\n sessionId: string;\n sessionDir: string;\n withAbstracts?: boolean;\n /** Skip fulltext attach step */\n noAttachFulltext?: boolean;\n onProgress?: (current: number, total: number) => void;\n onAttachProgress?: (current: number, total: number) => void;\n}\n\n/**\n * Check if an article has an identifier suitable for registration.\n * Any of DOI / PMID / arXiv ID / Scopus ID / ERIC ID qualifies;\n * only articles with none of these are counted as noId and skipped.\n */\nfunction hasIdentifier(article: Article): boolean {\n return !!(\n article.pmid ||\n article.doi ||\n article.arxivId ||\n article.scopusId ||\n article.ericId\n );\n}\n\n/**\n * Register articles with reference-manager using bulk CSL-JSON import.\n *\n * Flow:\n * 1. Filter out articles without identifiers (noId)\n * 2. Convert remaining articles to CSL-JSON array\n * 3. Write to temporary file in sessionDir\n * 4. Call refAddBulk() once\n * 5. Map output to RegistrationRecord\n * 6. Clean up temporary file\n */\nexport async function registerArticles(\n articles: Article[],\n options: RegisterOptions\n): Promise<RegistrationRecord> {\n const { sessionId, sessionDir, withAbstracts, noAttachFulltext, onProgress, onAttachProgress } = options;\n const libraryPath = path.join(sessionDir, 'references.json');\n\n if (withAbstracts) {\n console.warn(\n 'Note: abstracts are now always included in bulk import. --with-abstracts flag is no longer needed.'\n );\n }\n\n const record: RegistrationRecord = {\n sessionId,\n timestamp: new Date().toISOString(),\n summary: {\n total: articles.length,\n added: 0,\n skipped: 0,\n failed: 0,\n noId: 0,\n },\n added: [],\n duplicates: [],\n failed: [],\n };\n\n // Separate articles with and without identifiers\n const articlesWithId: Article[] = [];\n for (const article of articles) {\n if (hasIdentifier(article)) {\n articlesWithId.push(article);\n } else {\n record.summary.noId++;\n }\n }\n\n // Report progress: counting phase\n if (onProgress) {\n onProgress(articlesWithId.length, articles.length);\n }\n\n // If no articles have identifiers, return early\n if (articlesWithId.length === 0) {\n return record;\n }\n\n // Convert to CSL-JSON\n const cslJsonItems = articlesToCslJson(articlesWithId);\n const tempFilePath = path.join(sessionDir, BULK_IMPORT_FILE);\n\n try {\n // Write CSL-JSON to temporary file\n await fs.writeFile(tempFilePath, JSON.stringify(cslJsonItems));\n\n // Bulk import\n const output = await refAddBulk(tempFilePath, { libraryPath });\n\n // Aggregate summary\n record.summary.added = output.summary.added;\n record.summary.skipped = output.summary.skipped;\n record.summary.failed = output.summary.failed;\n\n // Record added items\n for (const item of output.added) {\n record.added.push({\n source: item.source,\n id: item.id,\n title: item.title,\n });\n }\n\n // Record duplicates\n for (const item of output.skipped) {\n record.duplicates.push({\n source: item.source,\n existingId: item.existingId,\n duplicateType: item.duplicateType,\n });\n }\n\n // Record failures\n for (const item of output.failed) {\n record.failed.push({\n source: item.source,\n reason: item.reason,\n error: item.error,\n });\n }\n } catch (error) {\n // Handle bulk import execution errors\n record.summary.failed = articlesWithId.length;\n record.failed.push({\n source: 'bulk_import',\n reason: 'execution_error',\n error: error instanceof Error ? error.message : 'Unknown error',\n });\n } finally {\n // Clean up temporary file\n await fs.unlink(tempFilePath).catch(() => {});\n }\n\n // Fulltext attach step (after successful import)\n if (!noAttachFulltext) {\n const addedRefs = record.added.map((item) => ({\n id: item.id,\n source: item.source,\n }));\n // Also include duplicates (already in library) — they may need fulltext attachment\n const dupRefs = record.duplicates.map((item) => ({\n id: item.existingId,\n source: item.source,\n }));\n const allRefs = [...addedRefs, ...dupRefs];\n\n if (allRefs.length > 0) {\n const attachOptions = {\n sessionDir,\n libraryPath,\n addedRefs: allRefs,\n ...(onAttachProgress ? { onProgress: onAttachProgress } : {}),\n };\n const fulltextResult = await attachFulltexts(attachOptions);\n\n record.fulltext = fulltextResult;\n }\n }\n\n return record;\n}\n"],"names":[],"mappings":";;;;;;AAaA,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AAKzB,eAAsB,uBACpB,YACA,QACe;AACf,QAAM,GAAG,MAAM,YAAY,EAAE,WAAW,MAAM;AAC9C,QAAM,WAAW,KAAK,KAAK,YAAY,iBAAiB;AACxD,QAAM,GAAG,UAAU,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC9D;AA2CA,SAAS,cAAc,SAA2B;AAChD,SAAO,CAAC,EACN,QAAQ,QACR,QAAQ,OACR,QAAQ,WACR,QAAQ,YACR,QAAQ;AAEZ;AAaA,eAAsB,iBACpB,UACA,SAC6B;AAC7B,QAAM,EAAE,WAAW,YAAY,eAAe,kBAAkB,YAAY,qBAAqB;AACjG,QAAM,cAAc,KAAK,KAAK,YAAY,iBAAiB;AAE3D,MAAI,eAAe;AACjB,YAAQ;AAAA,MACN;AAAA,IAAA;AAAA,EAEJ;AAEA,QAAM,SAA6B;AAAA,IACjC;AAAA,IACA,YAAW,oBAAI,KAAA,GAAO,YAAA;AAAA,IACtB,SAAS;AAAA,MACP,OAAO,SAAS;AAAA,MAChB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,MAAM;AAAA,IAAA;AAAA,IAER,OAAO,CAAA;AAAA,IACP,YAAY,CAAA;AAAA,IACZ,QAAQ,CAAA;AAAA,EAAC;AAIX,QAAM,iBAA4B,CAAA;AAClC,aAAW,WAAW,UAAU;AAC9B,QAAI,cAAc,OAAO,GAAG;AAC1B,qBAAe,KAAK,OAAO;AAAA,IAC7B,OAAO;AACL,aAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AAGA,MAAI,YAAY;AACd,eAAW,eAAe,QAAQ,SAAS,MAAM;AAAA,EACnD;AAGA,MAAI,eAAe,WAAW,GAAG;AAC/B,WAAO;AAAA,EACT;AAGA,QAAM,eAAe,kBAAkB,cAAc;AACrD,QAAM,eAAe,KAAK,KAAK,YAAY,gBAAgB;AAE3D,MAAI;AAEF,UAAM,GAAG,UAAU,cAAc,KAAK,UAAU,YAAY,CAAC;AAG7D,UAAM,SAAS,MAAM,WAAW,cAAc,EAAE,aAAa;AAG7D,WAAO,QAAQ,QAAQ,OAAO,QAAQ;AACtC,WAAO,QAAQ,UAAU,OAAO,QAAQ;AACxC,WAAO,QAAQ,SAAS,OAAO,QAAQ;AAGvC,eAAW,QAAQ,OAAO,OAAO;AAC/B,aAAO,MAAM,KAAK;AAAA,QAChB,QAAQ,KAAK;AAAA,QACb,IAAI,KAAK;AAAA,QACT,OAAO,KAAK;AAAA,MAAA,CACb;AAAA,IACH;AAGA,eAAW,QAAQ,OAAO,SAAS;AACjC,aAAO,WAAW,KAAK;AAAA,QACrB,QAAQ,KAAK;AAAA,QACb,YAAY,KAAK;AAAA,QACjB,eAAe,KAAK;AAAA,MAAA,CACrB;AAAA,IACH;AAGA,eAAW,QAAQ,OAAO,QAAQ;AAChC,aAAO,OAAO,KAAK;AAAA,QACjB,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,QACb,OAAO,KAAK;AAAA,MAAA,CACb;AAAA,IACH;AAAA,EACF,SAAS,OAAO;AAEd,WAAO,QAAQ,SAAS,eAAe;AACvC,WAAO,OAAO,KAAK;AAAA,MACjB,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAAA,CACjD;AAAA,EACH,UAAA;AAEE,UAAM,GAAG,OAAO,YAAY,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC9C;AAGA,MAAI,CAAC,kBAAkB;AACrB,UAAM,YAAY,OAAO,MAAM,IAAI,CAAC,UAAU;AAAA,MAC5C,IAAI,KAAK;AAAA,MACT,QAAQ,KAAK;AAAA,IAAA,EACb;AAEF,UAAM,UAAU,OAAO,WAAW,IAAI,CAAC,UAAU;AAAA,MAC/C,IAAI,KAAK;AAAA,MACT,QAAQ,KAAK;AAAA,IAAA,EACb;AACF,UAAM,UAAU,CAAC,GAAG,WAAW,GAAG,OAAO;AAEzC,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,gBAAgB;AAAA,QACpB;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,GAAI,mBAAmB,EAAE,YAAY,qBAAqB,CAAA;AAAA,MAAC;AAE7D,YAAM,iBAAiB,MAAM,gBAAgB,aAAa;AAE1D,aAAO,WAAW;AAAA,IACpB;AAAA,EACF;AAEA,SAAO;AACT;"}
|
|
@@ -724,6 +724,29 @@ function parseSection(secChildren, level) {
|
|
|
724
724
|
}
|
|
725
725
|
return { title, level, content, subsections };
|
|
726
726
|
}
|
|
727
|
+
function flushBlockBuffer(buffer, sections) {
|
|
728
|
+
const content = parseBlockContent(buffer);
|
|
729
|
+
if (content.length > 0) {
|
|
730
|
+
sections.push({ title: "", level: 2, content, subsections: [] });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function parseMixedBodyChildren(children, sections) {
|
|
734
|
+
let blockBuffer = [];
|
|
735
|
+
for (const child of children) {
|
|
736
|
+
if (getTagName(child) === "sec") {
|
|
737
|
+
if (blockBuffer.length > 0) {
|
|
738
|
+
flushBlockBuffer(blockBuffer, sections);
|
|
739
|
+
blockBuffer = [];
|
|
740
|
+
}
|
|
741
|
+
sections.push(parseSection(getChildren(child), 2));
|
|
742
|
+
} else {
|
|
743
|
+
blockBuffer.push(child);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (blockBuffer.length > 0) {
|
|
747
|
+
flushBlockBuffer(blockBuffer, sections);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
727
750
|
function parseJatsBody(xml) {
|
|
728
751
|
const parsed = parser.parse(xml);
|
|
729
752
|
const article = findArticle(parsed);
|
|
@@ -735,14 +758,9 @@ function parseJatsBody(xml) {
|
|
|
735
758
|
const sections = [];
|
|
736
759
|
const secs = findChildren(body.children, "sec");
|
|
737
760
|
if (secs.length > 0) {
|
|
738
|
-
|
|
739
|
-
sections.push(parseSection(sec.children, 2));
|
|
740
|
-
}
|
|
761
|
+
parseMixedBodyChildren(body.children, sections);
|
|
741
762
|
} else {
|
|
742
|
-
|
|
743
|
-
if (content.length > 0) {
|
|
744
|
-
sections.push({ title: "", level: 2, content, subsections: [] });
|
|
745
|
-
}
|
|
763
|
+
flushBlockBuffer(body.children, sections);
|
|
746
764
|
}
|
|
747
765
|
return sections;
|
|
748
766
|
}
|