@jbrowse/launch-args 5.0.0-beta.17 → 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.
@@ -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.17",
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.17"
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.17"
36
+ "@jbrowse/core": "^5.0.0-beta.18"
34
37
  },
35
38
  "publishConfig": {
36
39
  "access": "public"