jsbeeb 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,15 +51,15 @@ KEY.<host key>=<BBC key>
51
51
  Add one for each key you want to change. For example, Superior Software's Space Invaders fires with `COPY`; this makes
52
52
  `Enter` fire instead:
53
53
 
54
- [`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY)
54
+ [`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY`](https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&KEY.ENTER=COPY)
55
55
 
56
56
  Superior's Frogger uses `A`/`Z`/`DELETE`/`COPY` to move; this puts it on the arrow keys:
57
57
 
58
- [`https://bbc.xania.org/?disc1=sth:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FFrogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY)
58
+ [`https://bbc.xania.org/?disc1=sth:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY`](https://bbc.xania.org/?disc1=sth:Superior/Frogger-Superior.zip&autoboot&KEY.UP=A&KEY.DOWN=Z&KEY.LEFT=DELETE&KEY.RIGHT=COPY)
59
59
 
60
60
  And Superior's Hunchback steers with `CAPS LOCK` and `CTRL`, which the arrow keys can stand in for:
61
61
 
62
- [`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FHunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL)
62
+ [`https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL`](https://bbc.xania.org/?disc1=sth:Superior/Hunchback-Superior.zip&autoboot&KEY.LEFT=CAPSLOCK&KEY.RIGHT=CTRL)
63
63
 
64
64
  The **host key** names are jsbeeb's names for the keys on your own keyboard. Most are what you'd expect, but note:
65
65
 
@@ -141,7 +141,7 @@ GP.<gamepad control>=<BBC key>
141
141
  By default the D-pad presses the "Snapper" keys (`Z`, `X`, `:`, `/`), the `A` button presses `RETURN` and `Start`
142
142
  presses `SPACE`. To play Superior's Space Invaders on a pad, where `COPY` fires:
143
143
 
144
- [`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth%3ASuperior%2FSpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY)
144
+ [`https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY`](https://bbc.xania.org/?disc1=sth:Superior/SpaceInvaders-Superior.zip&autoboot&GP.FIRE=COPY)
145
145
 
146
146
  The gamepad control names are:
147
147
 
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "jsbeeb",
8
8
  "description": "Emulate a BBC Micro",
9
9
  "repository": "git@github.com:mattgodbolt/jsbeeb.git",
10
- "version": "1.19.0",
10
+ "version": "1.19.1",
11
11
  "//engines": "If you change the version of Node, it must also be updated at the top of the Dockerfile.",
12
12
  "engines": {
13
13
  "node": ">=24.15.0"
package/src/url-params.js CHANGED
@@ -41,18 +41,15 @@ export const ParamTypes = {
41
41
  export function parseQueryString(queryString, paramTypes = {}) {
42
42
  if (!queryString) return {};
43
43
 
44
- // workaround for shonky python web server
45
- const cleanQueryString = queryString.endsWith("/") ? queryString.substring(0, queryString.length - 1) : queryString;
46
-
47
44
  const parsedQuery = {};
48
45
 
49
- cleanQueryString.split("&").forEach(function (keyval) {
46
+ queryString.split("&").forEach(function (keyval) {
50
47
  if (!keyval) return;
51
48
 
52
49
  const keyAndVal = keyval.split("=");
53
50
  const key = decodeURIComponent(keyAndVal[0]);
54
51
  let val = null;
55
- if (keyAndVal.length > 1) val = decodeURIComponent(keyAndVal[1]);
52
+ if (keyAndVal.length > 1) val = decodeURIComponent(keyAndVal.slice(1).join("="));
56
53
 
57
54
  const paramType = paramTypes[key] || ParamTypes.STRING;
58
55
 
@@ -90,12 +87,26 @@ export function parseQueryString(queryString, paramTypes = {}) {
90
87
  }
91
88
 
92
89
  /**
93
- * Build a URL string from base URL and query parameters
94
- * @param {string} baseUrl - The base URL (without query string)
95
- * @param {Object} parsedQuery - Object containing query parameters
96
- * @param {Object.<string, ParamType>} [paramTypes={}] - Object mapping parameter names to their types
97
- * @returns {string} The complete URL with query parameters
90
+ * Characters RFC 3986 allows literally in a query that `encodeURIComponent` escapes anyway. `&`,
91
+ * `=`, `+`, `#`, `%` and space are left escaped because they delimit something, and `?` because a
92
+ * URL with a second one in it reads as broken even though the grammar permits it.
98
93
  */
94
+ const QueryLiterals = "$,/:;@";
95
+
96
+ const EscapedQueryLiterals = new RegExp(
97
+ [...QueryLiterals].map((char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).join("|"),
98
+ "g",
99
+ );
100
+
101
+ /**
102
+ * Percent-encode a key or value for a query string, escaping only what delimits something
103
+ * @param {string} component - The key or value to encode
104
+ * @returns {string} The encoded component
105
+ */
106
+ function encodeQueryComponent(component) {
107
+ return encodeURIComponent(component).replace(EscapedQueryLiterals, decodeURIComponent);
108
+ }
109
+
99
110
  /**
100
111
  * Append a parameter to the URL
101
112
  * @param {string} url - Current URL
@@ -105,13 +116,20 @@ export function parseQueryString(queryString, paramTypes = {}) {
105
116
  * @returns {Object} Updated URL and separator
106
117
  */
107
118
  function appendParam(url, sep, key, value = undefined) {
108
- url += sep + encodeURIComponent(key);
119
+ url += sep + encodeQueryComponent(key);
109
120
  if (value !== undefined) {
110
- url += "=" + encodeURIComponent(value);
121
+ url += "=" + encodeQueryComponent(value);
111
122
  }
112
123
  return { url, sep: "&" };
113
124
  }
114
125
 
126
+ /**
127
+ * Build a URL string from base URL and query parameters
128
+ * @param {string} baseUrl - The base URL (without query string)
129
+ * @param {Object} parsedQuery - Object containing query parameters
130
+ * @param {Object.<string, ParamType>} [paramTypes={}] - Object mapping parameter names to their types
131
+ * @returns {string} The complete URL with query parameters
132
+ */
115
133
  export function buildUrlFromParams(baseUrl, parsedQuery, paramTypes = {}) {
116
134
  let url = baseUrl;
117
135
  let sep = "?";