1 | 'use strict';
|
2 |
|
3 | Object.defineProperty(exports, '__esModule', { value: true });
|
4 |
|
5 | var vegaUtil = require('vega-util');
|
6 | var d3Dsv = require('d3-dsv');
|
7 | var topojsonClient = require('topojson-client');
|
8 | var vegaFormat = require('vega-format');
|
9 |
|
10 |
|
11 |
|
12 | const protocol_re = /^([A-Za-z]+:)?\/\//;
|
13 |
|
14 |
|
15 | const allowed_re = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
16 | const whitespace_re = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g;
|
17 |
|
18 |
|
19 |
|
20 | const fileProtocol = 'file://';
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | function loaderFactory(fetch, fs) {
|
34 | return options => ({
|
35 | options: options || {},
|
36 | sanitize: sanitize,
|
37 | load: load,
|
38 | fileAccess: !!fs,
|
39 | file: fileLoader(fs),
|
40 | http: httpLoader(fetch)
|
41 | });
|
42 | }
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | async function load(uri, options) {
|
55 | const opt = await this.sanitize(uri, options),
|
56 | url = opt.href;
|
57 |
|
58 | return opt.localFile
|
59 | ? this.file(url)
|
60 | : this.http(url, options);
|
61 | }
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | async function sanitize(uri, options) {
|
74 | options = vegaUtil.extend({}, this.options, options);
|
75 |
|
76 | const fileAccess = this.fileAccess,
|
77 | result = {href: null};
|
78 |
|
79 | let isFile, loadFile, base;
|
80 |
|
81 | const isAllowed = allowed_re.test(uri.replace(whitespace_re, ''));
|
82 |
|
83 | if (uri == null || typeof uri !== 'string' || !isAllowed) {
|
84 | vegaUtil.error('Sanitize failure, invalid URI: ' + vegaUtil.stringValue(uri));
|
85 | }
|
86 |
|
87 | const hasProtocol = protocol_re.test(uri);
|
88 |
|
89 |
|
90 | if ((base = options.baseURL) && !hasProtocol) {
|
91 |
|
92 | if (!uri.startsWith('/') && base[base.length-1] !== '/') {
|
93 | uri = '/' + uri;
|
94 | }
|
95 | uri = base + uri;
|
96 | }
|
97 |
|
98 |
|
99 | loadFile = (isFile = uri.startsWith(fileProtocol))
|
100 | || options.mode === 'file'
|
101 | || options.mode !== 'http' && !hasProtocol && fileAccess;
|
102 |
|
103 | if (isFile) {
|
104 |
|
105 | uri = uri.slice(fileProtocol.length);
|
106 | } else if (uri.startsWith('//')) {
|
107 | if (options.defaultProtocol === 'file') {
|
108 |
|
109 | uri = uri.slice(2);
|
110 | loadFile = true;
|
111 | } else {
|
112 |
|
113 | uri = (options.defaultProtocol || 'http') + ':' + uri;
|
114 | }
|
115 | }
|
116 |
|
117 |
|
118 | Object.defineProperty(result, 'localFile', {value: !!loadFile});
|
119 |
|
120 |
|
121 | result.href = uri;
|
122 |
|
123 |
|
124 | if (options.target) {
|
125 | result.target = options.target + '';
|
126 | }
|
127 |
|
128 |
|
129 | if (options.rel) {
|
130 | result.rel = options.rel + '';
|
131 | }
|
132 |
|
133 |
|
134 |
|
135 | if (options.context === 'image' && options.crossOrigin) {
|
136 | result.crossOrigin = options.crossOrigin + '';
|
137 | }
|
138 |
|
139 |
|
140 | return result;
|
141 | }
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | function fileLoader(fs) {
|
152 | return fs
|
153 | ? filename => new Promise((accept, reject) => {
|
154 | fs.readFile(filename, (error, data) => {
|
155 | if (error) reject(error);
|
156 | else accept(data);
|
157 | });
|
158 | })
|
159 | : fileReject;
|
160 | }
|
161 |
|
162 |
|
163 |
|
164 |
|
165 | async function fileReject() {
|
166 | vegaUtil.error('No file system access.');
|
167 | }
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | function httpLoader(fetch) {
|
178 | return fetch
|
179 | ? async function(url, options) {
|
180 | const opt = vegaUtil.extend({}, this.options.http, options),
|
181 | type = options && options.response,
|
182 | response = await fetch(url, opt);
|
183 |
|
184 | return !response.ok
|
185 | ? vegaUtil.error(response.status + '' + response.statusText)
|
186 | : vegaUtil.isFunction(response[type]) ? response[type]()
|
187 | : response.text();
|
188 | }
|
189 | : httpReject;
|
190 | }
|
191 |
|
192 |
|
193 |
|
194 |
|
195 | async function httpReject() {
|
196 | vegaUtil.error('No HTTP fetch method available.');
|
197 | }
|
198 |
|
199 | const isValid = _ => _ != null && _ === _;
|
200 |
|
201 | const isBoolean = _ => _ === 'true'
|
202 | || _ === 'false'
|
203 | || _ === true
|
204 | || _ === false;
|
205 |
|
206 | const isDate = _ => !Number.isNaN(Date.parse(_));
|
207 |
|
208 | const isNumber = _ => !Number.isNaN(+_) && !(_ instanceof Date);
|
209 |
|
210 | const isInteger = _ => isNumber(_) && Number.isInteger(+_);
|
211 |
|
212 | const typeParsers = {
|
213 | boolean: vegaUtil.toBoolean,
|
214 | integer: vegaUtil.toNumber,
|
215 | number: vegaUtil.toNumber,
|
216 | date: vegaUtil.toDate,
|
217 | string: vegaUtil.toString,
|
218 | unknown: vegaUtil.identity
|
219 | };
|
220 |
|
221 | const typeTests = [
|
222 | isBoolean,
|
223 | isInteger,
|
224 | isNumber,
|
225 | isDate
|
226 | ];
|
227 |
|
228 | const typeList = [
|
229 | 'boolean',
|
230 | 'integer',
|
231 | 'number',
|
232 | 'date'
|
233 | ];
|
234 |
|
235 | function inferType(values, field) {
|
236 | if (!values || !values.length) return 'unknown';
|
237 |
|
238 | const n = values.length,
|
239 | m = typeTests.length,
|
240 | a = typeTests.map((_, i) => i + 1);
|
241 |
|
242 | for (let i = 0, t = 0, j, value; i < n; ++i) {
|
243 | value = field ? values[i][field] : values[i];
|
244 | for (j = 0; j < m; ++j) {
|
245 | if (a[j] && isValid(value) && !typeTests[j](value)) {
|
246 | a[j] = 0;
|
247 | ++t;
|
248 | if (t === typeTests.length) return 'string';
|
249 | }
|
250 | }
|
251 | }
|
252 |
|
253 | return typeList[
|
254 | a.reduce((u, v) => u === 0 ? v : u, 0) - 1
|
255 | ];
|
256 | }
|
257 |
|
258 | function inferTypes(data, fields) {
|
259 | return fields.reduce((types, field) => {
|
260 | types[field] = inferType(data, field);
|
261 | return types;
|
262 | }, {});
|
263 | }
|
264 |
|
265 | function delimitedFormat(delimiter) {
|
266 | const parse = function(data, format) {
|
267 | const delim = {delimiter: delimiter};
|
268 | return dsv(data, format ? vegaUtil.extend(format, delim) : delim);
|
269 | };
|
270 |
|
271 | parse.responseType = 'text';
|
272 |
|
273 | return parse;
|
274 | }
|
275 |
|
276 | function dsv(data, format) {
|
277 | if (format.header) {
|
278 | data = format.header
|
279 | .map(vegaUtil.stringValue)
|
280 | .join(format.delimiter) + '\n' + data;
|
281 | }
|
282 | return d3Dsv.dsvFormat(format.delimiter).parse(data + '');
|
283 | }
|
284 |
|
285 | dsv.responseType = 'text';
|
286 |
|
287 | function isBuffer(_) {
|
288 | return (typeof Buffer === 'function' && vegaUtil.isFunction(Buffer.isBuffer))
|
289 | ? Buffer.isBuffer(_) : false;
|
290 | }
|
291 |
|
292 | function json(data, format) {
|
293 | const prop = (format && format.property) ? vegaUtil.field(format.property) : vegaUtil.identity;
|
294 | return vegaUtil.isObject(data) && !isBuffer(data)
|
295 | ? parseJSON(prop(data))
|
296 | : prop(JSON.parse(data));
|
297 | }
|
298 |
|
299 | json.responseType = 'json';
|
300 |
|
301 | function parseJSON(data, format) {
|
302 | return (format && format.copy)
|
303 | ? JSON.parse(JSON.stringify(data))
|
304 | : data;
|
305 | }
|
306 |
|
307 | const filters = {
|
308 | interior: (a, b) => a !== b,
|
309 | exterior: (a, b) => a === b
|
310 | };
|
311 |
|
312 | function topojson(data, format) {
|
313 | let method, object, property, filter;
|
314 | data = json(data, format);
|
315 |
|
316 | if (format && format.feature) {
|
317 | method = topojsonClient.feature;
|
318 | property = format.feature;
|
319 | } else if (format && format.mesh) {
|
320 | method = topojsonClient.mesh;
|
321 | property = format.mesh;
|
322 | filter = filters[format.filter];
|
323 | } else {
|
324 | vegaUtil.error('Missing TopoJSON feature or mesh parameter.');
|
325 | }
|
326 |
|
327 | object = (object = data.objects[property])
|
328 | ? method(data, object, filter)
|
329 | : vegaUtil.error('Invalid TopoJSON object: ' + property);
|
330 |
|
331 | return object && object.features || [object];
|
332 | }
|
333 |
|
334 | topojson.responseType = 'json';
|
335 |
|
336 | const format = {
|
337 | dsv: dsv,
|
338 | csv: delimitedFormat(','),
|
339 | tsv: delimitedFormat('\t'),
|
340 | json: json,
|
341 | topojson: topojson
|
342 | };
|
343 |
|
344 | function formats(name, reader) {
|
345 | if (arguments.length > 1) {
|
346 | format[name] = reader;
|
347 | return this;
|
348 | } else {
|
349 | return vegaUtil.hasOwnProperty(format, name) ? format[name] : null;
|
350 | }
|
351 | }
|
352 |
|
353 | function responseType(type) {
|
354 | const f = formats(type);
|
355 | return f && f.responseType || 'text';
|
356 | }
|
357 |
|
358 | function read(data, schema, timeParser, utcParser) {
|
359 | schema = schema || {};
|
360 |
|
361 | const reader = formats(schema.type || 'json');
|
362 | if (!reader) vegaUtil.error('Unknown data format type: ' + schema.type);
|
363 |
|
364 | data = reader(data, schema);
|
365 | if (schema.parse) parse(data, schema.parse, timeParser, utcParser);
|
366 |
|
367 | if (vegaUtil.hasOwnProperty(data, 'columns')) delete data.columns;
|
368 | return data;
|
369 | }
|
370 |
|
371 | function parse(data, types, timeParser, utcParser) {
|
372 | if (!data.length) return;
|
373 |
|
374 | const locale = vegaFormat.timeFormatDefaultLocale();
|
375 | timeParser = timeParser || locale.timeParse;
|
376 | utcParser = utcParser || locale.utcParse;
|
377 |
|
378 | let fields = data.columns || Object.keys(data[0]),
|
379 | datum, field, i, j, n, m;
|
380 |
|
381 | if (types === 'auto') types = inferTypes(data, fields);
|
382 |
|
383 | fields = Object.keys(types);
|
384 | const parsers = fields.map(field => {
|
385 | const type = types[field];
|
386 | let parts, pattern;
|
387 |
|
388 | if (type && (type.startsWith('date:') || type.startsWith('utc:'))) {
|
389 | parts = type.split(/:(.+)?/, 2);
|
390 | pattern = parts[1];
|
391 |
|
392 | if ((pattern[0] === '\'' && pattern[pattern.length-1] === '\'') ||
|
393 | (pattern[0] === '"' && pattern[pattern.length-1] === '"')) {
|
394 | pattern = pattern.slice(1, -1);
|
395 | }
|
396 |
|
397 | const parse = parts[0] === 'utc' ? utcParser : timeParser;
|
398 | return parse(pattern);
|
399 | }
|
400 |
|
401 | if (!typeParsers[type]) {
|
402 | throw Error('Illegal format pattern: ' + field + ':' + type);
|
403 | }
|
404 |
|
405 | return typeParsers[type];
|
406 | });
|
407 |
|
408 | for (i=0, n=data.length, m=fields.length; i<n; ++i) {
|
409 | datum = data[i];
|
410 | for (j=0; j<m; ++j) {
|
411 | field = fields[j];
|
412 | datum[field] = parsers[j](datum[field]);
|
413 | }
|
414 | }
|
415 | }
|
416 |
|
417 | var loader = loaderFactory(
|
418 | require('node-fetch'),
|
419 | require('fs')
|
420 | );
|
421 |
|
422 | exports.format = format;
|
423 | exports.formats = formats;
|
424 | exports.inferType = inferType;
|
425 | exports.inferTypes = inferTypes;
|
426 | exports.loader = loader;
|
427 | exports.read = read;
|
428 | exports.responseType = responseType;
|
429 | exports.typeParsers = typeParsers;
|