rockstar-strudel 1.0.3 → 1.0.7
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/PUBLISH.md +102 -0
- package/README.md +90 -41
- package/package.json +1 -1
- package/src/index.js +325 -11
- package/test/rockstar.test.js +130 -1
- package/verify_state.ipynb +203 -0
package/PUBLISH.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Publishing rockstar-strudel to npm
|
|
2
|
+
|
|
3
|
+
This package is ready to publish from the repo root.
|
|
4
|
+
|
|
5
|
+
## 1. Log in to npm
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm login
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
If you already have a token-based setup, make sure you are logged in as the correct account:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm whoami
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Run the tests
|
|
20
|
+
|
|
21
|
+
From the project root:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
You already verified the suite is passing.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Bump the version
|
|
32
|
+
|
|
33
|
+
Current version is the one in [package.json](package.json).
|
|
34
|
+
|
|
35
|
+
For a patch release:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm version patch
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For a minor release:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm version minor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a major release:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm version major
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This updates the version and creates a git tag.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 4. Preview what will be published
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm pack --dry-run
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Check that only the expected files are included.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 5. Publish to npm
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm publish
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you ever need public access explicitly:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm publish --access public
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 6. Push git commits and tags
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git push && git push --tags
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Handy one-release flow
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm test && npm version patch && npm publish && git push && git push --tags
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
- Package name: `rockstar-strudel`
|
|
100
|
+
- Entry point: [src/index.js](src/index.js)
|
|
101
|
+
- npm metadata is defined in [package.json](package.json)
|
|
102
|
+
- If `npm publish` fails because the version already exists, bump the version and try again.
|
package/README.md
CHANGED
|
@@ -4,29 +4,19 @@ Run [Rockstar](https://codewithrockstar.com) programs from the
|
|
|
4
4
|
[strudel.cc](https://strudel.cc) live-coding REPL (or any browser-based JS
|
|
5
5
|
environment) via a simple template-tag function.
|
|
6
6
|
|
|
7
|
-
```js
|
|
8
|
-
const data = await rockstar`
|
|
9
|
-
My heart is 123
|
|
10
|
-
Let your love be 456
|
|
11
|
-
Put 789 into the night
|
|
12
|
-
Shout my heart. Scream your love. Whisper the night.
|
|
13
|
-
`
|
|
14
|
-
// data === [123, 456, 789]
|
|
15
|
-
```
|
|
16
|
-
|
|
17
7
|
Every value printed by `Say` / `Shout` / `Scream` / `Whisper` becomes one
|
|
18
|
-
element of the returned array.
|
|
19
|
-
|
|
8
|
+
element of the returned array. The default `rockstar` view is now
|
|
9
|
+
numeric-first, so printed text is converted using Rockstar's poetic numeric
|
|
10
|
+
literal rules when needed.
|
|
11
|
+
|
|
12
|
+
For richer workflows (lyrics/text + numeric pipelines), use `rockstar_pro`.
|
|
20
13
|
|
|
21
14
|
---
|
|
22
15
|
|
|
23
16
|
## Using it in strudel.cc
|
|
24
17
|
|
|
25
|
-
This package is ESM-only. In `strudel.cc`, use dynamic `import()` rather than
|
|
26
|
-
static `import ... from`, since the editor input is not a module file.
|
|
27
|
-
|
|
28
18
|
```js
|
|
29
|
-
const { init, rockstar } = await import('https://esm.sh/rockstar-strudel')
|
|
19
|
+
const { init, rockstar, rockstar_pro } = await import('https://esm.sh/rockstar-strudel')
|
|
30
20
|
|
|
31
21
|
// Pre-warm the WASM engine while other code loads (optional but recommended)
|
|
32
22
|
await init()
|
|
@@ -34,36 +24,58 @@ await init()
|
|
|
34
24
|
// Run a Rockstar program
|
|
35
25
|
const notes = await rockstar`
|
|
36
26
|
Tommy was 60
|
|
27
|
+
Say Tommy
|
|
37
28
|
Build Tommy up, up, up, up
|
|
38
|
-
|
|
29
|
+
Say Tommy
|
|
39
30
|
Build Tommy up
|
|
40
31
|
Shout Tommy
|
|
41
32
|
Build Tommy up, up
|
|
42
|
-
|
|
33
|
+
Scream Tommy
|
|
43
34
|
`
|
|
44
|
-
//
|
|
35
|
+
//notes === [60, 64, 65, 67]
|
|
45
36
|
|
|
46
|
-
note(notes).sound("piano")
|
|
47
|
-
|
|
37
|
+
note(seq(notes)).sound("piano")
|
|
38
|
+
|
|
39
|
+
// Rich result with parallel views
|
|
40
|
+
const pro = await rockstar_pro`
|
|
41
|
+
Say hello world
|
|
42
|
+
Shout [ "012", ["my dreams", "007"] ]
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
// Default numeric-first values for number-based Strudel functions
|
|
46
|
+
// pro.output === [55, [12, [26, 7]]]
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
// Mixed typed values with words preserved where possible
|
|
49
|
+
// pro.mixed_output === ["hello world", [12, ["my dreams", 7]]]
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import { init, rockstar } from 'https://esm.sh/rockstar-strudel'
|
|
51
|
+
// Fully stringified values for speech/text workflows
|
|
52
|
+
// pro.text_output === ["hello world", ["12", ["my dreams", "7"]]]
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
// Sanitized source tokens for Shaba/Shabda speech sample names
|
|
55
|
+
// pro.speech === ["say_hello_world", "shout__012_my_dreams_007_"]
|
|
56
|
+
// Use in Strudel as:
|
|
57
|
+
// samples('shabda/speech:'+pro.speech.join(','))
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Build Tommy up, up, up, up
|
|
60
|
-
Shout Tommy
|
|
61
|
-
`
|
|
59
|
+
// Raw callback lines from WASM
|
|
60
|
+
// pro.raw_output keeps trailing newlines exactly as emitted
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Exact source text executed (after template interpolation)
|
|
63
|
+
// pro.sourceText is available for lyric reuse
|
|
64
|
+
|
|
65
|
+
const root = 60
|
|
66
|
+
const melody = await rockstar_pro`
|
|
67
|
+
Tommy was ${root}
|
|
68
|
+
Build Tommy up, up, up, up
|
|
69
|
+
Shout Tommy
|
|
70
|
+
`
|
|
71
|
+
|
|
72
|
+
// Run the same template again with new interpolation values
|
|
73
|
+
const shifted = await melody.rerun(62)
|
|
74
|
+
// or derive from the previous interpolation array
|
|
75
|
+
const shiftedAgain = await shifted.rerun(([prevRoot]) => [prevRoot + 2])
|
|
65
76
|
```
|
|
66
77
|
|
|
78
|
+
|
|
67
79
|
### Template interpolations
|
|
68
80
|
|
|
69
81
|
JavaScript values can be spliced into the source, letting you parameterise
|
|
@@ -83,10 +95,31 @@ const data = await rockstar`
|
|
|
83
95
|
|
|
84
96
|
## API
|
|
85
97
|
|
|
86
|
-
### `rockstar(strings, ...values)` → `Promise<Array<number|
|
|
98
|
+
### `rockstar(strings, ...values)` → `Promise<Array<number|Array>>`
|
|
99
|
+
|
|
100
|
+
Tagged-template function. Runs the Rockstar source code and resolves with the
|
|
101
|
+
numeric-first output view, ready for number-based Strudel functions.
|
|
102
|
+
|
|
103
|
+
### `rockstar_pro(strings, ...values)` → `Promise<object>`
|
|
104
|
+
|
|
105
|
+
Tagged-template function with richer parallel output views:
|
|
106
|
+
|
|
107
|
+
- `sourceText`: exact Rockstar code that was executed.
|
|
108
|
+
- `raw_output`: raw callback lines from WASM (verbatim, including trailing newlines).
|
|
109
|
+
- `output`: numeric-first values (`number` or nested numeric arrays).
|
|
110
|
+
- `mixed_output`: mixed typed values with words preserved.
|
|
111
|
+
- `text_output`: fully stringified values for text/speech use.
|
|
112
|
+
- `speech`: sanitized line tokens derived from source code for Shabda speech
|
|
113
|
+
sample lookup (for example `samples('shabda/speech:'+prog.speech.join(','))`).
|
|
114
|
+
- `templateValues`: the interpolation values used for this run.
|
|
115
|
+
- `rerun(...values)`: run the same template again, replacing interpolation
|
|
116
|
+
values positionally. Calling `rerun()` with no arguments repeats the same run.
|
|
87
117
|
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
`output[i]`, `mixed_output[i]`, and `text_output[i]` always refer to the same
|
|
119
|
+
emitted line.
|
|
120
|
+
|
|
121
|
+
For JSON-style list output (for example `[ "012" ]`), all views parse the same
|
|
122
|
+
structure, and `output` resolves that case to `[12]`.
|
|
90
123
|
|
|
91
124
|
### `init([dotnetUrl])` → `Promise<void>`
|
|
92
125
|
|
|
@@ -103,12 +136,24 @@ call. Exported for testing.
|
|
|
103
136
|
Pure helper that converts a raw WASM callback line to a typed JS value.
|
|
104
137
|
Exported for testing.
|
|
105
138
|
|
|
139
|
+
### `parsePoeticNumber(text)` → `number | undefined`
|
|
140
|
+
|
|
141
|
+
Converts text using the Rockstar poetic numeric literal algorithm:
|
|
142
|
+
each word contributes one digit using its length modulo 10, hyphens count as
|
|
143
|
+
letters, apostrophes do not, statement-ending punctuation stops parsing, and
|
|
144
|
+
an ellipsis (`...` or `…`) introduces the decimal separator.
|
|
145
|
+
|
|
146
|
+
### `parseOutputLine(line)` → `object | undefined`
|
|
147
|
+
|
|
148
|
+
Parses one raw callback line into a dual-view structure used by
|
|
149
|
+
`rockstar_pro` (`raw`, `output`, `poetic`). Exported for testing.
|
|
150
|
+
|
|
106
151
|
---
|
|
107
152
|
|
|
108
153
|
## How it works
|
|
109
154
|
|
|
110
155
|
The Rockstar **Starship** engine is a .NET 9 application compiled to
|
|
111
|
-
WebAssembly. The built WASM is hosted at
|
|
156
|
+
WebAssembly. The built WASM is hosted at `https://stretchyboy.github.io/rockstar/wasm` instead of
|
|
112
157
|
`https://codewithrockstar.com/wasm/`. This package dynamically imports
|
|
113
158
|
`dotnet.js` from that URL, initialises the runtime, and calls
|
|
114
159
|
`RockstarRunner.Run(source, outputCallback, stdin, args)`, which is
|
|
@@ -119,6 +164,14 @@ the callback with the printed string (plus a trailing newline added by
|
|
|
119
164
|
`WasmIO.WriteLine` in C#). The tag strips whitespace and coerces numeric
|
|
120
165
|
strings to `number`.
|
|
121
166
|
|
|
167
|
+
`rockstar_pro` adds a parsing layer on top of that callback stream:
|
|
168
|
+
|
|
169
|
+
- JSON-style lists are parsed when possible.
|
|
170
|
+
- List members are converted recursively.
|
|
171
|
+
- `output` is numeric-ready for sequence/math pipelines.
|
|
172
|
+
- `mixed_output` preserves words while keeping numbers typed.
|
|
173
|
+
- `text_output` keeps everything stringified for speech/lyrics workflows.
|
|
174
|
+
|
|
122
175
|
---
|
|
123
176
|
|
|
124
177
|
## CORS requirement
|
|
@@ -130,7 +183,6 @@ Access-Control-Allow-Origin: *
|
|
|
130
183
|
```
|
|
131
184
|
|
|
132
185
|
Without this, browsers will block the cross-origin `import()` of `dotnet.js`.
|
|
133
|
-
See [PLAN.md](PLAN.md) for the exact steps needed in the rockstar fork.
|
|
134
186
|
|
|
135
187
|
---
|
|
136
188
|
|
|
@@ -139,6 +191,3 @@ See [PLAN.md](PLAN.md) for the exact steps needed in the rockstar fork.
|
|
|
139
191
|
```bash
|
|
140
192
|
npm test # run the 19 unit tests (pure JS logic, no WASM required)
|
|
141
193
|
```
|
|
142
|
-
|
|
143
|
-
Integration testing (actual Rockstar program execution) requires a browser
|
|
144
|
-
environment where the WASM can load; see [PLAN.md](PLAN.md) for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rockstar-strudel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Run Rockstar lang programs via the Starship WASM engine, returning output as a JS array. Designed for use in the strudel.cc live-coding REPL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/index.js
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
*
|
|
20
20
|
* CORS requirement
|
|
21
21
|
* ────────────────
|
|
22
|
-
* The WASM engine is loaded from https://codewithrockstar.com/wasm
|
|
22
|
+
* The WASM engine is loaded from https://stretchyboy.github.io/rockstar/ as https://codewithrockstar.com/wasm/ has broken CORS.
|
|
23
23
|
* That origin must serve its WASM assets with
|
|
24
24
|
* Access-Control-Allow-Origin: *
|
|
25
25
|
* See PLAN.md for instructions on enabling this in a rockstar fork.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
/** Default URL of the .NET WASM loader published by
|
|
28
|
+
/** Default URL of the .NET WASM loader published by stretchyboy.github.io. */
|
|
29
29
|
const DEFAULT_DOTNET_URL =
|
|
30
30
|
'https://stretchyboy.github.io/rockstar/wasm/wwwroot/_framework/dotnet.js';
|
|
31
31
|
|
|
@@ -38,6 +38,7 @@ const DEFAULT_DOTNET_URL =
|
|
|
38
38
|
*/
|
|
39
39
|
export const ALLOWED_URL_PREFIXES = [
|
|
40
40
|
'https://codewithrockstar.com/',
|
|
41
|
+
'https://stretchyboy.github.io/rockstar/',
|
|
41
42
|
'https://cdn.jsdelivr.net/',
|
|
42
43
|
'https://unpkg.com/',
|
|
43
44
|
'http://localhost:',
|
|
@@ -170,6 +171,249 @@ export function coerce(line) {
|
|
|
170
171
|
return Number.isFinite(num) ? num : trimmed;
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Convert text to a Rockstar-style poetic numeric literal.
|
|
176
|
+
*
|
|
177
|
+
* Rockstar counts each word's length modulo 10. Hyphens count as letters,
|
|
178
|
+
* apostrophes do not. Parsing stops at the end of the current statement
|
|
179
|
+
* (`.`, `!`, `?`, `;`, or a newline). An ellipsis (`...` or `…`) acts as the
|
|
180
|
+
* decimal separator.
|
|
181
|
+
*
|
|
182
|
+
* Examples:
|
|
183
|
+
* - "a panther, he ain't talkin' 'bout love." -> 1724644
|
|
184
|
+
* - "ice... a life unfulfilled" -> 3.141
|
|
185
|
+
*
|
|
186
|
+
* @param {string} text
|
|
187
|
+
* @returns {number | undefined}
|
|
188
|
+
*/
|
|
189
|
+
export function parsePoeticNumber(text) {
|
|
190
|
+
const source = String(text).replace(/…/g, '...');
|
|
191
|
+
const intDigits = [];
|
|
192
|
+
const fracDigits = [];
|
|
193
|
+
let currentWord = '';
|
|
194
|
+
let sawDecimal = false;
|
|
195
|
+
|
|
196
|
+
const pushWord = () => {
|
|
197
|
+
if (!currentWord) return;
|
|
198
|
+
|
|
199
|
+
const normalized = currentWord.replace(/'/g, '');
|
|
200
|
+
currentWord = '';
|
|
201
|
+
if (!normalized) return;
|
|
202
|
+
|
|
203
|
+
const digit = normalized.length % 10;
|
|
204
|
+
if (sawDecimal) {
|
|
205
|
+
fracDigits.push(String(digit));
|
|
206
|
+
} else {
|
|
207
|
+
intDigits.push(String(digit));
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
212
|
+
const char = source[i];
|
|
213
|
+
|
|
214
|
+
if (source.slice(i, i + 3) === '...') {
|
|
215
|
+
pushWord();
|
|
216
|
+
sawDecimal = true;
|
|
217
|
+
i += 2;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (char === '\n' || char === '.' || char === '!' || char === '?' || char === ';') {
|
|
222
|
+
pushWord();
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (/[\p{L}'-]/u.test(char)) {
|
|
227
|
+
currentWord += char;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pushWord();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pushWord();
|
|
235
|
+
|
|
236
|
+
if (intDigits.length === 0 && fracDigits.length === 0) return undefined;
|
|
237
|
+
|
|
238
|
+
const intPart = intDigits.length > 0 ? intDigits.join('') : '0';
|
|
239
|
+
const value =
|
|
240
|
+
fracDigits.length > 0
|
|
241
|
+
? Number(`${intPart}.${fracDigits.join('')}`)
|
|
242
|
+
: Number(intPart);
|
|
243
|
+
|
|
244
|
+
return Number.isFinite(value) ? value : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parse a single output line into the parallel views used by `rockstar_pro`.
|
|
249
|
+
*
|
|
250
|
+
* - `output` is numeric-first for Strudel number pipelines.
|
|
251
|
+
* - `mixed_output` preserves words while keeping numeric values typed.
|
|
252
|
+
* - `text_output` keeps the same shape but stringifies all values.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} line
|
|
255
|
+
* @returns {{
|
|
256
|
+
* raw: string,
|
|
257
|
+
* output: number|Array<number|Array<unknown>>,
|
|
258
|
+
* mixed_output: number|string|Array<unknown>,
|
|
259
|
+
* text_output: string|Array<unknown>
|
|
260
|
+
* } | undefined}
|
|
261
|
+
*/
|
|
262
|
+
export function parseOutputLine(line) {
|
|
263
|
+
const raw = line;
|
|
264
|
+
const trimmed = line.trimEnd();
|
|
265
|
+
if (trimmed === '') return undefined;
|
|
266
|
+
|
|
267
|
+
const parsedArray = _parseJsonArray(trimmed);
|
|
268
|
+
if (parsedArray !== undefined) {
|
|
269
|
+
const mixed_output = _toMixedArray(parsedArray);
|
|
270
|
+
const text_output = _toTextArray(mixed_output);
|
|
271
|
+
const output = _toPoeticArray(parsedArray);
|
|
272
|
+
return { raw, output, mixed_output, text_output };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const mixed_output = coerce(trimmed);
|
|
276
|
+
const text_output = _toTextValue(mixed_output);
|
|
277
|
+
const output = _toPoeticNumber(mixed_output);
|
|
278
|
+
return { raw, output, mixed_output, text_output };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Attempt to parse a JSON-style array string, otherwise return undefined.
|
|
283
|
+
* @param {string} text
|
|
284
|
+
* @returns {Array<unknown> | undefined}
|
|
285
|
+
*/
|
|
286
|
+
function _parseJsonArray(text) {
|
|
287
|
+
const startsLikeArray = text.startsWith('[') && text.endsWith(']');
|
|
288
|
+
if (!startsLikeArray) return undefined;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(text);
|
|
292
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
293
|
+
} catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Convert any parsed array value into mixed output values.
|
|
300
|
+
* Numeric-looking strings become numbers; other strings stay as text.
|
|
301
|
+
*
|
|
302
|
+
* @param {Array<unknown>} value
|
|
303
|
+
* @returns {Array<unknown>}
|
|
304
|
+
*/
|
|
305
|
+
function _toMixedArray(value) {
|
|
306
|
+
return value.map((item) => {
|
|
307
|
+
if (Array.isArray(item)) return _toMixedArray(item);
|
|
308
|
+
if (typeof item === 'string') {
|
|
309
|
+
const numeric = _parseNumberish(item);
|
|
310
|
+
return numeric !== undefined ? numeric : item;
|
|
311
|
+
}
|
|
312
|
+
return item;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Convert a value tree to text output values.
|
|
318
|
+
* Numbers and other scalars are stringified while preserving array shape.
|
|
319
|
+
*
|
|
320
|
+
* @param {unknown} value
|
|
321
|
+
* @returns {string|Array<unknown>}
|
|
322
|
+
*/
|
|
323
|
+
function _toTextValue(value) {
|
|
324
|
+
if (Array.isArray(value)) return _toTextArray(value);
|
|
325
|
+
if (typeof value === 'string') return value;
|
|
326
|
+
return String(value);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Convert any parsed array value into text output values.
|
|
331
|
+
*
|
|
332
|
+
* @param {Array<unknown>} value
|
|
333
|
+
* @returns {Array<unknown>}
|
|
334
|
+
*/
|
|
335
|
+
function _toTextArray(value) {
|
|
336
|
+
return value.map((item) => _toTextValue(item));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Convert any parsed array value into numeric-first output values.
|
|
341
|
+
* Output is strictly numbers or nested arrays of numbers.
|
|
342
|
+
*
|
|
343
|
+
* @param {Array<unknown>} value
|
|
344
|
+
* @returns {Array<number|Array<unknown>>}
|
|
345
|
+
*/
|
|
346
|
+
function _toPoeticArray(value) {
|
|
347
|
+
return value.map((item) => {
|
|
348
|
+
if (Array.isArray(item)) return _toPoeticArray(item);
|
|
349
|
+
return _toPoeticNumber(item);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Convert a single scalar value to a number using numeric parse first,
|
|
355
|
+
* then Rockstar poetic numeric literal parsing.
|
|
356
|
+
*
|
|
357
|
+
* @param {unknown} value
|
|
358
|
+
* @returns {number}
|
|
359
|
+
*/
|
|
360
|
+
function _toPoeticNumber(value) {
|
|
361
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
362
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
363
|
+
if (value === null) return 0;
|
|
364
|
+
|
|
365
|
+
if (typeof value === 'string') {
|
|
366
|
+
const numeric = _parseNumberish(value);
|
|
367
|
+
if (numeric !== undefined) return numeric;
|
|
368
|
+
|
|
369
|
+
const poetic = parsePoeticNumber(value);
|
|
370
|
+
return poetic !== undefined ? poetic : 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Parse text as a finite JS number.
|
|
378
|
+
* @param {string} text
|
|
379
|
+
* @returns {number | undefined}
|
|
380
|
+
*/
|
|
381
|
+
function _parseNumberish(text) {
|
|
382
|
+
const num = Number(String(text));
|
|
383
|
+
return Number.isFinite(num) ? num : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Resolve the interpolation values for a rerun of `rockstar_pro`.
|
|
388
|
+
*
|
|
389
|
+
* - `rerun()` repeats the previous interpolation set.
|
|
390
|
+
* - `rerun(v1, v2, ...)` replaces values positionally.
|
|
391
|
+
* - `rerun([v1, v2, ...])` replaces from an array.
|
|
392
|
+
* - `rerun(fn)` derives the next values from the previous array.
|
|
393
|
+
*
|
|
394
|
+
* @param {Array<unknown>} previousValues
|
|
395
|
+
* @param {...unknown} nextArgs
|
|
396
|
+
* @returns {Array<unknown>}
|
|
397
|
+
*/
|
|
398
|
+
export function resolveRerunValues(previousValues, ...nextArgs) {
|
|
399
|
+
if (nextArgs.length === 0) return [...previousValues];
|
|
400
|
+
|
|
401
|
+
if (nextArgs.length === 1) {
|
|
402
|
+
const [arg] = nextArgs;
|
|
403
|
+
|
|
404
|
+
if (typeof arg === 'function') {
|
|
405
|
+
const result = arg([...previousValues]);
|
|
406
|
+
return Array.isArray(result) ? [...result] : [result];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (Array.isArray(arg)) {
|
|
410
|
+
return [...arg];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return [...nextArgs];
|
|
415
|
+
}
|
|
416
|
+
|
|
173
417
|
/**
|
|
174
418
|
* Build the full Rockstar source string from a tagged-template call's parts.
|
|
175
419
|
*
|
|
@@ -192,11 +436,12 @@ export function buildSource(strings, ...values) {
|
|
|
192
436
|
}
|
|
193
437
|
|
|
194
438
|
/**
|
|
195
|
-
* Template tag that executes a Rockstar program and returns
|
|
196
|
-
*
|
|
439
|
+
* Template tag that executes a Rockstar program and returns a numeric-first
|
|
440
|
+
* array of every printed value (via `Say` / `Shout` / `Scream` / `Whisper`).
|
|
197
441
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
442
|
+
* Plain numeric strings stay numeric, and other printed text is converted
|
|
443
|
+
* using Rockstar poetic numeric literal rules. JSON-style lists are converted
|
|
444
|
+
* recursively to nested numeric arrays.
|
|
200
445
|
*
|
|
201
446
|
* The WASM engine is loaded lazily on the first call and cached for subsequent
|
|
202
447
|
* calls. You can call `init()` first to pre-warm the engine if desired.
|
|
@@ -212,22 +457,91 @@ export function buildSource(strings, ...values) {
|
|
|
212
457
|
*
|
|
213
458
|
* @param {TemplateStringsArray} strings
|
|
214
459
|
* @param {...*} values
|
|
215
|
-
* @returns {Promise<Array<number|
|
|
460
|
+
* @returns {Promise<Array<number|Array<unknown>>>}
|
|
216
461
|
*/
|
|
217
462
|
export async function rockstar(strings, ...values) {
|
|
463
|
+
const result = await rockstar_pro(strings, ...values);
|
|
464
|
+
return result.output;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Template tag that executes Rockstar and returns richer parallel output views:
|
|
469
|
+
*
|
|
470
|
+
* - `output`: numeric-first values for Strudel sequence/math use.
|
|
471
|
+
* - `mixed_output`: mixed typed values with words preserved.
|
|
472
|
+
* - `text_output`: all values stringified for speech/text workflows.
|
|
473
|
+
* - `raw_output`: unmodified callback lines from WASM.
|
|
474
|
+
* - `sourceText`: exact source string that was executed.
|
|
475
|
+
*
|
|
476
|
+
* @param {TemplateStringsArray} strings
|
|
477
|
+
* @param {...*} values
|
|
478
|
+
* @returns {Promise<{
|
|
479
|
+
* sourceText: string,
|
|
480
|
+
* templateValues: Array<unknown>,
|
|
481
|
+
* raw_output: Array<string>,
|
|
482
|
+
* output: Array<number|Array<unknown>>,
|
|
483
|
+
* mixed_output: Array<number|string|Array<unknown>>,
|
|
484
|
+
* text_output: Array<string|Array<unknown>>,
|
|
485
|
+
* speech: Array<string>, // samples('shabda/speech:'+prog.speech.join(','))
|
|
486
|
+
* rerun: (...values: Array<unknown>) => Promise<object>,
|
|
487
|
+
* getVariables: () => never,
|
|
488
|
+
* callFunction: (name: string, ...args: Array<unknown>) => never,
|
|
489
|
+
* listFunctions: () => never
|
|
490
|
+
* }>}
|
|
491
|
+
*/
|
|
492
|
+
export async function rockstar_pro(strings, ...values) {
|
|
218
493
|
const code = buildSource(strings, ...values);
|
|
219
494
|
const runner = await _runner();
|
|
220
|
-
const
|
|
495
|
+
const raw_output = [];
|
|
496
|
+
const output = [];
|
|
497
|
+
const mixed_output = [];
|
|
498
|
+
const text_output = [];
|
|
499
|
+
const lines = code.split('\n')
|
|
500
|
+
const speech = lines.map((x) => x.toLowerCase()
|
|
501
|
+
.trim()
|
|
502
|
+
.replaceAll(' ', '_')
|
|
503
|
+
.replace(/\W/g, ''))
|
|
504
|
+
.filter((x)=> x.length);
|
|
505
|
+
|
|
506
|
+
console.log(`samples('shabda/speech:'+prog.speech.join(','))`)
|
|
221
507
|
|
|
222
508
|
await runner.Run(
|
|
223
509
|
code,
|
|
224
510
|
(line) => {
|
|
225
|
-
|
|
226
|
-
|
|
511
|
+
raw_output.push(line);
|
|
512
|
+
|
|
513
|
+
const parsed = parseOutputLine(line);
|
|
514
|
+
if (!parsed) return;
|
|
515
|
+
|
|
516
|
+
output.push(parsed.output);
|
|
517
|
+
mixed_output.push(parsed.mixed_output);
|
|
518
|
+
text_output.push(parsed.text_output);
|
|
227
519
|
},
|
|
228
520
|
/* stdin */ '',
|
|
229
521
|
/* args */ ''
|
|
230
522
|
);
|
|
231
523
|
|
|
232
|
-
|
|
524
|
+
const unsupported = (featureName) => {
|
|
525
|
+
throw new Error(
|
|
526
|
+
`${featureName} is not available in the current JS-only wrapper. ` +
|
|
527
|
+
`It requires new JSExport methods in the WASM RockstarRunner.`
|
|
528
|
+
);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
sourceText: code,
|
|
533
|
+
templateValues: [...values],
|
|
534
|
+
raw_output,
|
|
535
|
+
output,
|
|
536
|
+
mixed_output,
|
|
537
|
+
text_output,
|
|
538
|
+
speech,
|
|
539
|
+
rerun: (...nextArgs) => {
|
|
540
|
+
const nextValues = resolveRerunValues(values, ...nextArgs);
|
|
541
|
+
return rockstar_pro(strings, ...nextValues);
|
|
542
|
+
},
|
|
543
|
+
getVariables: () => unsupported('getVariables()'),
|
|
544
|
+
callFunction: () => unsupported('callFunction()'),
|
|
545
|
+
listFunctions: () => unsupported('listFunctions()'),
|
|
546
|
+
};
|
|
233
547
|
}
|
package/test/rockstar.test.js
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, it } from 'node:test';
|
|
11
11
|
import assert from 'node:assert/strict';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
buildSource,
|
|
14
|
+
coerce,
|
|
15
|
+
isTrustedUrl,
|
|
16
|
+
parsePoeticNumber,
|
|
17
|
+
parseOutputLine,
|
|
18
|
+
resolveRerunValues,
|
|
19
|
+
} from '../src/index.js';
|
|
13
20
|
|
|
14
21
|
// ─── buildSource ────────────────────────────────────────────────────────────
|
|
15
22
|
|
|
@@ -157,3 +164,125 @@ describe('isTrustedUrl', () => {
|
|
|
157
164
|
assert.ok(!isTrustedUrl('http://codewithrockstar.com/wasm/dotnet.js'));
|
|
158
165
|
});
|
|
159
166
|
});
|
|
167
|
+
|
|
168
|
+
// ─── parsePoeticNumber ──────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe('parsePoeticNumber', () => {
|
|
171
|
+
it('maps word lengths to digits modulo 10', () => {
|
|
172
|
+
assert.equal(parsePoeticNumber('My dreams'), 26);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('ignores apostrophes and stops at statement punctuation', () => {
|
|
176
|
+
assert.equal(
|
|
177
|
+
parsePoeticNumber("a panther, he ain't talkin' 'bout love. Shout Tommy"),
|
|
178
|
+
1724644
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('treats an ellipsis as the decimal separator', () => {
|
|
183
|
+
assert.equal(
|
|
184
|
+
parsePoeticNumber("ice... a life unfulfilled, wakin' everybody up, taking booze and pills."),
|
|
185
|
+
3.1415926535
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('counts hyphens as letters', () => {
|
|
190
|
+
assert.equal(parsePoeticNumber('life-long.'), 9);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('supports the unicode ellipsis character too', () => {
|
|
194
|
+
assert.equal(
|
|
195
|
+
parsePoeticNumber('my… darkest nightmarish longings, my cravings, a symphony of suff\'ring that lasts life-long.'),
|
|
196
|
+
2.718281828459
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns undefined when there are no words', () => {
|
|
201
|
+
assert.equal(parsePoeticNumber('!!!'), undefined);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─── parseOutputLine ────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
describe('parseOutputLine', () => {
|
|
208
|
+
it('returns numeric-first output while preserving words in other views', () => {
|
|
209
|
+
const parsed = parseOutputLine('hello world\n');
|
|
210
|
+
assert.deepEqual(parsed, {
|
|
211
|
+
raw: 'hello world\n',
|
|
212
|
+
output: 55,
|
|
213
|
+
mixed_output: 'hello world',
|
|
214
|
+
text_output: 'hello world',
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('stringifies numbers in text_output', () => {
|
|
219
|
+
const parsed = parseOutputLine('123\n');
|
|
220
|
+
assert.deepEqual(parsed, {
|
|
221
|
+
raw: '123\n',
|
|
222
|
+
output: 123,
|
|
223
|
+
mixed_output: 123,
|
|
224
|
+
text_output: '123',
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('parses JSON-style lists and converts string numerals', () => {
|
|
229
|
+
const parsed = parseOutputLine('[ "012" ]\n');
|
|
230
|
+
assert.deepEqual(parsed, {
|
|
231
|
+
raw: '[ "012" ]\n',
|
|
232
|
+
output: [12],
|
|
233
|
+
mixed_output: [12],
|
|
234
|
+
text_output: ['12'],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('keeps mixed and text views alongside numeric-first output', () => {
|
|
239
|
+
const parsed = parseOutputLine('["3", ["my dreams", "007"]]\n');
|
|
240
|
+
assert.deepEqual(parsed, {
|
|
241
|
+
raw: '["3", ["my dreams", "007"]]\n',
|
|
242
|
+
output: [3, [26, 7]],
|
|
243
|
+
mixed_output: [3, ['my dreams', 7]],
|
|
244
|
+
text_output: ['3', ['my dreams', '7']],
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns undefined for blank output lines', () => {
|
|
249
|
+
assert.equal(parseOutputLine('\n'), undefined);
|
|
250
|
+
assert.equal(parseOutputLine('\r\n'), undefined);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('falls back to text in mixed/text views when malformed JSON list is printed', () => {
|
|
254
|
+
const parsed = parseOutputLine('[ nope ]\n');
|
|
255
|
+
assert.deepEqual(parsed, {
|
|
256
|
+
raw: '[ nope ]\n',
|
|
257
|
+
output: 4,
|
|
258
|
+
mixed_output: '[ nope ]',
|
|
259
|
+
text_output: '[ nope ]',
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ─── resolveRerunValues ────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
describe('resolveRerunValues', () => {
|
|
267
|
+
it('repeats the previous values when no args are provided', () => {
|
|
268
|
+
assert.deepEqual(resolveRerunValues([1, 'two', 3]), [1, 'two', 3]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('replaces values positionally from variadic args', () => {
|
|
272
|
+
assert.deepEqual(resolveRerunValues([1, 2, 3], 9, 8), [9, 8]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('replaces values from an array argument', () => {
|
|
276
|
+
assert.deepEqual(resolveRerunValues([1, 2, 3], [7, 6]), [7, 6]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('derives next values from a function', () => {
|
|
280
|
+
const next = resolveRerunValues([2, 4, 6], (prev) => prev.map((n) => n * 10));
|
|
281
|
+
assert.deepEqual(next, [20, 40, 60]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('wraps a non-array function result as a single interpolation value', () => {
|
|
285
|
+
const next = resolveRerunValues([2, 4, 6], () => 99);
|
|
286
|
+
assert.deepEqual(next, [99]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "code",
|
|
5
|
+
"execution_count": 1,
|
|
6
|
+
"id": "e8b054ac",
|
|
7
|
+
"metadata": {},
|
|
8
|
+
"outputs": [
|
|
9
|
+
{
|
|
10
|
+
"name": "stdout",
|
|
11
|
+
"output_type": "stream",
|
|
12
|
+
"text": [
|
|
13
|
+
"\n",
|
|
14
|
+
"> rockstar-strudel@1.0.4 test\n",
|
|
15
|
+
"> node --test test/*.test.js\n",
|
|
16
|
+
"\n",
|
|
17
|
+
"▶ buildSource\n",
|
|
18
|
+
" \u001b[32m✔ returns the raw template string when there are no interpolations \u001b[90m(0.835688ms)\u001b[39m\u001b[39m\n",
|
|
19
|
+
" \u001b[32m✔ splices a single string interpolation \u001b[90m(0.159772ms)\u001b[39m\u001b[39m\n",
|
|
20
|
+
" \u001b[32m✔ stringifies a numeric interpolation \u001b[90m(0.114796ms)\u001b[39m\u001b[39m\n",
|
|
21
|
+
" \u001b[32m✔ handles multiple interpolations in the correct order \u001b[90m(0.129431ms)\u001b[39m\u001b[39m\n",
|
|
22
|
+
" \u001b[32m✔ preserves leading and trailing whitespace / newlines in the template \u001b[90m(0.217994ms)\u001b[39m\u001b[39m\n",
|
|
23
|
+
" \u001b[32m✔ treats undefined interpolation values as empty strings \u001b[90m(0.098519ms)\u001b[39m\u001b[39m\n",
|
|
24
|
+
"\u001b[32m✔ buildSource \u001b[90m(2.715025ms)\u001b[39m\u001b[39m\n",
|
|
25
|
+
"▶ coerce\n",
|
|
26
|
+
" \u001b[32m✔ converts an integer string to a JS number \u001b[90m(0.608473ms)\u001b[39m\u001b[39m\n",
|
|
27
|
+
" \u001b[32m✔ converts a decimal string to a JS number \u001b[90m(0.590633ms)\u001b[39m\u001b[39m\n",
|
|
28
|
+
" \u001b[32m✔ converts a negative number string \u001b[90m(2.175091ms)\u001b[39m\u001b[39m\n",
|
|
29
|
+
" \u001b[32m✔ handles Windows-style CRLF line endings \u001b[90m(0.363412ms)\u001b[39m\u001b[39m\n",
|
|
30
|
+
" \u001b[32m✔ converts zero \u001b[90m(0.158039ms)\u001b[39m\u001b[39m\n",
|
|
31
|
+
" \u001b[32m✔ returns a string for non-numeric output \u001b[90m(0.194609ms)\u001b[39m\u001b[39m\n",
|
|
32
|
+
" \u001b[32m✔ returns a string for boolean-like output \u001b[90m(0.094095ms)\u001b[39m\u001b[39m\n",
|
|
33
|
+
" \u001b[32m✔ returns a string for null-like output \u001b[90m(0.055956ms)\u001b[39m\u001b[39m\n",
|
|
34
|
+
" \u001b[32m✔ returns undefined for an empty line \u001b[90m(0.046294ms)\u001b[39m\u001b[39m\n",
|
|
35
|
+
" \u001b[32m✔ returns undefined for a line containing only a newline \u001b[90m(0.044543ms)\u001b[39m\u001b[39m\n",
|
|
36
|
+
" \u001b[32m✔ does not coerce Infinity to a number (not finite) \u001b[90m(0.045679ms)\u001b[39m\u001b[39m\n",
|
|
37
|
+
" \u001b[32m✔ does not coerce NaN to a number \u001b[90m(0.044212ms)\u001b[39m\u001b[39m\n",
|
|
38
|
+
" \u001b[32m✔ strips multiple trailing newlines \u001b[90m(0.044264ms)\u001b[39m\u001b[39m\n",
|
|
39
|
+
"\u001b[32m✔ coerce \u001b[90m(5.028022ms)\u001b[39m\u001b[39m\n",
|
|
40
|
+
"▶ isTrustedUrl\n",
|
|
41
|
+
" \u001b[32m✔ trusts codewithrockstar.com \u001b[90m(0.192548ms)\u001b[39m\u001b[39m\n",
|
|
42
|
+
" \u001b[32m✔ trusts cdn.jsdelivr.net \u001b[90m(0.054461ms)\u001b[39m\u001b[39m\n",
|
|
43
|
+
" \u001b[32m✔ trusts unpkg.com \u001b[90m(0.047384ms)\u001b[39m\u001b[39m\n",
|
|
44
|
+
" \u001b[32m✔ trusts any *.github.io subdomain \u001b[90m(0.159751ms)\u001b[39m\u001b[39m\n",
|
|
45
|
+
" \u001b[32m✔ trusts localhost with a port \u001b[90m(0.10433ms)\u001b[39m\u001b[39m\n",
|
|
46
|
+
" \u001b[32m✔ trusts 127.0.0.1 with a port \u001b[90m(0.058382ms)\u001b[39m\u001b[39m\n",
|
|
47
|
+
" \u001b[32m✔ rejects an arbitrary https URL \u001b[90m(0.090083ms)\u001b[39m\u001b[39m\n",
|
|
48
|
+
" \u001b[32m✔ rejects a bare github.io URL without a subdomain \u001b[90m(0.054799ms)\u001b[39m\u001b[39m\n",
|
|
49
|
+
" \u001b[32m✔ rejects http (non-localhost) \u001b[90m(0.052302ms)\u001b[39m\u001b[39m\n",
|
|
50
|
+
"\u001b[32m✔ isTrustedUrl \u001b[90m(1.01552ms)\u001b[39m\u001b[39m\n",
|
|
51
|
+
"▶ parsePoeticNumber\n",
|
|
52
|
+
" \u001b[32m✔ maps word lengths to digits modulo 10 \u001b[90m(0.546635ms)\u001b[39m\u001b[39m\n",
|
|
53
|
+
" \u001b[32m✔ ignores apostrophes and stops at statement punctuation \u001b[90m(0.197288ms)\u001b[39m\u001b[39m\n",
|
|
54
|
+
" \u001b[32m✔ treats an ellipsis as the decimal separator \u001b[90m(0.099897ms)\u001b[39m\u001b[39m\n",
|
|
55
|
+
" \u001b[32m✔ counts hyphens as letters \u001b[90m(0.057811ms)\u001b[39m\u001b[39m\n",
|
|
56
|
+
" \u001b[32m✔ supports the unicode ellipsis character too \u001b[90m(0.076137ms)\u001b[39m\u001b[39m\n",
|
|
57
|
+
" \u001b[32m✔ returns undefined when there are no words \u001b[90m(0.05618ms)\u001b[39m\u001b[39m\n",
|
|
58
|
+
"\u001b[32m✔ parsePoeticNumber \u001b[90m(1.207209ms)\u001b[39m\u001b[39m\n",
|
|
59
|
+
"▶ parseOutputLine\n",
|
|
60
|
+
" \u001b[32m✔ returns numeric-first output while preserving words in other views \u001b[90m(0.870018ms)\u001b[39m\u001b[39m\n",
|
|
61
|
+
" \u001b[32m✔ stringifies numbers in text_output \u001b[90m(0.102378ms)\u001b[39m\u001b[39m\n",
|
|
62
|
+
" \u001b[32m✔ parses JSON-style lists and converts string numerals \u001b[90m(0.231375ms)\u001b[39m\u001b[39m\n",
|
|
63
|
+
" \u001b[32m✔ keeps mixed and text views alongside numeric-first output \u001b[90m(0.173793ms)\u001b[39m\u001b[39m\n",
|
|
64
|
+
" \u001b[32m✔ returns undefined for blank output lines \u001b[90m(0.097894ms)\u001b[39m\u001b[39m\n",
|
|
65
|
+
" \u001b[32m✔ falls back to text in mixed/text views when malformed JSON list is printed \u001b[90m(0.173253ms)\u001b[39m\u001b[39m\n",
|
|
66
|
+
"\u001b[32m✔ parseOutputLine \u001b[90m(1.835256ms)\u001b[39m\u001b[39m\n",
|
|
67
|
+
"▶ resolveRerunValues\n",
|
|
68
|
+
" \u001b[32m✔ repeats the previous values when no args are provided \u001b[90m(0.133796ms)\u001b[39m\u001b[39m\n",
|
|
69
|
+
" \u001b[32m✔ replaces values positionally from variadic args \u001b[90m(0.06069ms)\u001b[39m\u001b[39m\n",
|
|
70
|
+
" \u001b[32m✔ replaces values from an array argument \u001b[90m(0.3767ms)\u001b[39m\u001b[39m\n",
|
|
71
|
+
" \u001b[32m✔ derives next values from a function \u001b[90m(0.136754ms)\u001b[39m\u001b[39m\n",
|
|
72
|
+
" \u001b[32m✔ wraps a non-array function result as a single interpolation value \u001b[90m(0.080649ms)\u001b[39m\u001b[39m\n",
|
|
73
|
+
"\u001b[32m✔ resolveRerunValues \u001b[90m(0.943939ms)\u001b[39m\u001b[39m\n",
|
|
74
|
+
"\u001b[34mℹ tests 45\u001b[39m\n",
|
|
75
|
+
"\u001b[34mℹ suites 6\u001b[39m\n",
|
|
76
|
+
"\u001b[34mℹ pass 45\u001b[39m\n",
|
|
77
|
+
"\u001b[34mℹ fail 0\u001b[39m\n",
|
|
78
|
+
"\u001b[34mℹ cancelled 0\u001b[39m\n",
|
|
79
|
+
"\u001b[34mℹ skipped 0\u001b[39m\n",
|
|
80
|
+
"\u001b[34mℹ todo 0\u001b[39m\n",
|
|
81
|
+
"\u001b[34mℹ duration_ms 127.20622\u001b[39m\n",
|
|
82
|
+
"\n",
|
|
83
|
+
"{\"exports\":[\"ALLOWED_URL_PREFIXES\",\"GITHUB_IO_PATTERN\",\"buildSource\",\"coerce\",\"init\",\"isTrustedUrl\",\"parseOutputLine\",\"parsePoeticNumber\",\"resolveRerunValues\",\"rockstar\",\"rockstar_pro\"],\"hasRockstar\":true,\"hasRockstarPro\":true,\"parsedWordOutput\":{\"raw\":\"hello world\\n\",\"output\":55,\"mixed_output\":\"hello world\",\"text_output\":\"hello world\"},\"parsedNestedOutput\":{\"raw\":\"[\\\"3\\\", [\\\"my dreams\\\", \\\"007\\\"]]\\n\",\"output\":[3,[26,7]],\"mixed_output\":[3,[\"my dreams\",7]],\"text_output\":[\"3\",[\"my dreams\",\"7\"]]},\"rockstarReturnsStateOutput\":false,\"rockstarProExposesAliases\":true}\n",
|
|
84
|
+
"\n",
|
|
85
|
+
"\n",
|
|
86
|
+
"FINAL_VERIFICATION_JSON=\n",
|
|
87
|
+
"{\n",
|
|
88
|
+
" \"pass_fail\": \"PASS\",\n",
|
|
89
|
+
" \"summary_counts\": {},\n",
|
|
90
|
+
" \"failing_tests\": [],\n",
|
|
91
|
+
" \"rockstar_numeric_first\": false,\n",
|
|
92
|
+
" \"rockstar_pro_aliases\": true,\n",
|
|
93
|
+
" \"api_exports\": [\n",
|
|
94
|
+
" \"ALLOWED_URL_PREFIXES\",\n",
|
|
95
|
+
" \"GITHUB_IO_PATTERN\",\n",
|
|
96
|
+
" \"buildSource\",\n",
|
|
97
|
+
" \"coerce\",\n",
|
|
98
|
+
" \"init\",\n",
|
|
99
|
+
" \"isTrustedUrl\",\n",
|
|
100
|
+
" \"parseOutputLine\",\n",
|
|
101
|
+
" \"parsePoeticNumber\",\n",
|
|
102
|
+
" \"resolveRerunValues\",\n",
|
|
103
|
+
" \"rockstar\",\n",
|
|
104
|
+
" \"rockstar_pro\"\n",
|
|
105
|
+
" ]\n",
|
|
106
|
+
"}\n"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"source": [
|
|
111
|
+
"from pathlib import Path\n",
|
|
112
|
+
"import subprocess, json, re, textwrap\n",
|
|
113
|
+
"\n",
|
|
114
|
+
"repo = Path('/workspaces/rockstar-strudel')\n",
|
|
115
|
+
"\n",
|
|
116
|
+
"# Run the full test suite\n",
|
|
117
|
+
"proc = subprocess.run(\n",
|
|
118
|
+
" ['npm', 'test'],\n",
|
|
119
|
+
" cwd=repo,\n",
|
|
120
|
+
" capture_output=True,\n",
|
|
121
|
+
" text=True,\n",
|
|
122
|
+
")\n",
|
|
123
|
+
"\n",
|
|
124
|
+
"output = (proc.stdout or '') + ('\\n' + proc.stderr if proc.stderr else '')\n",
|
|
125
|
+
"print(output)\n",
|
|
126
|
+
"\n",
|
|
127
|
+
"counts = {}\n",
|
|
128
|
+
"for key in ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms']:\n",
|
|
129
|
+
" m = re.search(rf'#\\s+{key}\\s+(\\d+)', output)\n",
|
|
130
|
+
" if m:\n",
|
|
131
|
+
" counts[key] = int(m.group(1))\n",
|
|
132
|
+
"\n",
|
|
133
|
+
"failing_tests = []\n",
|
|
134
|
+
"for line in output.splitlines():\n",
|
|
135
|
+
" stripped = line.strip()\n",
|
|
136
|
+
" if stripped.startswith('not ok '):\n",
|
|
137
|
+
" failing_tests.append(stripped[7:])\n",
|
|
138
|
+
"\n",
|
|
139
|
+
"node_code = r'''\n",
|
|
140
|
+
"import * as mod from './src/index.js';\n",
|
|
141
|
+
"const result = {\n",
|
|
142
|
+
" exports: Object.keys(mod).sort(),\n",
|
|
143
|
+
" hasRockstar: typeof mod.rockstar === 'function',\n",
|
|
144
|
+
" hasRockstarPro: typeof mod.rockstar_pro === 'function',\n",
|
|
145
|
+
" parsedWordOutput: mod.parseOutputLine('hello world\\n'),\n",
|
|
146
|
+
" parsedNestedOutput: mod.parseOutputLine('[\"3\", [\"my dreams\", \"007\"]]\\n'),\n",
|
|
147
|
+
" rockstarReturnsStateOutput: /return\\s+state\\.output\\b/.test(String(mod.rockstar)),\n",
|
|
148
|
+
" rockstarProExposesAliases:\n",
|
|
149
|
+
" /output/.test(String(mod.rockstar_pro)) &&\n",
|
|
150
|
+
" /mixed_output/.test(String(mod.rockstar_pro)) &&\n",
|
|
151
|
+
" /text_output/.test(String(mod.rockstar_pro)) &&\n",
|
|
152
|
+
" /raw_output/.test(String(mod.rockstar_pro)),\n",
|
|
153
|
+
"};\n",
|
|
154
|
+
"console.log(JSON.stringify(result));\n",
|
|
155
|
+
"'''\n",
|
|
156
|
+
"api_proc = subprocess.run(\n",
|
|
157
|
+
" ['node', '--input-type=module', '-e', node_code],\n",
|
|
158
|
+
" cwd=repo,\n",
|
|
159
|
+
" capture_output=True,\n",
|
|
160
|
+
" text=True,\n",
|
|
161
|
+
")\n",
|
|
162
|
+
"print(api_proc.stdout)\n",
|
|
163
|
+
"if api_proc.stderr:\n",
|
|
164
|
+
" print(api_proc.stderr)\n",
|
|
165
|
+
"\n",
|
|
166
|
+
"api = json.loads(api_proc.stdout)\n",
|
|
167
|
+
"\n",
|
|
168
|
+
"verification = {\n",
|
|
169
|
+
" 'pass_fail': 'PASS' if proc.returncode == 0 else 'FAIL',\n",
|
|
170
|
+
" 'summary_counts': counts,\n",
|
|
171
|
+
" 'failing_tests': failing_tests,\n",
|
|
172
|
+
" 'rockstar_numeric_first': bool(api['rockstarReturnsStateOutput']) and api['parsedWordOutput']['output'] == 55,\n",
|
|
173
|
+
" 'rockstar_pro_aliases': bool(api['rockstarProExposesAliases']),\n",
|
|
174
|
+
" 'api_exports': api['exports'],\n",
|
|
175
|
+
"}\n",
|
|
176
|
+
"\n",
|
|
177
|
+
"print('\\nFINAL_VERIFICATION_JSON=')\n",
|
|
178
|
+
"print(json.dumps(verification, indent=2))"
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"metadata": {
|
|
183
|
+
"kernelspec": {
|
|
184
|
+
"display_name": "Python 3",
|
|
185
|
+
"language": "python",
|
|
186
|
+
"name": "python3"
|
|
187
|
+
},
|
|
188
|
+
"language_info": {
|
|
189
|
+
"codemirror_mode": {
|
|
190
|
+
"name": "ipython",
|
|
191
|
+
"version": 3
|
|
192
|
+
},
|
|
193
|
+
"file_extension": ".py",
|
|
194
|
+
"mimetype": "text/x-python",
|
|
195
|
+
"name": "python",
|
|
196
|
+
"nbconvert_exporter": "python",
|
|
197
|
+
"pygments_lexer": "ipython3",
|
|
198
|
+
"version": "3.12.1"
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"nbformat": 4,
|
|
202
|
+
"nbformat_minor": 5
|
|
203
|
+
}
|