Some applications truncates the last closing parenthesis in perf dashboard URL
[WebKit-https.git] / Websites / perf.webkit.org / public / v3 / pages / page-router.js
1 class PageRouter {
2     constructor()
3     {
4         this._pages = [];
5         this._defaultPage = null;
6         this._currentPage = null;
7         this._historyTimer = null;
8         this._hash = null;
9
10         window.onhashchange = this._hashDidChange.bind(this);
11     }
12
13     addPage(page)
14     {
15         this._pages.push(page);
16         page.setRouter(this);
17     }
18
19     setDefaultPage(defaultPage)
20     {
21         this._defaultPage = defaultPage;
22     }
23
24     currentPage() { return this._currentPage; }
25
26     route()
27     {
28         var destinationPage = this._defaultPage;
29         var parsed = this._deserializeFromHash(location.hash);
30         if (parsed.route) {
31             var hashUrl = parsed.route;
32             var queryIndex = hashUrl.indexOf('?');
33             if (queryIndex >= 0)
34                 hashUrl = hashUrl.substring(0, queryIndex);
35
36             for (var page of this._pages) {
37                 var routeName = page.routeName();
38                 if (routeName == hashUrl
39                     || (hashUrl.startsWith(routeName) && hashUrl.charAt(routeName.length) == '/')) {
40                     parsed.state.remainingRoute = hashUrl.substring(routeName.length + 1);
41                     destinationPage = page;
42                     break;
43                 }
44             }
45         }
46
47         if (!destinationPage)
48             return false;
49
50         if (this._currentPage != destinationPage) {
51             this._currentPage = destinationPage;
52             destinationPage.open(parsed.state);
53         } else
54             destinationPage.updateFromSerializedState(parsed.state, false);
55
56         return true;
57     }
58
59     pageDidOpen(page)
60     {
61         console.assert(page instanceof Page);
62         var pageDidChange = this._currentPage != page;
63         this._currentPage = page;
64         if (pageDidChange)
65             this.scheduleUrlStateUpdate();
66     }
67
68     scheduleUrlStateUpdate()
69     {
70         if (this._historyTimer)
71             return;
72         this._historyTimer = setTimeout(this._updateURLState.bind(this), 0);
73     }
74
75     url(routeName, state)
76     {
77         return this._serializeToHash(routeName, state);
78     }
79
80     _updateURLState()
81     {
82         this._historyTimer = null;
83         console.assert(this._currentPage);
84         var currentPage = this._currentPage;
85         this._hash = this._serializeToHash(currentPage.routeName(), currentPage.serializeState());
86         location.hash = this._hash;
87     }
88
89     _hashDidChange()
90     {
91         if (unescape(location.hash) == this._hash)
92             return;
93         this.route();
94         this._hash = null;
95     }
96
97     _serializeToHash(route, state)
98     {
99         var params = [];
100         for (var key in state)
101             params.push(key + '=' + this._serializeHashQueryValue(state[key]));
102         var query = params.length ? ('?' + params.join('&')) : '';
103         return `#/${route}${query}`;
104     }
105     
106     _deserializeFromHash(hash)
107     {
108         if (!hash || !hash.startsWith('#/'))
109             return {route: null, state: {}};
110
111         hash = unescape(hash); // For Firefox.
112
113         var queryIndex = hash.indexOf('?');
114         var route;
115         var state = {};
116         if (queryIndex >= 0) {
117             route = hash.substring(2, queryIndex);
118             for (var part of hash.substring(queryIndex + 1).split('&')) {
119                 var keyValuePair = part.split('=');
120                 state[keyValuePair[0]] = this._deserializeHashQueryValue(keyValuePair[1]);
121             }
122         } else
123             route = hash.substring(2);
124
125         return {route: route, state: state};
126     }
127
128     _serializeHashQueryValue(value)
129     {
130         if (!(value instanceof Array)) {
131             console.assert(value === null || typeof(value) === 'number' || /[A-Za-z0-9]*/.test(value));
132             return value === null ? 'null' : value;
133         }
134
135         var serializedItems = [];
136         for (var item of value)
137             serializedItems.push(this._serializeHashQueryValue(item));
138         return '(' + serializedItems.join('-') + ')';
139     }
140
141     _deserializeHashQueryValue(value)
142     {
143         var json = value.replace(/\(/g, '[').replace(/\)/g, ']').replace(/-/g, ',');
144         try {
145             return JSON.parse(json);
146         } catch (error) {
147
148             // Some applications don't linkify two consecutive closing parentheses: )).
149             // Try fixing adding one extra parenthesis to see if that works.
150             function count(regex)
151             {
152                 var match = json.match(regex);
153                 return match ? match.length : 0;
154             }
155             var missingClosingBrackets = count(/\[/g) - count(/\]/g);
156             var fix = new Array(missingClosingBrackets).fill(']').join('');
157             try {
158                 return JSON.parse(json + fix);
159             } catch (newError) { }
160
161             return value;
162         }
163     }
164 }