From babce0dd695e4a82ff247c4729402f218760b25a Mon Sep 17 00:00:00 2001 From: Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com> Date: Sat, 31 Dec 2022 11:52:54 -0700 Subject: [PATCH] Tsify rison.js (#4510) * Tsify rison.js * Turn eslint back on * Improve typing, cleanup code * Matt comments * Prettier-ignore a type declaration * A couple small type tweaks --- lib/common-utils.ts | 34 +++ static/rison.js | 516 -------------------------------------------- static/rison.ts | 456 +++++++++++++++++++++++++++++++++++++++ static/url.ts | 5 +- 4 files changed, 493 insertions(+), 518 deletions(-) create mode 100644 lib/common-utils.ts delete mode 100644 static/rison.js create mode 100644 static/rison.ts diff --git a/lib/common-utils.ts b/lib/common-utils.ts new file mode 100644 index 00000000000..76aa049cf5a --- /dev/null +++ b/lib/common-utils.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +export function isString(x: any): x is string { + return typeof x === 'string' || x instanceof String; +} + +// Object.keys is typed as returning :string[] for some reason +// This util is for cases where the key is a union of a few possible keys and we +// want the resulting array properly typed. +export function keys(o: Record): K[] { + return Object.keys(o) as K[]; +} diff --git a/static/rison.js b/static/rison.js deleted file mode 100644 index 7945668c806..00000000000 --- a/static/rison.js +++ /dev/null @@ -1,516 +0,0 @@ -/* eslint-disable */ -// Taken from https://github.com/Nanonid/rison at e64af6c096fd30950ec32cfd48526ca6ee21649d (Jun 9, 2017) -// Uses CommonJS, AMD or browser globals to create a module. -// Based on: https://github.com/umdjs/umd/blob/master/commonjsStrict.js -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['exports'], factory); - } else if (typeof exports === 'object') { - // CommonJS - factory(exports); - } else { - // Browser globals - factory((root.rison = {})); - } -})(this, function (exports) { - var rison = exports; - - ////////////////////////////////////////////////// - // - // the stringifier is based on - // http://json.org/json.js as of 2006-04-28 from json.org - // the parser is based on - // http://osteele.com/sources/openlaszlo/json - // - - if (typeof rison == 'undefined') window.rison = {}; - - /** - * rules for an uri encoder that is more tolerant than encodeURIComponent - * - * encodeURIComponent passes ~!*()-_.' - * - * we also allow ,:@$/ - * - */ - rison.uri_ok = { - // ok in url paths and in form query args - '~': true, - '!': true, - '*': true, - '(': true, - ')': true, - '-': true, - _: true, - '.': true, - ',': true, - ':': true, - '@': true, - $: true, - "'": true, - '/': true, - }; - - /* - * we divide the uri-safe glyphs into three sets - * - used by rison ' ! : ( ) , - * - not common in strings, reserved * @ $ & ; = - * - * we define as anything that's not forbidden - */ - - /** - * punctuation characters that are legal inside ids. - */ - // this var isn't actually used - //rison.idchar_punctuation = "_-./~"; - - (function () { - var l = []; - for (var hi = 0; hi < 16; hi++) { - for (var lo = 0; lo < 16; lo++) { - if (hi + lo === 0) continue; - var c = String.fromCharCode(hi * 16 + lo); - if (!/\w|[-_.\/~]/.test(c)) l.push('\\u00' + hi.toString(16) + lo.toString(16)); - } - } - /** - * characters that are illegal inside ids. - * and classes are illegal in ids. - * - */ - rison.not_idchar = l.join(''); - //idcrx = new RegExp('[' + rison.not_idchar + ']'); - //console.log('NOT', (idcrx.test(' ')) ); - })(); - //rison.not_idchar = " \t\r\n\"<>[]{}'!=:(),*@$;&"; - rison.not_idchar = " '!:(),*@$"; - - /** - * characters that are illegal as the start of an id - * this is so ids can't look like numbers. - */ - rison.not_idstart = '-0123456789'; - - (function () { - var idrx = '[^' + rison.not_idstart + rison.not_idchar + '][^' + rison.not_idchar + ']*'; - - rison.id_ok = new RegExp('^' + idrx + '$'); - - // regexp to find the end of an id when parsing - // g flag on the regexp is necessary for iterative regexp.exec() - rison.next_id = new RegExp(idrx, 'g'); - })(); - - /** - * this is like encodeURIComponent() but quotes fewer characters. - * - * @see rison.uri_ok - * - * encodeURIComponent passes ~!*()-_.' - * rison.quote also passes ,:@$/ - * and quotes " " as "+" instead of "%20" - */ - rison.quote = function (x) { - if (/^[-A-Za-z0-9~!*()_.',:@$\/]*$/.test(x)) return x; - - return encodeURIComponent(x) - .replace(/%2C/g, ',') - .replace(/%3A/g, ':') - .replace(/%40/g, '@') - .replace(/%24/g, '$') - .replace(/%2F/g, '/') - .replace(/%20/g, '+'); - }; - - // - // based on json.js 2006-04-28 from json.org - // license: http://www.json.org/license.html - // - // hacked by nix for use in uris. - // - - (function () { - var sq = { - // url-ok but quoted in strings - "'": true, - '!': true, - }, - enc = function (v) { - if (v && typeof v.toJSON === 'function') v = v.toJSON(); - var fn = s[typeof v]; - if (fn) return fn(v); - }, - s = { - array: function (x) { - var a = ['!('], - b, - f, - i, - l = x.length, - v; - for (i = 0; i < l; i += 1) { - v = enc(x[i]); - if (typeof v == 'string') { - if (b) { - a[a.length] = ','; - } - a[a.length] = v; - b = true; - } - } - a[a.length] = ')'; - return a.join(''); - }, - boolean: function (x) { - if (x) return '!t'; - return '!f'; - }, - null: function () { - return '!n'; - }, - number: function (x) { - if (!isFinite(x)) return '!n'; - // strip '+' out of exponent, '-' is ok though - return String(x).replace(/\+/, ''); - }, - object: function (x) { - if (x) { - if (x instanceof Array) { - return s.array(x); - } - // WILL: will this work on non-Firefox browsers? - if (typeof x.__prototype__ === 'object' && typeof x.__prototype__.encode_rison !== 'undefined') - return x.encode_rison(); - - var a = ['('], - b, - i, - v, - k, - ki, - ks = []; - for (i in x) ks[ks.length] = i; - ks.sort(); - for (ki = 0; ki < ks.length; ki++) { - i = ks[ki]; - v = enc(x[i]); - if (typeof v == 'string') { - if (b) { - a[a.length] = ','; - } - k = isNaN(parseInt(i)) ? s.string(i) : s.number(i); - a.push(k, ':', v); - b = true; - } - } - a[a.length] = ')'; - return a.join(''); - } - return '!n'; - }, - string: function (x) { - if (x === '') return "''"; - - if (rison.id_ok.test(x)) return x; - - x = x.replace(/(['!])/g, function (a, b) { - if (sq[b]) return '!' + b; - return b; - }); - return "'" + x + "'"; - }, - undefined: function () { - // ignore undefined just like JSON - return; - }, - }; - - /** - * rison-encode a javascript structure - * - * implemementation based on Douglas Crockford's json.js: - * http://json.org/json.js as of 2006-04-28 from json.org - * - */ - rison.encode = function (v) { - return enc(v); - }; - - /** - * rison-encode a javascript object without surrounding parens - * - */ - rison.encode_object = function (v) { - if (typeof v != 'object' || v === null || v instanceof Array) - throw new Error('rison.encode_object expects an object argument'); - var r = s[typeof v](v); - return r.substring(1, r.length - 1); - }; - - /** - * rison-encode a javascript array without surrounding parens - * - */ - rison.encode_array = function (v) { - if (!(v instanceof Array)) throw new Error('rison.encode_array expects an array argument'); - var r = s[typeof v](v); - return r.substring(2, r.length - 1); - }; - - /** - * rison-encode and uri-encode a javascript structure - * - */ - rison.encode_uri = function (v) { - return rison.quote(s[typeof v](v)); - }; - })(); - - // - // based on openlaszlo-json and hacked by nix for use in uris. - // - // Author: Oliver Steele - // Copyright: Copyright 2006 Oliver Steele. All rights reserved. - // Homepage: http://osteele.com/sources/openlaszlo/json - // License: MIT License. - // Version: 1.0 - - /** - * parse a rison string into a javascript structure. - * - * this is the simplest decoder entry point. - * - * based on Oliver Steele's OpenLaszlo-JSON - * http://osteele.com/sources/openlaszlo/json - */ - rison.decode = function (r) { - var errcb = function (e) { - throw Error('rison decoder error: ' + e); - }; - var p = new rison.parser(errcb); - return p.parse(r); - }; - - /** - * parse an o-rison string into a javascript structure. - * - * this simply adds parentheses around the string before parsing. - */ - rison.decode_object = function (r) { - return rison.decode('(' + r + ')'); - }; - - /** - * parse an a-rison string into a javascript structure. - * - * this simply adds array markup around the string before parsing. - */ - rison.decode_array = function (r) { - return rison.decode('!(' + r + ')'); - }; - - /** - * construct a new parser object for reuse. - * - * @constructor - * @class A Rison parser class. You should probably - * use rison.decode instead. - * @see rison.decode - */ - rison.parser = function (errcb) { - this.errorHandler = errcb; - }; - - /** - * a string containing acceptable whitespace characters. - * by default the rison decoder tolerates no whitespace. - * to accept whitespace set rison.parser.WHITESPACE = " \t\n\r\f"; - */ - rison.parser.WHITESPACE = ''; - - // expose this as-is? - rison.parser.prototype.setOptions = function (options) { - if (options['errorHandler']) this.errorHandler = options.errorHandler; - }; - - /** - * parse a rison string into a javascript structure. - */ - rison.parser.prototype.parse = function (str) { - this.string = str; - this.index = 0; - this.message = null; - var value = this.readValue(); - if (!this.message && this.next()) - value = this.error("unable to parse string as rison: '" + rison.encode(str) + "'"); - if (this.message && this.errorHandler) this.errorHandler(this.message, this.index); - return value; - }; - - rison.parser.prototype.error = function (message) { - if (typeof console != 'undefined') console.log('rison parser error: ', message); - this.message = message; - return undefined; - }; - - rison.parser.prototype.readValue = function () { - var c = this.next(); - var fn = c && this.table[c]; - - if (fn) return fn.apply(this); - - // fell through table, parse as an id - - var s = this.string; - var i = this.index - 1; - - // Regexp.lastIndex may not work right in IE before 5.5? - // g flag on the regexp is also necessary - rison.next_id.lastIndex = i; - var m = rison.next_id.exec(s); - - // console.log('matched id', i, r.lastIndex); - - if (m.length > 0) { - var id = m[0]; - this.index = i + id.length; - return id; // a string - } - - if (c) return this.error("invalid character: '" + c + "'"); - return this.error('empty expression'); - }; - - rison.parser.parse_array = function (parser) { - var ar = []; - var c; - while ((c = parser.next()) !== ')') { - if (!c) return parser.error("unmatched '!('"); - if (ar.length) { - if (c !== ',') parser.error("missing ','"); - } else if (c === ',') { - return parser.error("extra ','"); - } else --parser.index; - var n = parser.readValue(); - if (typeof n == 'undefined') return undefined; - ar.push(n); - } - return ar; - }; - - rison.parser.bangs = { - t: true, - f: false, - n: null, - '(': rison.parser.parse_array, - }; - - rison.parser.prototype.table = { - '!': function () { - var s = this.string; - var c = s.charAt(this.index++); - if (!c) return this.error('"!" at end of input'); - var x = rison.parser.bangs[c]; - if (typeof x == 'function') { - return x.call(null, this); - } else if (typeof x == 'undefined') { - return this.error('unknown literal: "!' + c + '"'); - } - return x; - }, - '(': function () { - var o = {}; - var c; - var count = 0; - while ((c = this.next()) !== ')') { - if (count) { - if (c !== ',') this.error("missing ','"); - } else if (c === ',') { - return this.error("extra ','"); - } else --this.index; - var k = this.readValue(); - if (typeof k == 'undefined') return undefined; - if (this.next() !== ':') return this.error("missing ':'"); - var v = this.readValue(); - if (typeof v == 'undefined') return undefined; - o[k] = v; - count++; - } - return o; - }, - "'": function () { - var s = this.string; - var i = this.index; - var start = i; - var segments = []; - var c; - while ((c = s.charAt(i++)) !== "'") { - //if (i == s.length) return this.error('unmatched "\'"'); - if (!c) return this.error('unmatched "\'"'); - if (c === '!') { - if (start < i - 1) segments.push(s.slice(start, i - 1)); - c = s.charAt(i++); - if ("!'".indexOf(c) >= 0) { - segments.push(c); - } else { - return this.error('invalid string escape: "!' + c + '"'); - } - start = i; - } - } - if (start < i - 1) segments.push(s.slice(start, i - 1)); - this.index = i; - return segments.length === 1 ? segments[0] : segments.join(''); - }, - // Also any digit. The statement that follows this table - // definition fills in the digits. - '-': function () { - var s = this.string; - var i = this.index; - var start = i - 1; - var state = 'int'; - var permittedSigns = '-'; - var transitions = { - 'int+.': 'frac', - 'int+e': 'exp', - 'frac+e': 'exp', - }; - do { - var c = s.charAt(i++); - if (!c) break; - if ('0' <= c && c <= '9') continue; - if (permittedSigns.indexOf(c) >= 0) { - permittedSigns = ''; - continue; - } - state = transitions[state + '+' + c.toLowerCase()]; - if (state === 'exp') permittedSigns = '-'; - } while (state); - this.index = --i; - s = s.slice(start, i); - if (s === '-') return this.error('invalid number'); - return Number(s); - }, - }; - // copy table['-'] to each of table[i] | i <- '0'..'9': - (function (table) { - for (var i = 0; i <= 9; i++) table[String(i)] = table['-']; - })(rison.parser.prototype.table); - - // return the next non-whitespace character, or undefined - rison.parser.prototype.next = function () { - var c; - var s = this.string; - var i = this.index; - do { - if (i === s.length) return undefined; - c = s.charAt(i++); - } while (rison.parser.WHITESPACE.indexOf(c) >= 0); - this.index = i; - return c; - }; - - // End of UMD module wrapper -}); diff --git a/static/rison.ts b/static/rison.ts new file mode 100644 index 00000000000..735dceaddcf --- /dev/null +++ b/static/rison.ts @@ -0,0 +1,456 @@ +// Based on https://github.com/Nanonid/rison at e64af6c096fd30950ec32cfd48526ca6ee21649d (Jun 9, 2017) + +import {assert, unwrap} from './assert'; + +import {isString} from '../lib/common-utils'; + +////////////////////////////////////////////////// +// +// the stringifier is based on +// http://json.org/json.js as of 2006-04-28 from json.org +// the parser is based on +// http://osteele.com/sources/openlaszlo/json +// + +/* + * we divide the uri-safe glyphs into three sets + * - used by rison ' ! : ( ) , + * - not common in strings, reserved * @ $ & ; = + * + * we define as anything that's not forbidden + */ + +/** + * punctuation characters that are legal inside ids. + */ +// this var isn't actually used +//rison.idchar_punctuation = "_-./~"; + +const not_idchar = " '!:(),*@$"; + +/** + * characters that are illegal as the start of an id + * this is so ids can't look like numbers. + */ +const not_idstart = '-0123456789'; + +const [id_ok, next_id] = (() => { + const _idrx = '[^' + not_idstart + not_idchar + '][^' + not_idchar + ']*'; + return [ + new RegExp('^' + _idrx + '$'), + // regexp to find the end of an id when parsing + // g flag on the regexp is necessary for iterative regexp.exec() + new RegExp(_idrx, 'g'), + ]; +})(); + +/** + * this is like encodeURIComponent() but quotes fewer characters. + * + * @see rison.uri_ok + * + * encodeURIComponent passes ~!*()-_.' + * rison.quote also passes ,:@$/ + * and quotes " " as "+" instead of "%20" + */ +export function quote(x: string) { + if (/^[-A-Za-z0-9~!*()_.',:@$/]*$/.test(x)) return x; + + return encodeURIComponent(x) + .replace(/%2C/g, ',') + .replace(/%3A/g, ':') + .replace(/%40/g, '@') + .replace(/%24/g, '$') + .replace(/%2F/g, '/') + .replace(/%20/g, '+'); +} + +// +// based on json.js 2006-04-28 from json.org +// license: http://www.json.org/license.html +// +// hacked by nix for use in uris. +// +// url-ok but quoted in strings +const string_table = { + "'": true, + '!': true, +}; + +class Encoders { + static array(x: JSONValue[]) { + const a = ['!(']; + let b; + let i; + const l = x.length; + let v; + for (i = 0; i < l; i += 1) { + v = enc(x[i]); + if (typeof v == 'string') { + if (b) { + a[a.length] = ','; + } + a[a.length] = v; + b = true; + } + } + a[a.length] = ')'; + return a.join(''); + } + static boolean(x: boolean) { + if (x) return '!t'; + return '!f'; + } + static null() { + return '!n'; + } + static number(x: number) { + if (!isFinite(x)) return '!n'; + // strip '+' out of exponent, '-' is ok though + return String(x).replace(/\+/, ''); + } + static object(x: Record | null) { + if (x) { + // because typeof null === 'object' + if (x instanceof Array) { + return Encoders.array(x); + } + + const a = ['(']; + let b = false; + let i: string; + let v: string | undefined; + let k: string; + let ki: number; + const ks: string[] = []; + for (const i in x) ks[ks.length] = i; + ks.sort(); + for (ki = 0; ki < ks.length; ki++) { + i = ks[ki]; + v = enc(x[i]); + if (typeof v == 'string') { + if (b) { + a[a.length] = ','; + } + k = isNaN(parseInt(i)) ? Encoders.string(i) : Encoders.number(parseInt(i)); + a.push(k, ':', v); + b = true; + } + } + a[a.length] = ')'; + return a.join(''); + } + return '!n'; + } + static string(x: string) { + if (x === '') return "''"; + + if (id_ok.test(x)) return x; + + x = x.replace(/(['!])/g, function (a, b) { + if (string_table[b]) return '!' + b; + return b; + }); + return "'" + x + "'"; + } + static undefined() { + // ignore undefined just like JSON + return undefined; + } +} + +const encode_table: Record string | undefined> = { + array: Encoders.array, + object: Encoders.object, + boolean: Encoders.boolean, + string: Encoders.string, + number: Encoders.number, + null: Encoders.null, + undefined: Encoders.undefined, +}; + +function enc(v: JSONValue | (JSONValue & {toJSON?: () => string})) { + if (v && typeof v === 'object' && 'toJSON' in v && typeof v.toJSON === 'function') v = v.toJSON(); + if (typeof v in encode_table) { + return encode_table[typeof v](v); + } +} + +/** + * rison-encode a javascript structure + * + * implemementation based on Douglas Crockford's json.js: + * http://json.org/json.js as of 2006-04-28 from json.org + * + */ +export function encode(v: JSONValue | (JSONValue & {toJSON?: () => string})) { + return enc(v); +} + +/** + * rison-encode a javascript object without surrounding parens + * + */ +export function encode_object(v: JSONValue) { + if (typeof v != 'object' || v === null || v instanceof Array) + throw new Error('rison.encode_object expects an object argument'); + const r = unwrap(encode_table[typeof v](v)); + return r.substring(1, r.length - 1); +} + +/** + * rison-encode a javascript array without surrounding parens + * + */ +export function encode_array(v: JSONValue) { + if (!(v instanceof Array)) throw new Error('rison.encode_array expects an array argument'); + const r = unwrap(encode_table[typeof v](v)); + return r.substring(2, r.length - 1); +} + +/** + * rison-encode and uri-encode a javascript structure + * + */ +export function encode_uri(v: JSONValue) { + return quote(unwrap(encode_table[typeof v](v))); +} + +// +// based on openlaszlo-json and hacked by nix for use in uris. +// +// Author: Oliver Steele +// Copyright: Copyright 2006 Oliver Steele. All rights reserved. +// Homepage: http://osteele.com/sources/openlaszlo/json +// License: MIT License. +// Version: 1.0 + +/** + * parse a rison string into a javascript structure. + * + * this is the simplest decoder entry point. + * + * based on Oliver Steele's OpenLaszlo-JSON + * http://osteele.com/sources/openlaszlo/json + */ +export function decode(r: string) { + const p = new Parser(); + return p.parse(r); +} + +/** + * parse an o-rison string into a javascript structure. + * + * this simply adds parentheses around the string before parsing. + */ +export function decode_object(r: string) { + return decode('(' + r + ')'); +} + +/** + * parse an a-rison string into a javascript structure. + * + * this simply adds array markup around the string before parsing. + */ +export function decode_array(r: string) { + return decode('!(' + r + ')'); +} + +// prettier-ignore +export type JSONValue = + | string + | number + | boolean + | null + | undefined + | {[x: string]: JSONValue} + | Array; + +class Parser { + /** + * a string containing acceptable whitespace characters. + * by default the rison decoder tolerates no whitespace. + * to accept whitespace set rison.parser.WHITESPACE = " \t\n\r\f"; + */ + static WHITESPACE = ''; + + static readonly bangs = { + t: true, + f: false, + n: null, + '(': Parser.parse_array, + }; + + string: string; + index: number; + readonly table: Record JSONValue>; + + constructor() { + this.string = ''; + this.index = -1; + this.table = { + '!': () => { + const s = this.string; + const c = s.charAt(this.index++); + if (!c) return this.error('"!" at end of input'); + const x = Parser.bangs[c]; + if (typeof x == 'function') { + // eslint-disable-next-line no-useless-call + return x.call(null, this); + } else if (typeof x === 'undefined') { + return this.error('unknown literal: "!' + c + '"'); + } + return x; + }, + '(': () => { + const o: JSONValue = {}; + let c; + let count = 0; + while ((c = this.next()) !== ')') { + if (count) { + if (c !== ',') this.error("missing ','"); + } else if (c === ',') { + this.error("extra ','"); + } else --this.index; + const k = this.readValue(); + if (typeof k == 'undefined') return undefined; + if (this.next() !== ':') this.error("missing ':'"); + const v = this.readValue(); + if (typeof v == 'undefined') return undefined; + assert(isString(k)); + o[k] = v; + count++; + } + return o; + }, + "'": () => { + const s = this.string; + let i = this.index; + let start = i; + const segments: string[] = []; + let c; + while ((c = s.charAt(i++)) !== "'") { + //if (i == s.length) return this.error('unmatched "\'"'); + if (!c) this.error('unmatched "\'"'); + if (c === '!') { + if (start < i - 1) segments.push(s.slice(start, i - 1)); + c = s.charAt(i++); + if ("!'".indexOf(c) >= 0) { + segments.push(c); + } else { + this.error('invalid string escape: "!' + c + '"'); + } + start = i; + } + } + if (start < i - 1) segments.push(s.slice(start, i - 1)); + this.index = i; + return segments.length === 1 ? segments[0] : segments.join(''); + }, + // Also any digit. The statement that follows this table + // definition fills in the digits. + '-': () => { + let s = this.string; + let i = this.index; + const start = i - 1; + let state = 'int'; + let permittedSigns = '-'; + const transitions = { + 'int+.': 'frac', + 'int+e': 'exp', + 'frac+e': 'exp', + }; + do { + const c = s.charAt(i++); + if (!c) break; + if ('0' <= c && c <= '9') continue; + if (permittedSigns.indexOf(c) >= 0) { + permittedSigns = ''; + continue; + } + state = transitions[state + '+' + c.toLowerCase()]; + if (state === 'exp') permittedSigns = '-'; + } while (state); + this.index = --i; + s = s.slice(start, i); + if (s === '-') this.error('invalid number'); + return Number(s); + }, + }; + // copy table['-'] to each of table[i] | i <- '0'..'9': + for (let i = 0; i <= 9; i++) this.table[String(i)] = this.table['-']; + } + + /** + * parse a rison string into a javascript structure. + */ + parse(str: string): JSONValue { + this.string = str; + this.index = 0; + const value = this.readValue(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.next()) this.error("unable to parse string as rison: '" + encode(str) + "'"); + return value; + } + + error(message: string): never { + throw new Error('rison parser error: ' + message); + } + + readValue(): JSONValue { + const c = this.next(); + const fn = c && this.table[c]; + + if (fn) return fn.apply(this); + + // fell through table, parse as an id + + const s = this.string; + const i = this.index - 1; + + // Regexp.lastIndex may not work right in IE before 5.5? + // g flag on the regexp is also necessary + next_id.lastIndex = i; + const m = unwrap(next_id.exec(s)); + + // console.log('matched id', i, r.lastIndex); + + if (m.length > 0) { + const id = m[0]; + this.index = i + id.length; + return id; // a string + } + + if (c) this.error("invalid character: '" + c + "'"); + this.error('empty expression'); + } + + next(): string | undefined { + let c: string; + const s = this.string; + let i = this.index; + do { + if (i === s.length) return undefined; + c = s.charAt(i++); + } while (Parser.WHITESPACE.indexOf(c) >= 0); + this.index = i; + return c; + } + + static parse_array(parser: Parser): JSONValue[] | undefined { + const ar: JSONValue[] = []; + let c; + while ((c = parser.next()) !== ')') { + if (!c) return parser.error("unmatched '!('"); + if (ar.length) { + if (c !== ',') parser.error("missing ','"); + } else if (c === ',') { + return parser.error("extra ','"); + } else --parser.index; + const n = parser.readValue(); + if (n === undefined) return undefined; + ar.push(n); + } + return ar; + } +} diff --git a/static/url.ts b/static/url.ts index ba94dfcc67a..811f22e5750 100644 --- a/static/url.ts +++ b/static/url.ts @@ -26,9 +26,10 @@ import _ from 'underscore'; import GoldenLayout from 'golden-layout'; const lzstring = require('lz-string'); -const rison = require('rison'); const Components = require('./components'); +import * as rison from './rison'; + export function convertOldState(state: any): any { const sc = state.compilers[0]; if (!sc) throw new Error('Unable to determine compiler from old state'); @@ -74,7 +75,7 @@ export function loadState(state: any): any { return state; } -export function risonify(obj: object): string { +export function risonify(obj: rison.JSONValue): string { return rison.quote(rison.encode_object(obj)); }