@ncukondo/search-hub 0.23.0 → 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/query/init.d.ts +0 -1
- package/dist/cli/commands/query/init.d.ts.map +1 -1
- package/dist/cli/commands/query/init.js +2 -3
- package/dist/cli/commands/query/init.js.map +1 -1
- package/dist/cli/commands/query/resolve.d.ts.map +1 -1
- package/dist/cli/commands/query/resolve.js +5 -2
- package/dist/cli/commands/query/resolve.js.map +1 -1
- 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 +12 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/suggestions/rules.js +1 -1
- package/dist/cli/suggestions/rules.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/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/paths.d.ts +1 -1
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +6 -2
- package/dist/config/paths.js.map +1 -1
- package/dist/index.js +2 -2
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateReviewNextSteps, computeBatchContinuation } from "../commands/review/next-steps.js";
|
|
2
2
|
const queryInitRule = (ctx) => {
|
|
3
3
|
if (ctx.command !== "query init") return null;
|
|
4
|
-
const file = ctx.outputFile ?? "queries/query.yaml";
|
|
4
|
+
const file = ctx.outputFile ?? ".search-hub/queries/query.yaml";
|
|
5
5
|
return {
|
|
6
6
|
next: [
|
|
7
7
|
{ command: `$EDITOR ${file}`, description: "Edit your query" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? 'queries/query.yaml';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit your query' },\n { command: `search-hub query validate ${file}`, description: 'Validate query' },\n { command: `search-hub search ${file} --count-only`, description: 'Check hit counts' },\n ],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init \"<title>\"', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init \"<title>\"',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.previousSessionId) {\n seeAlso.push({\n command: `search-hub diff ${ctx.previousSessionId} ${sid}`,\n description: 'Compare with previous',\n });\n } else if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit query to refine' },\n { command: `search-hub search ${file} --count-only`, description: 'Re-check counts' },\n { command: `search-hub query assess ${file} --verdict refine`, description: 'Record assessment' },\n ],\n seeAlso: [\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init \"<title>\"', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\nconst relatedRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'related') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View related articles' },\n { command: `search-hub review init ${sid}`, description: 'Screen related articles' },\n { command: `search-hub export ${sid}`, description: 'Export results' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n relatedRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? '.search-hub/queries/query.yaml';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit your query' },\n { command: `search-hub query validate ${file}`, description: 'Validate query' },\n { command: `search-hub search ${file} --count-only`, description: 'Check hit counts' },\n ],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init \"<title>\"', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init \"<title>\"',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.previousSessionId) {\n seeAlso.push({\n command: `search-hub diff ${ctx.previousSessionId} ${sid}`,\n description: 'Compare with previous',\n });\n } else if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit query to refine' },\n { command: `search-hub search ${file} --count-only`, description: 'Re-check counts' },\n { command: `search-hub query assess ${file} --verdict refine`, description: 'Record assessment' },\n ],\n seeAlso: [\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init \"<title>\"', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\nconst relatedRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'related') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View related articles' },\n { command: `search-hub review init ${sid}`, description: 'Screen related articles' },\n { command: `search-hub export ${sid}`, description: 'Export results' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n relatedRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
export { loadConfig, saveConfig, type LoadConfigOptions, type SaveConfigOptions } from './loader';
|
|
7
7
|
export { ConfigSchema, type Config, type ProviderConfig } from './schema';
|
|
8
8
|
export { getDefaultConfig, DEFAULT_CONFIG } from './defaults';
|
|
9
|
-
export { getProjectDir, getLocalConfigPath, getLocalSessionsDir,
|
|
9
|
+
export { getProjectDir, getLocalConfigPath, getLocalSessionsDir, getQueriesDir, isInsideProject, } from './paths';
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,eAAe,GAChB,MAAM,SAAS,CAAC"}
|
package/dist/config/paths.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare function getLocalSessionsDir(baseDir?: string): string;
|
|
|
36
36
|
/**
|
|
37
37
|
* Get the local queries directory (.search-hub/queries/).
|
|
38
38
|
*/
|
|
39
|
-
export declare function
|
|
39
|
+
export declare function getQueriesDir(baseDir?: string): string;
|
|
40
40
|
/**
|
|
41
41
|
* Check if the given directory (default: cwd) contains a .search-hub/ project directory.
|
|
42
42
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAKD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAKD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOxE"}
|
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
|
}
|
|
@@ -21,7 +24,7 @@ function getLocalConfigPath(baseDir) {
|
|
|
21
24
|
function getLocalSessionsDir(baseDir) {
|
|
22
25
|
return join(getProjectDir(baseDir), "sessions");
|
|
23
26
|
}
|
|
24
|
-
function
|
|
27
|
+
function getQueriesDir(baseDir) {
|
|
25
28
|
return join(getProjectDir(baseDir), "queries");
|
|
26
29
|
}
|
|
27
30
|
async function isInsideProject(baseDir) {
|
|
@@ -34,12 +37,13 @@ async function isInsideProject(baseDir) {
|
|
|
34
37
|
}
|
|
35
38
|
export {
|
|
36
39
|
getConfigDir,
|
|
40
|
+
getDataDir,
|
|
37
41
|
getDefaultConfigPath,
|
|
38
42
|
getDefaultSessionsDir,
|
|
39
43
|
getLocalConfigPath,
|
|
40
|
-
getLocalQueriesDir,
|
|
41
44
|
getLocalSessionsDir,
|
|
42
45
|
getProjectDir,
|
|
46
|
+
getQueriesDir,
|
|
43
47
|
isInsideProject
|
|
44
48
|
};
|
|
45
49
|
//# sourceMappingURL=paths.js.map
|
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
|
|
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;"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { extractControlledVocabTerms, validateControlledVocab } from "./query/vo
|
|
|
6
6
|
import { loadConfig, saveConfig } from "./config/loader.js";
|
|
7
7
|
import { ConfigSchema } from "./config/schema.js";
|
|
8
8
|
import { DEFAULT_CONFIG, getDefaultConfig } from "./config/defaults.js";
|
|
9
|
-
import { getLocalConfigPath,
|
|
9
|
+
import { getLocalConfigPath, getLocalSessionsDir, getProjectDir, getQueriesDir, isInsideProject } from "./config/paths.js";
|
|
10
10
|
import { createSession, generateSessionId, getResumableProviders, listSessions, loadSession, sanitizeName, saveSession, sessionExists, updateDatabaseStatus, updateSessionStatus } from "./session/manager.js";
|
|
11
11
|
import { SessionLogger } from "./session/logger.js";
|
|
12
12
|
import { createProviderError, isAuthError, isProviderError, isRateLimitError } from "./providers/base/types.js";
|
|
@@ -35,9 +35,9 @@ export {
|
|
|
35
35
|
generateSessionId,
|
|
36
36
|
getDefaultConfig,
|
|
37
37
|
getLocalConfigPath,
|
|
38
|
-
getLocalQueriesDir,
|
|
39
38
|
getLocalSessionsDir,
|
|
40
39
|
getProjectDir,
|
|
40
|
+
getQueriesDir,
|
|
41
41
|
getResumableProviders,
|
|
42
42
|
globalRegistry,
|
|
43
43
|
isAuthError,
|
|
@@ -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":"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"}
|
|
@@ -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
|
}
|