@jbrowse/launch-args 5.0.0-beta.16 → 5.0.0-beta.18
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/esm/comparative.d.ts +62 -0
- package/esm/comparative.js +187 -0
- package/esm/displaySettings.d.ts +160 -0
- package/esm/displaySettings.js +686 -0
- package/esm/displaySlots.generated.d.ts +2 -0
- package/esm/displaySlots.generated.js +23 -0
- package/esm/fileAdapter.d.ts +2 -0
- package/esm/fileAdapter.js +5 -0
- package/esm/fileInput.d.ts +35 -16
- package/esm/fileInput.js +96 -30
- package/esm/hostedGenomes.d.ts +39 -0
- package/esm/hostedGenomes.js +45 -0
- package/esm/index.d.ts +8 -4
- package/esm/index.js +8 -4
- package/esm/launchPlan.d.ts +73 -9
- package/esm/launchPlan.js +156 -66
- package/esm/modifierValues.d.ts +10 -0
- package/esm/modifierValues.js +47 -0
- package/esm/parseArgv.d.ts +35 -5
- package/esm/parseArgv.js +56 -23
- package/esm/slotPath.d.ts +48 -0
- package/esm/slotPath.js +211 -0
- package/package.json +7 -4
package/esm/launchPlan.js
CHANGED
|
@@ -1,30 +1,75 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { buildComparative, comparativeRole, hasComparativeArgs, isSyntenyFlag, syntenyFlags, } from './comparative.js';
|
|
3
|
+
import { checkModifiers } from './displaySettings.js';
|
|
4
|
+
import { fileTrack, genomeAssembly, genomeFlags, isGenomeFlag, isTrackFlag, trackFlags, } from './fileInput.js';
|
|
3
5
|
import { genomeConfigUrl } from './genome.js';
|
|
4
|
-
import { FILE_FLAG,
|
|
6
|
+
import { FILE_FLAG, isUrl, locValue, splitModifier } from './parseArgv.js';
|
|
7
|
+
import { viewSettings } from './slotPath.js';
|
|
8
|
+
/**
|
|
9
|
+
* The views jb2export's subcommand words name, which Desktop takes as its
|
|
10
|
+
* first argument too.
|
|
11
|
+
*/
|
|
12
|
+
export const launchViewTypes = {
|
|
13
|
+
lgv: 'LinearGenomeView',
|
|
14
|
+
synteny: 'LinearSyntenyView',
|
|
15
|
+
dotplot: 'DotplotView',
|
|
16
|
+
};
|
|
17
|
+
export function launchViewType(word) {
|
|
18
|
+
return Object.hasOwn(launchViewTypes, word)
|
|
19
|
+
? launchViewTypes[word]
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
// the app applies modifiers once the track is open, so a bad one fails here
|
|
23
|
+
function showTrack(trackId, modifiers) {
|
|
24
|
+
try {
|
|
25
|
+
checkModifiers(modifiers);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
throw new Error(`${trackId}: ${e instanceof Error ? e.message : e}`, {
|
|
29
|
+
cause: e,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return { trackId, modifiers };
|
|
33
|
+
}
|
|
5
34
|
export const launchFlags = new Set([
|
|
6
35
|
'genome',
|
|
7
36
|
'config',
|
|
8
37
|
'assembly',
|
|
9
38
|
'loc',
|
|
39
|
+
'view',
|
|
10
40
|
'track',
|
|
11
|
-
|
|
12
|
-
'aliases',
|
|
13
|
-
'cytobands',
|
|
41
|
+
...genomeFlags,
|
|
14
42
|
FILE_FLAG,
|
|
15
43
|
...trackFlags,
|
|
44
|
+
...syntenyFlags,
|
|
45
|
+
]);
|
|
46
|
+
/**
|
|
47
|
+
* jb2export's flags that take only true or false, so a file after one is a
|
|
48
|
+
* file of its own. Desktop reads them to report one without taking the file.
|
|
49
|
+
*/
|
|
50
|
+
export const exportBooleanFlags = new Set([
|
|
51
|
+
'noRasterize',
|
|
52
|
+
'defaultSession',
|
|
53
|
+
'showGridlines',
|
|
54
|
+
'refseq',
|
|
55
|
+
'passOnly',
|
|
56
|
+
'resume',
|
|
57
|
+
'manifest',
|
|
58
|
+
'dryRun',
|
|
16
59
|
]);
|
|
17
|
-
// consumed while building the config, so they reach the view as config
|
|
18
|
-
const BUILD_MODIFIERS = new Set(['name', 'index', 'aliases', 'cytobands']);
|
|
19
60
|
const PATH_MODIFIERS = new Set(['index', 'aliases', 'cytobands']);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return key === FILE_FLAG || trackFlags.includes(key);
|
|
61
|
+
function opensTrack(key) {
|
|
62
|
+
return key === FILE_FLAG || isTrackFlag(key);
|
|
23
63
|
}
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
/** Whether a flag opens a data file, as a track, genome or comparison flag does. */
|
|
65
|
+
export function takesFile(key) {
|
|
66
|
+
return opensTrack(key) || isGenomeFlag(key) || isSyntenyFlag(key);
|
|
26
67
|
}
|
|
27
|
-
|
|
68
|
+
/**
|
|
69
|
+
* `base`, or the first of `base-2`, `base-3`, ... that `used` lacks, which it
|
|
70
|
+
* adds to `used`.
|
|
71
|
+
*/
|
|
72
|
+
export function uniqueId(used, base) {
|
|
28
73
|
let id = base;
|
|
29
74
|
for (let i = 2; used.has(id); i++) {
|
|
30
75
|
id = `${base}-${i}`;
|
|
@@ -32,37 +77,26 @@ function uniqueId(used, base) {
|
|
|
32
77
|
used.add(id);
|
|
33
78
|
return id;
|
|
34
79
|
}
|
|
80
|
+
function entryText([key, values]) {
|
|
81
|
+
return [...(key === FILE_FLAG ? [] : [`--${key}`]), ...values].join(' ');
|
|
82
|
+
}
|
|
35
83
|
/**
|
|
36
84
|
* The plan a command line describes, with every local path resolved against
|
|
37
85
|
* `cwd`: the directory the command ran in, which is not the app's own once a
|
|
38
86
|
* second launch hands its arguments to a running one.
|
|
87
|
+
*
|
|
88
|
+
* `viewType` is the view a subcommand word named. Without one, a second genome
|
|
89
|
+
* or a comparison file opens a synteny view, and anything else a linear one.
|
|
39
90
|
*/
|
|
40
|
-
export function launchPlan(entries, cwd) {
|
|
41
|
-
const abs = (file) =>
|
|
42
|
-
const absMod = (mod) =>
|
|
43
|
-
|
|
44
|
-
: mod;
|
|
45
|
-
const one = (key) => {
|
|
46
|
-
const given = entries.filter(([k]) => k === key);
|
|
47
|
-
if (given.length > 1) {
|
|
48
|
-
throw new Error(`--${key} is given more than once`);
|
|
49
|
-
}
|
|
50
|
-
return given[0]?.[1][0];
|
|
91
|
+
export function launchPlan(entries, cwd, viewType) {
|
|
92
|
+
const abs = (file) => (isUrl(file) ? file : path.resolve(cwd, file));
|
|
93
|
+
const absMod = (mod) => {
|
|
94
|
+
const [name, value] = splitModifier(mod) ?? [];
|
|
95
|
+
return name && PATH_MODIFIERS.has(name) ? `${name}:${abs(value)}` : mod;
|
|
51
96
|
};
|
|
52
|
-
const genome = one('genome');
|
|
53
|
-
const config = one('config');
|
|
54
|
-
const fasta = one('fasta');
|
|
55
|
-
if (genome && config) {
|
|
56
|
-
throw new Error('pass --genome or --config, not both');
|
|
57
|
-
}
|
|
58
|
-
const configRef = genome
|
|
59
|
-
? genomeConfigUrl(genome)
|
|
60
|
-
: config
|
|
61
|
-
? abs(config)
|
|
62
|
-
: undefined;
|
|
63
97
|
const resolved = entries.map(([key, values]) => {
|
|
64
98
|
const [file, ...mods] = values;
|
|
65
|
-
if (!
|
|
99
|
+
if (!takesFile(key)) {
|
|
66
100
|
return [key, values];
|
|
67
101
|
}
|
|
68
102
|
else if (!file) {
|
|
@@ -72,12 +106,76 @@ export function launchPlan(entries, cwd) {
|
|
|
72
106
|
return [key, [abs(file), ...mods.map(absMod)]];
|
|
73
107
|
}
|
|
74
108
|
});
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
const type = viewType ??
|
|
110
|
+
(hasComparativeArgs(resolved) ? 'LinearSyntenyView' : 'LinearGenomeView');
|
|
111
|
+
return type === 'LinearGenomeView'
|
|
112
|
+
? linearPlan(resolved, abs)
|
|
113
|
+
: comparativePlan(resolved, type);
|
|
114
|
+
}
|
|
115
|
+
function givenOnce(entries, key) {
|
|
116
|
+
const found = entries.filter(([k]) => k === key);
|
|
117
|
+
if (found.length > 1) {
|
|
118
|
+
throw new Error(`--${key} is given more than once`);
|
|
119
|
+
}
|
|
120
|
+
return found[0]?.[1];
|
|
121
|
+
}
|
|
122
|
+
const VIEW_NAMES = {
|
|
123
|
+
LinearSyntenyView: 'A synteny view',
|
|
124
|
+
DotplotView: 'A dotplot',
|
|
125
|
+
};
|
|
126
|
+
function comparativePlan(entries, type) {
|
|
127
|
+
const viewName = VIEW_NAMES[type];
|
|
128
|
+
if (entries.some(([key]) => key === 'config')) {
|
|
129
|
+
throw new Error(`${viewName} opens one row per genome, and --config names a config; pass --genome names or FASTA, 2bit or .chrom.sizes files`);
|
|
130
|
+
}
|
|
131
|
+
const data = buildComparative(entries);
|
|
132
|
+
const count = data.rows.length;
|
|
133
|
+
if (type === 'DotplotView' ? count !== 2 : count < 2) {
|
|
134
|
+
throw new Error(`${viewName} compares ${type === 'DotplotView' ? 'two genomes' : 'two genomes or more'}, and this command line names ${count}`);
|
|
135
|
+
}
|
|
136
|
+
const used = new Set();
|
|
137
|
+
for (const track of data.syntenyTracks) {
|
|
138
|
+
track.trackId = uniqueId(used, track.trackId);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
type,
|
|
142
|
+
assemblies: data.assemblies,
|
|
143
|
+
hosted: data.hosted.map(name => ({ name, config: genomeConfigUrl(name) })),
|
|
144
|
+
views: data.rows,
|
|
145
|
+
tracks: data.syntenyTracks,
|
|
146
|
+
view: viewSettings(givenOnce(entries, 'view') ?? []),
|
|
147
|
+
unapplied: entries
|
|
148
|
+
.filter(entry => !comparativeRole(entry) && entry[0] !== 'view')
|
|
149
|
+
.map(entryText),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function linearPlan(resolved, abs) {
|
|
153
|
+
const given = (key) => givenOnce(resolved, key);
|
|
154
|
+
const one = (key) => given(key)?.[0];
|
|
155
|
+
const genome = one('genome');
|
|
156
|
+
const config = one('config');
|
|
157
|
+
if (genome && config) {
|
|
158
|
+
throw new Error('pass --genome or --config, not both');
|
|
159
|
+
}
|
|
160
|
+
else if (config && isUrl(config) && !/^https?:\/\//i.test(config)) {
|
|
161
|
+
throw new Error(`--config ${config}: JBrowse fetches a config over http or https, or reads one from a file`);
|
|
162
|
+
}
|
|
163
|
+
const configRef = genome
|
|
164
|
+
? genomeConfigUrl(genome)
|
|
165
|
+
: config
|
|
166
|
+
? abs(config)
|
|
167
|
+
: undefined;
|
|
168
|
+
const roles = resolved.map(comparativeRole);
|
|
169
|
+
const comparison = resolved.find((_, i) => roles[i] === 'synteny');
|
|
170
|
+
const [genomeFile, otherGenome] = resolved.filter(([key], i) => roles[i] === 'genome' && key !== 'genome');
|
|
79
171
|
const genomePath = genomeFile?.[1][0];
|
|
80
|
-
if (
|
|
172
|
+
if (comparison) {
|
|
173
|
+
throw new Error(`${comparison[1][0]} compares two genomes, which a linear view does not show; open it in a synteny view`);
|
|
174
|
+
}
|
|
175
|
+
else if (otherGenome) {
|
|
176
|
+
throw new Error(`${otherGenome[1][0]} is a genome, and this view already has one; open one genome per view`);
|
|
177
|
+
}
|
|
178
|
+
else if (genomePath && configRef) {
|
|
81
179
|
throw new Error(`${genomePath} is a genome, and ${genome ? '--genome' : '--config'} names one too; pass one or the other`);
|
|
82
180
|
}
|
|
83
181
|
else if (!genomePath && !configRef) {
|
|
@@ -86,50 +184,42 @@ export function launchPlan(entries, cwd) {
|
|
|
86
184
|
else if (genomePath && one('assembly')) {
|
|
87
185
|
throw new Error(`--assembly picks among the genomes of --genome or --config; ${genomePath} is its own`);
|
|
88
186
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const aliases = one('aliases') ?? modifierValue(mods, 'aliases');
|
|
93
|
-
const cytobands = one('cytobands') ?? modifierValue(mods, 'cytobands');
|
|
94
|
-
const adapter = fileAdapter(genomePath, {
|
|
95
|
-
adapterType: genomeFile[0] === 'fasta' && !isDataFile(genomePath)
|
|
96
|
-
? 'IndexedFastaAdapter'
|
|
97
|
-
: undefined,
|
|
98
|
-
});
|
|
99
|
-
assembly = sequenceAssembly(adapter, genomePath, {
|
|
100
|
-
aliases: aliases && abs(aliases),
|
|
101
|
-
cytobands: cytobands && abs(cytobands),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
187
|
+
const built = genomeFile && genomeAssembly(genomeFile[0], genomeFile[1]);
|
|
188
|
+
const assembly = built?.assembly;
|
|
189
|
+
const unapplied = built?.unused.map(mod => `${built.assembly.name} ${mod}`) ?? [];
|
|
104
190
|
const assemblyName = assembly?.name ?? one('assembly');
|
|
105
191
|
const used = new Set();
|
|
106
192
|
const tracks = [];
|
|
107
193
|
const show = [];
|
|
108
|
-
const unapplied = [];
|
|
109
194
|
for (const entry of resolved) {
|
|
110
195
|
const [key, [first, ...mods]] = entry;
|
|
111
|
-
if (key === '
|
|
196
|
+
if (entry === genomeFile || key === 'loc' || key === 'view') {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
else if (key === 'track') {
|
|
112
200
|
if (!first) {
|
|
113
201
|
throw new Error('--track requires a trackId');
|
|
114
202
|
}
|
|
115
|
-
show.push(first);
|
|
116
|
-
unapplied.push(...mods.map(mod => `${first} ${mod}`));
|
|
203
|
+
show.push(showTrack(first, mods));
|
|
117
204
|
}
|
|
118
|
-
else if (
|
|
205
|
+
else if (opensTrack(key)) {
|
|
119
206
|
const track = fileTrack(key, entry[1], assemblyName);
|
|
120
207
|
track.trackId = uniqueId(used, track.trackId);
|
|
121
208
|
tracks.push(track);
|
|
122
|
-
show.push(track.trackId);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
209
|
+
show.push(showTrack(track.trackId, mods));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
unapplied.push(...mods.map(extra => `--${key} ${extra}`));
|
|
126
213
|
}
|
|
127
214
|
}
|
|
215
|
+
const loc = given('loc');
|
|
128
216
|
return {
|
|
217
|
+
type: 'LinearGenomeView',
|
|
129
218
|
config: configRef,
|
|
130
219
|
assembly,
|
|
131
220
|
assemblyName,
|
|
132
|
-
loc:
|
|
221
|
+
loc: loc && locValue(loc),
|
|
222
|
+
view: viewSettings(given('view') ?? []),
|
|
133
223
|
tracks,
|
|
134
224
|
show,
|
|
135
225
|
unapplied,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function invalidModifierValue(prefix: string, val: string, expected: string): never;
|
|
2
|
+
export declare function parseNum(prefix: string, val: string, expected?: string): number;
|
|
3
|
+
export declare function parseStr(prefix: string, val: string, expected?: string): string;
|
|
4
|
+
export declare function parseEnum<T extends string>(prefix: string, val: string, allowed: readonly T[]): T;
|
|
5
|
+
export declare function parseBool(prefix: string, val: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* A JSON modifier, reported with its token, since a bare SyntaxError from a
|
|
8
|
+
* shell-mangled brace names nothing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseJsonModifier(token: string): Record<string, unknown>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// One rule for every modifier value, so the grammar reads the same whatever the
|
|
2
|
+
// track type: a value that isn't one of the things the modifier accepts is an
|
|
3
|
+
// error. jb2export writes a figure and exits, so a warning about a typo scrolls
|
|
4
|
+
// past and leaves a wrong image behind: `arcs:upp` used to render a plot with no
|
|
5
|
+
// arcs at all.
|
|
6
|
+
export function invalidModifierValue(prefix, val, expected) {
|
|
7
|
+
throw new Error(val === ''
|
|
8
|
+
? `Missing ${prefix} value. Expected ${prefix}:<${expected}>.`
|
|
9
|
+
: `Invalid ${prefix} value "${val}". Expected ${expected}.`);
|
|
10
|
+
}
|
|
11
|
+
// A bare `height:` would otherwise become a silent 0 via `+''`. `expected`
|
|
12
|
+
// names the wider grammar for the modifiers that also accept keywords.
|
|
13
|
+
export function parseNum(prefix, val, expected = 'a number') {
|
|
14
|
+
const n = val.trim() === '' ? Number.NaN : Number(val);
|
|
15
|
+
return Number.isFinite(n) ? n : invalidModifierValue(prefix, val, expected);
|
|
16
|
+
}
|
|
17
|
+
// A modifier whose value is mandatory and free-form (a color, a tag, a display
|
|
18
|
+
// name): `color:` with nothing after the colon is a typo.
|
|
19
|
+
export function parseStr(prefix, val, expected = 'value') {
|
|
20
|
+
return val || invalidModifierValue(prefix, val, expected);
|
|
21
|
+
}
|
|
22
|
+
// A value from a fixed set, returned as the matched member so the caller keeps
|
|
23
|
+
// the narrow literal type.
|
|
24
|
+
export function parseEnum(prefix, val, allowed) {
|
|
25
|
+
return (allowed.find(a => a === val) ??
|
|
26
|
+
invalidModifierValue(prefix, val, allowed.join(', ')));
|
|
27
|
+
}
|
|
28
|
+
// Bare (`force`) or `:true` is on, `:false` is off, anything else is a typo.
|
|
29
|
+
export function parseBool(prefix, val) {
|
|
30
|
+
return val === '' || val === 'true'
|
|
31
|
+
? true
|
|
32
|
+
: val === 'false'
|
|
33
|
+
? false
|
|
34
|
+
: invalidModifierValue(prefix, val, 'true or false');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A JSON modifier, reported with its token, since a bare SyntaxError from a
|
|
38
|
+
* shell-mangled brace names nothing.
|
|
39
|
+
*/
|
|
40
|
+
export function parseJsonModifier(token) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(token);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new Error(`Invalid JSON track option: ${token}`, { cause: e });
|
|
46
|
+
}
|
|
47
|
+
}
|
package/esm/parseArgv.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type Entry = [string, string[]];
|
|
|
4
4
|
* reads.bam`, a file typed by its name.
|
|
5
5
|
*/
|
|
6
6
|
export declare const FILE_FLAG = "file";
|
|
7
|
+
/** Whether a command-line file is a URL (http, s3, ftp, …) rather than a path. */
|
|
8
|
+
export declare function isUrl(file: string): boolean;
|
|
7
9
|
/**
|
|
8
10
|
* A `name:value` or `path=value` modifier, or a JSON one. A URL and a Windows
|
|
9
11
|
* path carry a colon too, which is why the name before it has to be more than
|
|
@@ -22,10 +24,11 @@ export declare function isDataFile(token: string): boolean;
|
|
|
22
24
|
* `value`. A bare `-` is stdin, so it is a value.
|
|
23
25
|
* - The token after a flag is its value, except after a flag in `booleans`,
|
|
24
26
|
* which takes only `true` or `false`.
|
|
25
|
-
* - Later tokens are the entry's modifiers (`height:100`,
|
|
26
|
-
* `{"json":1}`), until a token that names a
|
|
27
|
-
* {@link FILE_FLAG} entry of its own. So
|
|
28
|
-
* both, and `reads.bam height:100` gives
|
|
27
|
+
* - Later tokens are the entry's modifiers (`height:100`,
|
|
28
|
+
* `encoding.color.field=tags.HP`, `{"json":1}`), until a token that names a
|
|
29
|
+
* data file, which opens a {@link FILE_FLAG} entry of its own. So
|
|
30
|
+
* `--loc chr1:1-100 reads.bam` keeps both, and `reads.bam height:100` gives
|
|
31
|
+
* the file its modifier.
|
|
29
32
|
* - A bare token before any flag that is not a data file throws; it used to be
|
|
30
33
|
* dropped without a word.
|
|
31
34
|
*/
|
|
@@ -33,8 +36,35 @@ export declare function parseArgv(rawArgv: string[], { booleans }?: {
|
|
|
33
36
|
booleans?: ReadonlySet<string>;
|
|
34
37
|
}): Entry[];
|
|
35
38
|
export declare function splitModifier(token: string): [string, string] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The value of a `key:value` modifier in `tokens`. The last one given wins, as
|
|
41
|
+
* with the display modifiers, so a script overrides a shared list by
|
|
42
|
+
* appending to it.
|
|
43
|
+
*/
|
|
36
44
|
export declare function modifierValue(tokens: string[], key: string): string | undefined;
|
|
37
|
-
|
|
45
|
+
/** `--loc`'s tokens, which together name the regions of one view. */
|
|
46
|
+
export declare function locValue(values: string[]): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Words a flag's value is followed by, read off as one error: a flag that
|
|
49
|
+
* takes one value has no use for them.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extraValuesMessage(flag: string, extra: string[]): string;
|
|
52
|
+
/**
|
|
53
|
+
* The command line as option values, with the track entries apart, in order.
|
|
54
|
+
*
|
|
55
|
+
* - `--loc` takes all its tokens, so `--loc chr1:1-100 chr2:1-100` names the
|
|
56
|
+
* regions a quoted `--loc` does.
|
|
57
|
+
* - A flag in `repeatable` may repeat and so has no single value here; the
|
|
58
|
+
* caller reads it from the entries.
|
|
59
|
+
* - A flag in `withModifiers` takes modifiers after its value, which the
|
|
60
|
+
* caller reads from the entries too.
|
|
61
|
+
* - Any other flag given twice, or followed by a token it has no use for,
|
|
62
|
+
* throws.
|
|
63
|
+
*/
|
|
64
|
+
export declare function standardizeArgv(args: Entry[], trackTypes: string[], { repeatable, withModifiers, }?: {
|
|
65
|
+
repeatable?: string[];
|
|
66
|
+
withModifiers?: string[];
|
|
67
|
+
}): {
|
|
38
68
|
trackList: Entry[];
|
|
39
69
|
[key: string]: unknown;
|
|
40
70
|
};
|
package/esm/parseArgv.js
CHANGED
|
@@ -5,6 +5,10 @@ import { fileNameOf, matchFormat } from '@jbrowse/add-track-core';
|
|
|
5
5
|
*/
|
|
6
6
|
export const FILE_FLAG = 'file';
|
|
7
7
|
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
8
|
+
/** Whether a command-line file is a URL (http, s3, ftp, …) rather than a path. */
|
|
9
|
+
export function isUrl(file) {
|
|
10
|
+
return URL_SCHEME.test(file);
|
|
11
|
+
}
|
|
8
12
|
const DRIVE_LETTER = /^[a-z]:[\\/]/i;
|
|
9
13
|
const MODIFIER = /^[a-z_][\w.-]*[:=]/i;
|
|
10
14
|
/**
|
|
@@ -14,9 +18,7 @@ const MODIFIER = /^[a-z_][\w.-]*[:=]/i;
|
|
|
14
18
|
*/
|
|
15
19
|
export function isModifier(token) {
|
|
16
20
|
return (token.startsWith('{') ||
|
|
17
|
-
(MODIFIER.test(token) &&
|
|
18
|
-
!URL_SCHEME.test(token) &&
|
|
19
|
-
!DRIVE_LETTER.test(token)));
|
|
21
|
+
(MODIFIER.test(token) && !isUrl(token) && !DRIVE_LETTER.test(token)));
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* A file the shared format table recognizes by name, as a local path or a URL:
|
|
@@ -33,10 +35,11 @@ export function isDataFile(token) {
|
|
|
33
35
|
* `value`. A bare `-` is stdin, so it is a value.
|
|
34
36
|
* - The token after a flag is its value, except after a flag in `booleans`,
|
|
35
37
|
* which takes only `true` or `false`.
|
|
36
|
-
* - Later tokens are the entry's modifiers (`height:100`,
|
|
37
|
-
* `{"json":1}`), until a token that names a
|
|
38
|
-
* {@link FILE_FLAG} entry of its own. So
|
|
39
|
-
* both, and `reads.bam height:100` gives
|
|
38
|
+
* - Later tokens are the entry's modifiers (`height:100`,
|
|
39
|
+
* `encoding.color.field=tags.HP`, `{"json":1}`), until a token that names a
|
|
40
|
+
* data file, which opens a {@link FILE_FLAG} entry of its own. So
|
|
41
|
+
* `--loc chr1:1-100 reads.bam` keeps both, and `reads.bam height:100` gives
|
|
42
|
+
* the file its modifier.
|
|
40
43
|
* - A bare token before any flag that is not a data file throws; it used to be
|
|
41
44
|
* dropped without a word.
|
|
42
45
|
*/
|
|
@@ -73,35 +76,65 @@ export function parseArgv(rawArgv, { booleans = new Set() } = {}) {
|
|
|
73
76
|
}
|
|
74
77
|
return entries;
|
|
75
78
|
}
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// undefined for a token with no colon or a leading colon. This is the
|
|
79
|
-
// colon-preserving grammar the assembly-flag (`loc:`/`aliases:`) and track-file
|
|
80
|
-
// (`index:`/`name:`) modifiers use; the display modifiers instead split every
|
|
81
|
-
// colon (`color:tag:XS`) directly where they're parsed.
|
|
79
|
+
// `key:value` split at the first colon, so `aliases:https://x/a.txt` keeps its
|
|
80
|
+
// URL. The display modifiers split every colon instead (`color:tag:XS`).
|
|
82
81
|
export function splitModifier(token) {
|
|
83
82
|
const i = token.indexOf(':');
|
|
84
83
|
return i > 0 ? [token.slice(0, i), token.slice(i + 1)] : undefined;
|
|
85
84
|
}
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The value of a `key:value` modifier in `tokens`. The last one given wins, as
|
|
87
|
+
* with the display modifiers, so a script overrides a shared list by
|
|
88
|
+
* appending to it.
|
|
89
|
+
*/
|
|
87
90
|
export function modifierValue(tokens, key) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
return undefined;
|
|
91
|
+
return tokens.map(splitModifier).findLast(split => split?.[0] === key)?.[1];
|
|
92
|
+
}
|
|
93
|
+
/** `--loc`'s tokens, which together name the regions of one view. */
|
|
94
|
+
export function locValue(values) {
|
|
95
|
+
return values.join(' ').trim() || undefined;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Words a flag's value is followed by, read off as one error: a flag that
|
|
99
|
+
* takes one value has no use for them.
|
|
100
|
+
*/
|
|
101
|
+
export function extraValuesMessage(flag, extra) {
|
|
102
|
+
return `--${flag} takes one value, so ${extra.map(e => `"${e}"`).join(' ')} ${extra.length > 1 ? 'apply' : 'applies'} to nothing`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The command line as option values, with the track entries apart, in order.
|
|
106
|
+
*
|
|
107
|
+
* - `--loc` takes all its tokens, so `--loc chr1:1-100 chr2:1-100` names the
|
|
108
|
+
* regions a quoted `--loc` does.
|
|
109
|
+
* - A flag in `repeatable` may repeat and so has no single value here; the
|
|
110
|
+
* caller reads it from the entries.
|
|
111
|
+
* - A flag in `withModifiers` takes modifiers after its value, which the
|
|
112
|
+
* caller reads from the entries too.
|
|
113
|
+
* - Any other flag given twice, or followed by a token it has no use for,
|
|
114
|
+
* throws.
|
|
115
|
+
*/
|
|
116
|
+
export function standardizeArgv(args, trackTypes, { repeatable = ['track'], withModifiers = ['track', 'view', 'fasta', 'chromSizes'], } = {}) {
|
|
97
117
|
const trackList = [];
|
|
98
118
|
const rest = {};
|
|
99
119
|
for (const [key, vals] of args) {
|
|
100
120
|
if (key === FILE_FLAG || trackTypes.includes(key)) {
|
|
101
121
|
trackList.push([key, vals]);
|
|
102
122
|
}
|
|
123
|
+
else if (repeatable.includes(key)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
else if (Object.hasOwn(rest, key)) {
|
|
127
|
+
throw new Error(`--${key} is given more than once`);
|
|
128
|
+
}
|
|
129
|
+
else if (key === 'loc') {
|
|
130
|
+
rest[key] = locValue(vals) ?? true;
|
|
131
|
+
}
|
|
103
132
|
else {
|
|
104
|
-
|
|
133
|
+
const [value, ...extra] = vals;
|
|
134
|
+
if (extra.length && !withModifiers.includes(key)) {
|
|
135
|
+
throw new Error(extraValuesMessage(key, extra));
|
|
136
|
+
}
|
|
137
|
+
rest[key] = value ?? true;
|
|
105
138
|
}
|
|
106
139
|
}
|
|
107
140
|
return { trackList, ...rest };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value of a `path=value` modifier: `true`/`false`, a number, a
|
|
3
|
+
* comma-separated list (a trailing comma makes a list of one, and a `jexl:`
|
|
4
|
+
* item keeps the commas inside its own brackets and quotes), or the text as
|
|
5
|
+
* written.
|
|
6
|
+
*/
|
|
7
|
+
export declare function slotValue(text: string): string | number | boolean | (string | number | boolean)[];
|
|
8
|
+
/** Whether a track option is a `path=value` slot write rather than a named modifier. */
|
|
9
|
+
export declare function isSlotPathOption(opt: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a slot write names a member of a setting, `encoding.color.field=…`,
|
|
12
|
+
* rather than a whole setting, `height=…`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isMemberWrite(opt: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Merge `patch` into `target`, object into object and anything else replacing
|
|
17
|
+
* what was there, so `encoding.color.field=…` and
|
|
18
|
+
* `encoding.color.scale.range=…` fill one object in either order.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeSettings(target: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
21
|
+
/** A `path=value` modifier read apart: the segments it walks, and what it sets. */
|
|
22
|
+
export interface SlotWrite {
|
|
23
|
+
opt: string;
|
|
24
|
+
segments: string[];
|
|
25
|
+
value: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `encoding.color.scale.range=tan,teal` and `marks.0.encoding.y=score`, read
|
|
29
|
+
* apart.
|
|
30
|
+
*/
|
|
31
|
+
export declare function slotWrite(opt: string): SlotWrite;
|
|
32
|
+
/**
|
|
33
|
+
* Apply one `path=value` write to `target`, creating the objects and lists the
|
|
34
|
+
* path walks through and leaving everything beside them alone, so the writes of
|
|
35
|
+
* one command line compose whatever order they come in.
|
|
36
|
+
*
|
|
37
|
+
* Indexing a list is what carries the grammar: `marks`, the `transform` steps
|
|
38
|
+
* under a mark and an aggregate's `ops` are all lists of objects, and while a
|
|
39
|
+
* path stopped at the first of them a declared figure could only be written as
|
|
40
|
+
* raw JSON.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applySlotWrite(target: Record<string, unknown>, write: SlotWrite): Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* The settings `--view`'s tokens write onto the view, as a session spec's view
|
|
45
|
+
* object holds them: `mark.interpolate=bump`, `filter.0.field=length`, or a
|
|
46
|
+
* JSON object merged in whole.
|
|
47
|
+
*/
|
|
48
|
+
export declare function viewSettings(tokens: string[]): Record<string, unknown>;
|