UNPKG

13.4 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega-util'), require('d3-dsv'), require('topojson-client'), require('vega-format')) :
3 typeof define === 'function' && define.amd ? define(['exports', 'vega-util', 'd3-dsv', 'topojson-client', 'vega-format'], factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.vega = {}, global.vega, global.d3, global.topojson, global.vega));
5}(this, (function (exports, vegaUtil, d3Dsv, topojsonClient, vegaFormat) { 'use strict';
6
7 // Matches absolute URLs with optional protocol
8 // https://... file://... //...
9 const protocol_re = /^([A-Za-z]+:)?\/\//;
10
11 // Matches allowed URIs. From https://github.com/cure53/DOMPurify/blob/master/src/regexp.js with added file://
12 const 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
13 const whitespace_re = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
14
15
16 // Special treatment in node.js for the file: protocol
17 const fileProtocol = 'file://';
18
19 /**
20 * Factory for a loader constructor that provides methods for requesting
21 * files from either the network or disk, and for sanitizing request URIs.
22 * @param {function} fetch - The Fetch API for HTTP network requests.
23 * If null or undefined, HTTP loading will be disabled.
24 * @param {object} fs - The file system interface for file loading.
25 * If null or undefined, local file loading will be disabled.
26 * @return {function} A loader constructor with the following signature:
27 * param {object} [options] - Optional default loading options to use.
28 * return {object} - A new loader instance.
29 */
30 function loaderFactory(fetch, fs) {
31 return options => ({
32 options: options || {},
33 sanitize: sanitize,
34 load: load,
35 fileAccess: !!fs,
36 file: fileLoader(fs),
37 http: httpLoader(fetch)
38 });
39 }
40
41 /**
42 * Load an external resource, typically either from the web or from the local
43 * filesystem. This function uses {@link sanitize} to first sanitize the uri,
44 * then calls either {@link http} (for web requests) or {@link file} (for
45 * filesystem loading).
46 * @param {string} uri - The resource indicator (e.g., URL or filename).
47 * @param {object} [options] - Optional loading options. These options will
48 * override any existing default options.
49 * @return {Promise} - A promise that resolves to the loaded content.
50 */
51 async function load(uri, options) {
52 const opt = await this.sanitize(uri, options),
53 url = opt.href;
54
55 return opt.localFile
56 ? this.file(url)
57 : this.http(url, options);
58 }
59
60 /**
61 * URI sanitizer function.
62 * @param {string} uri - The uri (url or filename) to sanity check.
63 * @param {object} options - An options hash.
64 * @return {Promise} - A promise that resolves to an object containing
65 * sanitized uri data, or rejects it the input uri is deemed invalid.
66 * The properties of the resolved object are assumed to be
67 * valid attributes for an HTML 'a' tag. The sanitized uri *must* be
68 * provided by the 'href' property of the returned object.
69 */
70 async function sanitize(uri, options) {
71 options = vegaUtil.extend({}, this.options, options);
72
73 const fileAccess = this.fileAccess,
74 result = {href: null};
75
76 let isFile, loadFile, base;
77
78 const isAllowed = allowed_re.test(uri.replace(whitespace_re, ''));
79
80 if (uri == null || typeof uri !== 'string' || !isAllowed) {
81 vegaUtil.error('Sanitize failure, invalid URI: ' + vegaUtil.stringValue(uri));
82 }
83
84 const hasProtocol = protocol_re.test(uri);
85
86 // if relative url (no protocol/host), prepend baseURL
87 if ((base = options.baseURL) && !hasProtocol) {
88 // Ensure that there is a slash between the baseURL (e.g. hostname) and url
89 if (!uri.startsWith('/') && base[base.length-1] !== '/') {
90 uri = '/' + uri;
91 }
92 uri = base + uri;
93 }
94
95 // should we load from file system?
96 loadFile = (isFile = uri.startsWith(fileProtocol))
97 || options.mode === 'file'
98 || options.mode !== 'http' && !hasProtocol && fileAccess;
99
100 if (isFile) {
101 // strip file protocol
102 uri = uri.slice(fileProtocol.length);
103 } else if (uri.startsWith('//')) {
104 if (options.defaultProtocol === 'file') {
105 // if is file, strip protocol and set loadFile flag
106 uri = uri.slice(2);
107 loadFile = true;
108 } else {
109 // if relative protocol (starts with '//'), prepend default protocol
110 uri = (options.defaultProtocol || 'http') + ':' + uri;
111 }
112 }
113
114 // set non-enumerable mode flag to indicate local file load
115 Object.defineProperty(result, 'localFile', {value: !!loadFile});
116
117 // set uri
118 result.href = uri;
119
120 // set default result target, if specified
121 if (options.target) {
122 result.target = options.target + '';
123 }
124
125 // set default result rel, if specified (#1542)
126 if (options.rel) {
127 result.rel = options.rel + '';
128 }
129
130 // provide control over cross-origin image handling (#2238)
131 // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
132 if (options.context === 'image' && options.crossOrigin) {
133 result.crossOrigin = options.crossOrigin + '';
134 }
135
136 // return
137 return result;
138 }
139
140 /**
141 * File system loader factory.
142 * @param {object} fs - The file system interface.
143 * @return {function} - A file loader with the following signature:
144 * param {string} filename - The file system path to load.
145 * param {string} filename - The file system path to load.
146 * return {Promise} A promise that resolves to the file contents.
147 */
148 function fileLoader(fs) {
149 return fs
150 ? filename => new Promise((accept, reject) => {
151 fs.readFile(filename, (error, data) => {
152 if (error) reject(error);
153 else accept(data);
154 });
155 })
156 : fileReject;
157 }
158
159 /**
160 * Default file system loader that simply rejects.
161 */
162 async function fileReject() {
163 vegaUtil.error('No file system access.');
164 }
165
166 /**
167 * HTTP request handler factory.
168 * @param {function} fetch - The Fetch API method.
169 * @return {function} - An http loader with the following signature:
170 * param {string} url - The url to request.
171 * param {object} options - An options hash.
172 * return {Promise} - A promise that resolves to the file contents.
173 */
174 function httpLoader(fetch) {
175 return fetch
176 ? async function(url, options) {
177 const opt = vegaUtil.extend({}, this.options.http, options),
178 type = options && options.response,
179 response = await fetch(url, opt);
180
181 return !response.ok
182 ? vegaUtil.error(response.status + '' + response.statusText)
183 : vegaUtil.isFunction(response[type]) ? response[type]()
184 : response.text();
185 }
186 : httpReject;
187 }
188
189 /**
190 * Default http request handler that simply rejects.
191 */
192 async function httpReject() {
193 vegaUtil.error('No HTTP fetch method available.');
194 }
195
196 const isValid = _ => _ != null && _ === _;
197
198 const isBoolean = _ => _ === 'true'
199 || _ === 'false'
200 || _ === true
201 || _ === false;
202
203 const isDate = _ => !Number.isNaN(Date.parse(_));
204
205 const isNumber = _ => !Number.isNaN(+_) && !(_ instanceof Date);
206
207 const isInteger = _ => isNumber(_) && Number.isInteger(+_);
208
209 const typeParsers = {
210 boolean: vegaUtil.toBoolean,
211 integer: vegaUtil.toNumber,
212 number: vegaUtil.toNumber,
213 date: vegaUtil.toDate,
214 string: vegaUtil.toString,
215 unknown: vegaUtil.identity
216 };
217
218 const typeTests = [
219 isBoolean,
220 isInteger,
221 isNumber,
222 isDate
223 ];
224
225 const typeList = [
226 'boolean',
227 'integer',
228 'number',
229 'date'
230 ];
231
232 function inferType(values, field) {
233 if (!values || !values.length) return 'unknown';
234
235 const n = values.length,
236 m = typeTests.length,
237 a = typeTests.map((_, i) => i + 1);
238
239 for (let i = 0, t = 0, j, value; i < n; ++i) {
240 value = field ? values[i][field] : values[i];
241 for (j = 0; j < m; ++j) {
242 if (a[j] && isValid(value) && !typeTests[j](value)) {
243 a[j] = 0;
244 ++t;
245 if (t === typeTests.length) return 'string';
246 }
247 }
248 }
249
250 return typeList[
251 a.reduce((u, v) => u === 0 ? v : u, 0) - 1
252 ];
253 }
254
255 function inferTypes(data, fields) {
256 return fields.reduce((types, field) => {
257 types[field] = inferType(data, field);
258 return types;
259 }, {});
260 }
261
262 function delimitedFormat(delimiter) {
263 const parse = function(data, format) {
264 const delim = {delimiter: delimiter};
265 return dsv(data, format ? vegaUtil.extend(format, delim) : delim);
266 };
267
268 parse.responseType = 'text';
269
270 return parse;
271 }
272
273 function dsv(data, format) {
274 if (format.header) {
275 data = format.header
276 .map(vegaUtil.stringValue)
277 .join(format.delimiter) + '\n' + data;
278 }
279 return d3Dsv.dsvFormat(format.delimiter).parse(data + '');
280 }
281
282 dsv.responseType = 'text';
283
284 function isBuffer(_) {
285 return (typeof Buffer === 'function' && vegaUtil.isFunction(Buffer.isBuffer))
286 ? Buffer.isBuffer(_) : false;
287 }
288
289 function json(data, format) {
290 const prop = (format && format.property) ? vegaUtil.field(format.property) : vegaUtil.identity;
291 return vegaUtil.isObject(data) && !isBuffer(data)
292 ? parseJSON(prop(data))
293 : prop(JSON.parse(data));
294 }
295
296 json.responseType = 'json';
297
298 function parseJSON(data, format) {
299 return (format && format.copy)
300 ? JSON.parse(JSON.stringify(data))
301 : data;
302 }
303
304 const filters = {
305 interior: (a, b) => a !== b,
306 exterior: (a, b) => a === b
307 };
308
309 function topojson(data, format) {
310 let method, object, property, filter;
311 data = json(data, format);
312
313 if (format && format.feature) {
314 method = topojsonClient.feature;
315 property = format.feature;
316 } else if (format && format.mesh) {
317 method = topojsonClient.mesh;
318 property = format.mesh;
319 filter = filters[format.filter];
320 } else {
321 vegaUtil.error('Missing TopoJSON feature or mesh parameter.');
322 }
323
324 object = (object = data.objects[property])
325 ? method(data, object, filter)
326 : vegaUtil.error('Invalid TopoJSON object: ' + property);
327
328 return object && object.features || [object];
329 }
330
331 topojson.responseType = 'json';
332
333 const format = {
334 dsv: dsv,
335 csv: delimitedFormat(','),
336 tsv: delimitedFormat('\t'),
337 json: json,
338 topojson: topojson
339 };
340
341 function formats(name, reader) {
342 if (arguments.length > 1) {
343 format[name] = reader;
344 return this;
345 } else {
346 return vegaUtil.hasOwnProperty(format, name) ? format[name] : null;
347 }
348 }
349
350 function responseType(type) {
351 const f = formats(type);
352 return f && f.responseType || 'text';
353 }
354
355 function read(data, schema, timeParser, utcParser) {
356 schema = schema || {};
357
358 const reader = formats(schema.type || 'json');
359 if (!reader) vegaUtil.error('Unknown data format type: ' + schema.type);
360
361 data = reader(data, schema);
362 if (schema.parse) parse(data, schema.parse, timeParser, utcParser);
363
364 if (vegaUtil.hasOwnProperty(data, 'columns')) delete data.columns;
365 return data;
366 }
367
368 function parse(data, types, timeParser, utcParser) {
369 if (!data.length) return; // early exit for empty data
370
371 const locale = vegaFormat.timeFormatDefaultLocale();
372 timeParser = timeParser || locale.timeParse;
373 utcParser = utcParser || locale.utcParse;
374
375 let fields = data.columns || Object.keys(data[0]),
376 datum, field, i, j, n, m;
377
378 if (types === 'auto') types = inferTypes(data, fields);
379
380 fields = Object.keys(types);
381 const parsers = fields.map(field => {
382 const type = types[field];
383 let parts, pattern;
384
385 if (type && (type.startsWith('date:') || type.startsWith('utc:'))) {
386 parts = type.split(/:(.+)?/, 2); // split on first :
387 pattern = parts[1];
388
389 if ((pattern[0] === '\'' && pattern[pattern.length-1] === '\'') ||
390 (pattern[0] === '"' && pattern[pattern.length-1] === '"')) {
391 pattern = pattern.slice(1, -1);
392 }
393
394 const parse = parts[0] === 'utc' ? utcParser : timeParser;
395 return parse(pattern);
396 }
397
398 if (!typeParsers[type]) {
399 throw Error('Illegal format pattern: ' + field + ':' + type);
400 }
401
402 return typeParsers[type];
403 });
404
405 for (i=0, n=data.length, m=fields.length; i<n; ++i) {
406 datum = data[i];
407 for (j=0; j<m; ++j) {
408 field = fields[j];
409 datum[field] = parsers[j](datum[field]);
410 }
411 }
412 }
413
414 var loader = loaderFactory(
415 typeof fetch !== 'undefined' && fetch, // use built-in fetch API
416 null // no file system access
417 );
418
419 exports.format = format;
420 exports.formats = formats;
421 exports.inferType = inferType;
422 exports.inferTypes = inferTypes;
423 exports.loader = loader;
424 exports.read = read;
425 exports.responseType = responseType;
426 exports.typeParsers = typeParsers;
427
428 Object.defineProperty(exports, '__esModule', { value: true });
429
430})));