UNPKG

12.2 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var vegaUtil = require('vega-util');
6var d3Dsv = require('d3-dsv');
7var topojsonClient = require('topojson-client');
8var vegaFormat = require('vega-format');
9
10// Matches absolute URLs with optional protocol
11// https://... file://... //...
12const protocol_re = /^([A-Za-z]+:)?\/\//;
13
14// Matches allowed URIs. From https://github.com/cure53/DOMPurify/blob/master/src/regexp.js with added file://
15const allowed_re = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|file|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
16const whitespace_re = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
17
18
19// Special treatment in node.js for the file: protocol
20const fileProtocol = 'file://';
21
22/**
23 * Factory for a loader constructor that provides methods for requesting
24 * files from either the network or disk, and for sanitizing request URIs.
25 * @param {function} fetch - The Fetch API for HTTP network requests.
26 * If null or undefined, HTTP loading will be disabled.
27 * @param {object} fs - The file system interface for file loading.
28 * If null or undefined, local file loading will be disabled.
29 * @return {function} A loader constructor with the following signature:
30 * param {object} [options] - Optional default loading options to use.
31 * return {object} - A new loader instance.
32 */
33function 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 * Load an external resource, typically either from the web or from the local
46 * filesystem. This function uses {@link sanitize} to first sanitize the uri,
47 * then calls either {@link http} (for web requests) or {@link file} (for
48 * filesystem loading).
49 * @param {string} uri - The resource indicator (e.g., URL or filename).
50 * @param {object} [options] - Optional loading options. These options will
51 * override any existing default options.
52 * @return {Promise} - A promise that resolves to the loaded content.
53 */
54async 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 * URI sanitizer function.
65 * @param {string} uri - The uri (url or filename) to sanity check.
66 * @param {object} options - An options hash.
67 * @return {Promise} - A promise that resolves to an object containing
68 * sanitized uri data, or rejects it the input uri is deemed invalid.
69 * The properties of the resolved object are assumed to be
70 * valid attributes for an HTML 'a' tag. The sanitized uri *must* be
71 * provided by the 'href' property of the returned object.
72 */
73async 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 // if relative url (no protocol/host), prepend baseURL
90 if ((base = options.baseURL) && !hasProtocol) {
91 // Ensure that there is a slash between the baseURL (e.g. hostname) and url
92 if (!uri.startsWith('/') && base[base.length-1] !== '/') {
93 uri = '/' + uri;
94 }
95 uri = base + uri;
96 }
97
98 // should we load from file system?
99 loadFile = (isFile = uri.startsWith(fileProtocol))
100 || options.mode === 'file'
101 || options.mode !== 'http' && !hasProtocol && fileAccess;
102
103 if (isFile) {
104 // strip file protocol
105 uri = uri.slice(fileProtocol.length);
106 } else if (uri.startsWith('//')) {
107 if (options.defaultProtocol === 'file') {
108 // if is file, strip protocol and set loadFile flag
109 uri = uri.slice(2);
110 loadFile = true;
111 } else {
112 // if relative protocol (starts with '//'), prepend default protocol
113 uri = (options.defaultProtocol || 'http') + ':' + uri;
114 }
115 }
116
117 // set non-enumerable mode flag to indicate local file load
118 Object.defineProperty(result, 'localFile', {value: !!loadFile});
119
120 // set uri
121 result.href = uri;
122
123 // set default result target, if specified
124 if (options.target) {
125 result.target = options.target + '';
126 }
127
128 // set default result rel, if specified (#1542)
129 if (options.rel) {
130 result.rel = options.rel + '';
131 }
132
133 // provide control over cross-origin image handling (#2238)
134 // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
135 if (options.context === 'image' && options.crossOrigin) {
136 result.crossOrigin = options.crossOrigin + '';
137 }
138
139 // return
140 return result;
141}
142
143/**
144 * File system loader factory.
145 * @param {object} fs - The file system interface.
146 * @return {function} - A file loader with the following signature:
147 * param {string} filename - The file system path to load.
148 * param {string} filename - The file system path to load.
149 * return {Promise} A promise that resolves to the file contents.
150 */
151function 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 * Default file system loader that simply rejects.
164 */
165async function fileReject() {
166 vegaUtil.error('No file system access.');
167}
168
169/**
170 * HTTP request handler factory.
171 * @param {function} fetch - The Fetch API method.
172 * @return {function} - An http loader with the following signature:
173 * param {string} url - The url to request.
174 * param {object} options - An options hash.
175 * return {Promise} - A promise that resolves to the file contents.
176 */
177function 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 * Default http request handler that simply rejects.
194 */
195async function httpReject() {
196 vegaUtil.error('No HTTP fetch method available.');
197}
198
199const isValid = _ => _ != null && _ === _;
200
201const isBoolean = _ => _ === 'true'
202 || _ === 'false'
203 || _ === true
204 || _ === false;
205
206const isDate = _ => !Number.isNaN(Date.parse(_));
207
208const isNumber = _ => !Number.isNaN(+_) && !(_ instanceof Date);
209
210const isInteger = _ => isNumber(_) && Number.isInteger(+_);
211
212const 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
221const typeTests = [
222 isBoolean,
223 isInteger,
224 isNumber,
225 isDate
226];
227
228const typeList = [
229 'boolean',
230 'integer',
231 'number',
232 'date'
233];
234
235function 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
258function inferTypes(data, fields) {
259 return fields.reduce((types, field) => {
260 types[field] = inferType(data, field);
261 return types;
262 }, {});
263}
264
265function 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
276function 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
285dsv.responseType = 'text';
286
287function isBuffer(_) {
288 return (typeof Buffer === 'function' && vegaUtil.isFunction(Buffer.isBuffer))
289 ? Buffer.isBuffer(_) : false;
290}
291
292function 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
299json.responseType = 'json';
300
301function parseJSON(data, format) {
302 return (format && format.copy)
303 ? JSON.parse(JSON.stringify(data))
304 : data;
305}
306
307const filters = {
308 interior: (a, b) => a !== b,
309 exterior: (a, b) => a === b
310};
311
312function 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
334topojson.responseType = 'json';
335
336const format = {
337 dsv: dsv,
338 csv: delimitedFormat(','),
339 tsv: delimitedFormat('\t'),
340 json: json,
341 topojson: topojson
342};
343
344function 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
353function responseType(type) {
354 const f = formats(type);
355 return f && f.responseType || 'text';
356}
357
358function 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
371function parse(data, types, timeParser, utcParser) {
372 if (!data.length) return; // early exit for empty data
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); // split on first :
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
417var loader = loaderFactory(
418 require('node-fetch'),
419 require('fs')
420);
421
422exports.format = format;
423exports.formats = formats;
424exports.inferType = inferType;
425exports.inferTypes = inferTypes;
426exports.loader = loader;
427exports.read = read;
428exports.responseType = responseType;
429exports.typeParsers = typeParsers;