@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/slotPath.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const UNSAFE_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
2
|
+
function isPlainObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function scalar(text) {
|
|
6
|
+
if (text === 'true' || text === 'false') {
|
|
7
|
+
return text === 'true';
|
|
8
|
+
}
|
|
9
|
+
const n = Number(text);
|
|
10
|
+
return text.trim() !== '' && Number.isFinite(n) ? n : text;
|
|
11
|
+
}
|
|
12
|
+
const LOCATION_SO_FAR = /^[^\s,:]+:[\d,-]*\d$/;
|
|
13
|
+
// The comma at `i` groups a location's digits, as the app writes one:
|
|
14
|
+
// `chr2:135,787,850-135,876,467`.
|
|
15
|
+
function groupsDigits(text, start, i) {
|
|
16
|
+
return (LOCATION_SO_FAR.test(text.slice(start, i)) &&
|
|
17
|
+
/^\d{3}(?!\d)/.test(text.slice(i + 1)));
|
|
18
|
+
}
|
|
19
|
+
// A `jexl:` item runs to the first comma outside its own brackets and quotes,
|
|
20
|
+
// since a jexl call separates its arguments with commas, and a location keeps
|
|
21
|
+
// the commas grouping its digits.
|
|
22
|
+
function listItems(text) {
|
|
23
|
+
const items = [];
|
|
24
|
+
let start = 0;
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let quote;
|
|
27
|
+
for (let i = 0; i < text.length; i++) {
|
|
28
|
+
const c = text.charAt(i);
|
|
29
|
+
if (quote) {
|
|
30
|
+
if (c === '\\') {
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
else if (c === quote) {
|
|
34
|
+
quote = undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (c === ',' && depth === 0 && !groupsDigits(text, start, i)) {
|
|
38
|
+
items.push(text.slice(start, i));
|
|
39
|
+
start = i + 1;
|
|
40
|
+
}
|
|
41
|
+
else if (text.startsWith('jexl:', start)) {
|
|
42
|
+
if (c === "'" || c === '"') {
|
|
43
|
+
quote = c;
|
|
44
|
+
}
|
|
45
|
+
else if ('([{'.includes(c)) {
|
|
46
|
+
depth++;
|
|
47
|
+
}
|
|
48
|
+
else if (')]}'.includes(c)) {
|
|
49
|
+
depth--;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
items.push(text.slice(start));
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The value of a `path=value` modifier: `true`/`false`, a number, a
|
|
58
|
+
* comma-separated list (a trailing comma makes a list of one, and a `jexl:`
|
|
59
|
+
* item keeps the commas inside its own brackets and quotes), or the text as
|
|
60
|
+
* written.
|
|
61
|
+
*/
|
|
62
|
+
export function slotValue(text) {
|
|
63
|
+
const items = listItems(text);
|
|
64
|
+
return items.length > 1
|
|
65
|
+
? items.filter(item => item !== '').map(scalar)
|
|
66
|
+
: scalar(text);
|
|
67
|
+
}
|
|
68
|
+
/** Whether a track option is a `path=value` slot write rather than a named modifier. */
|
|
69
|
+
export function isSlotPathOption(opt) {
|
|
70
|
+
return /^[A-Za-z_][\w.]*=/.test(opt);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a slot write names a member of a setting, `encoding.color.field=…`,
|
|
74
|
+
* rather than a whole setting, `height=…`.
|
|
75
|
+
*/
|
|
76
|
+
export function isMemberWrite(opt) {
|
|
77
|
+
return isSlotPathOption(opt) && opt.slice(0, opt.indexOf('=')).includes('.');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Merge `patch` into `target`, object into object and anything else replacing
|
|
81
|
+
* what was there, so `encoding.color.field=…` and
|
|
82
|
+
* `encoding.color.scale.range=…` fill one object in either order.
|
|
83
|
+
*/
|
|
84
|
+
export function mergeSettings(target, patch) {
|
|
85
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
86
|
+
if (UNSAFE_SEGMENTS.has(key)) {
|
|
87
|
+
throw new Error(`Invalid track option key "${key}"`);
|
|
88
|
+
}
|
|
89
|
+
const existing = target[key];
|
|
90
|
+
if (isPlainObject(value) && isPlainObject(existing)) {
|
|
91
|
+
mergeSettings(existing, value);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
target[key] = isPlainObject(value) ? mergeSettings({}, value) : value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return target;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* `encoding.color.scale.range=tan,teal` and `marks.0.encoding.y=score`, read
|
|
101
|
+
* apart.
|
|
102
|
+
*/
|
|
103
|
+
export function slotWrite(opt) {
|
|
104
|
+
const eq = opt.indexOf('=');
|
|
105
|
+
const segments = opt.slice(0, eq).split('.');
|
|
106
|
+
if (segments.includes('')) {
|
|
107
|
+
throw new Error(`Invalid track option "${opt}": an empty path segment`);
|
|
108
|
+
}
|
|
109
|
+
for (const segment of segments) {
|
|
110
|
+
if (UNSAFE_SEGMENTS.has(segment)) {
|
|
111
|
+
throw new Error(`Invalid track option key "${segment}"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { opt, segments, value: slotValue(opt.slice(eq + 1)) };
|
|
115
|
+
}
|
|
116
|
+
// A segment of digits indexes a list, anything else names a member.
|
|
117
|
+
function indexOf(segment) {
|
|
118
|
+
return /^\d+$/.test(segment) ? Number(segment) : undefined;
|
|
119
|
+
}
|
|
120
|
+
// Where a segment lands: a key on an object, an index on a list. An index past
|
|
121
|
+
// the end would leave a hole no schema accepts, so it names the entry that has
|
|
122
|
+
// to be written first instead.
|
|
123
|
+
function slotAt(container, segment, { opt }) {
|
|
124
|
+
if (!Array.isArray(container)) {
|
|
125
|
+
return segment;
|
|
126
|
+
}
|
|
127
|
+
const i = indexOf(segment);
|
|
128
|
+
if (i === undefined) {
|
|
129
|
+
throw new Error(`Invalid track option "${opt}": "${segment}" names a member of a list; use an index`);
|
|
130
|
+
}
|
|
131
|
+
if (i > container.length) {
|
|
132
|
+
throw new Error(`Invalid track option "${opt}": index ${i} is past the end of a list of ${container.length}; write index ${container.length} first`);
|
|
133
|
+
}
|
|
134
|
+
return i;
|
|
135
|
+
}
|
|
136
|
+
function readSlot(container, at) {
|
|
137
|
+
return Array.isArray(container)
|
|
138
|
+
? container[at]
|
|
139
|
+
: container[at];
|
|
140
|
+
}
|
|
141
|
+
function setSlot(container, at, value) {
|
|
142
|
+
if (Array.isArray(container)) {
|
|
143
|
+
container[at] = value;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
container[at] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// The container to walk into under `segment`, made to the shape the NEXT
|
|
150
|
+
// segment asks for. A scalar there is replaced — a path keeps the rest of a
|
|
151
|
+
// setting, and a scalar has no rest to keep — but a list where the path wants
|
|
152
|
+
// an object, or the reverse, is two writes disagreeing about one setting, and
|
|
153
|
+
// carrying on would drop whichever came first.
|
|
154
|
+
function containerUnder(container, at, wantsList, { opt }) {
|
|
155
|
+
const existing = readSlot(container, at);
|
|
156
|
+
if (wantsList ? Array.isArray(existing) : isPlainObject(existing)) {
|
|
157
|
+
return existing;
|
|
158
|
+
}
|
|
159
|
+
if (isPlainObject(existing) || Array.isArray(existing)) {
|
|
160
|
+
throw new Error(`Invalid track option "${opt}": "${at}" is ${wantsList ? 'a setting, not a list' : 'a list, not a setting'}`);
|
|
161
|
+
}
|
|
162
|
+
const made = wantsList ? [] : {};
|
|
163
|
+
setSlot(container, at, made);
|
|
164
|
+
return made;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Apply one `path=value` write to `target`, creating the objects and lists the
|
|
168
|
+
* path walks through and leaving everything beside them alone, so the writes of
|
|
169
|
+
* one command line compose whatever order they come in.
|
|
170
|
+
*
|
|
171
|
+
* Indexing a list is what carries the grammar: `marks`, the `transform` steps
|
|
172
|
+
* under a mark and an aggregate's `ops` are all lists of objects, and while a
|
|
173
|
+
* path stopped at the first of them a declared figure could only be written as
|
|
174
|
+
* raw JSON.
|
|
175
|
+
*/
|
|
176
|
+
export function applySlotWrite(target, write) {
|
|
177
|
+
const { segments, value } = write;
|
|
178
|
+
let container = target;
|
|
179
|
+
for (const [i, segment] of segments.slice(0, -1).entries()) {
|
|
180
|
+
container = containerUnder(container, slotAt(container, segment, write), indexOf(segments[i + 1]) !== undefined, write);
|
|
181
|
+
}
|
|
182
|
+
setSlot(container, slotAt(container, segments.at(-1), write), value);
|
|
183
|
+
return target;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The settings `--view`'s tokens write onto the view, as a session spec's view
|
|
187
|
+
* object holds them: `mark.interpolate=bump`, `filter.0.field=length`, or a
|
|
188
|
+
* JSON object merged in whole.
|
|
189
|
+
*/
|
|
190
|
+
export function viewSettings(tokens) {
|
|
191
|
+
const settings = {};
|
|
192
|
+
for (const token of tokens) {
|
|
193
|
+
if (token.startsWith('{')) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(token);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
throw new Error(`--view ${token} is not valid JSON`, { cause: e });
|
|
200
|
+
}
|
|
201
|
+
mergeSettings(settings, parsed);
|
|
202
|
+
}
|
|
203
|
+
else if (isSlotPathOption(token)) {
|
|
204
|
+
applySlotWrite(settings, slotWrite(token));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
throw new Error(`--view "${token}" is not a setting: write one as path=value (mark.interpolate=bump) or as a JSON object`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return settings;
|
|
211
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/launch-args",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The command-line grammar jbrowse-img, jb2capture and JBrowse Desktop share: data files, a genome, a location and tracks, turned into JBrowse config",
|
|
6
6
|
"keywords": [
|
|
@@ -21,16 +21,19 @@
|
|
|
21
21
|
"types": "esm/index.d.ts",
|
|
22
22
|
"exports": {
|
|
23
23
|
".": "./esm/index.js",
|
|
24
|
-
"./trackIds": "./esm/trackIds.js"
|
|
24
|
+
"./trackIds": "./esm/trackIds.js",
|
|
25
|
+
"./hostedGenomes": "./esm/hostedGenomes.js",
|
|
26
|
+
"./displaySettings": "./esm/displaySettings.js"
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
27
29
|
"esm"
|
|
28
30
|
],
|
|
29
31
|
"dependencies": {
|
|
30
|
-
"@jbrowse/add-track-core": "^5.0.0-beta.
|
|
32
|
+
"@jbrowse/add-track-core": "^5.0.0-beta.18",
|
|
33
|
+
"@jbrowse/cigar-utils": "^5.0.0-beta.18"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
36
|
+
"@jbrowse/core": "^5.0.0-beta.18"
|
|
34
37
|
},
|
|
35
38
|
"publishConfig": {
|
|
36
39
|
"access": "public"
|