@brett_lamy/docstream 1.10.0 → 1.12.0

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.
Files changed (63) hide show
  1. package/README.md +25 -1
  2. package/dist/docs/DocsRenderer.d.ts.map +1 -1
  3. package/dist/docs/DocsRenderer.js +2 -1
  4. package/dist/docs/DocsRenderer.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strudel/StepGrid.d.ts +10 -0
  10. package/dist/strudel/StepGrid.d.ts.map +1 -0
  11. package/dist/strudel/StepGrid.js +78 -0
  12. package/dist/strudel/StepGrid.js.map +1 -0
  13. package/dist/strudel/StrudelPlayer.d.ts +7 -1
  14. package/dist/strudel/StrudelPlayer.d.ts.map +1 -1
  15. package/dist/strudel/StrudelPlayer.js +37 -8
  16. package/dist/strudel/StrudelPlayer.js.map +1 -1
  17. package/dist/strudel/StrudelRack.d.ts +24 -0
  18. package/dist/strudel/StrudelRack.d.ts.map +1 -0
  19. package/dist/strudel/StrudelRack.js +123 -0
  20. package/dist/strudel/StrudelRack.js.map +1 -0
  21. package/dist/strudel/ast/edit.d.ts +41 -0
  22. package/dist/strudel/ast/edit.d.ts.map +1 -0
  23. package/dist/strudel/ast/edit.js +78 -0
  24. package/dist/strudel/ast/edit.js.map +1 -0
  25. package/dist/strudel/ast/index.d.ts +7 -0
  26. package/dist/strudel/ast/index.d.ts.map +1 -0
  27. package/dist/strudel/ast/index.js +4 -0
  28. package/dist/strudel/ast/index.js.map +1 -0
  29. package/dist/strudel/ast/mini.d.ts +86 -0
  30. package/dist/strudel/ast/mini.d.ts.map +1 -0
  31. package/dist/strudel/ast/mini.js +269 -0
  32. package/dist/strudel/ast/mini.js.map +1 -0
  33. package/dist/strudel/ast/program.d.ts +93 -0
  34. package/dist/strudel/ast/program.d.ts.map +1 -0
  35. package/dist/strudel/ast/program.js +288 -0
  36. package/dist/strudel/ast/program.js.map +1 -0
  37. package/dist/strudel/controls.d.ts +21 -0
  38. package/dist/strudel/controls.d.ts.map +1 -0
  39. package/dist/strudel/controls.js +115 -0
  40. package/dist/strudel/controls.js.map +1 -0
  41. package/dist/strudel/engine.d.ts +5 -0
  42. package/dist/strudel/engine.d.ts.map +1 -1
  43. package/dist/strudel/engine.js +14 -0
  44. package/dist/strudel/engine.js.map +1 -1
  45. package/dist/strudel/index.d.ts +9 -1
  46. package/dist/strudel/index.d.ts.map +1 -1
  47. package/dist/strudel/index.js +5 -1
  48. package/dist/strudel/index.js.map +1 -1
  49. package/dist/styles.css +598 -0
  50. package/package.json +2 -1
  51. package/src/docs/DocsRenderer.tsx +2 -1
  52. package/src/index.ts +3 -2
  53. package/src/strudel/StepGrid.tsx +130 -0
  54. package/src/strudel/StrudelPlayer.tsx +79 -8
  55. package/src/strudel/StrudelRack.tsx +322 -0
  56. package/src/strudel/ast/edit.ts +95 -0
  57. package/src/strudel/ast/index.ts +19 -0
  58. package/src/strudel/ast/mini.ts +319 -0
  59. package/src/strudel/ast/program.ts +348 -0
  60. package/src/strudel/controls.tsx +182 -0
  61. package/src/strudel/engine.ts +14 -0
  62. package/src/strudel/index.ts +17 -1
  63. package/src/styles.css +598 -0
@@ -0,0 +1,95 @@
1
+ /* Exact source edits on a parsed song: each helper returns new code that differs only in the characters the
2
+ change touches, so comments, formatting and everything the rack doesn't understand survive. */
3
+ import type { MiniAtom } from "./mini"
4
+ import { findCall, type StrudelArg, type StrudelChain, type StrudelSong, type StrudelTrack } from "./program"
5
+
6
+ export interface CodeEdit {
7
+ start: number
8
+ end: number
9
+ text: string
10
+ }
11
+
12
+ /** Apply non-overlapping edits (in any order). */
13
+ export function applyEdits(code: string, edits: CodeEdit[]): string {
14
+ const sorted = [...edits].sort((a, b) => b.start - a.start)
15
+ for (let i = 1; i < sorted.length; i++) {
16
+ if (sorted[i]!.end > sorted[i - 1]!.start) throw new Error("Overlapping edits")
17
+ }
18
+ return sorted.reduce((out, e) => out.slice(0, e.start) + e.text + out.slice(e.end), code)
19
+ }
20
+
21
+ /** A compact literal: at most 4 significant decimals, no trailing zeros, `.5` style like hand-written Strudel. */
22
+ export function formatNumber(value: number): string {
23
+ if (!Number.isFinite(value)) return "0"
24
+ const abs = Math.abs(value)
25
+ const digits = abs >= 1000 ? 0 : abs >= 100 ? 1 : abs >= 10 ? 2 : abs >= 1 ? 3 : 4
26
+ let s = String(+value.toFixed(digits))
27
+ if (s.startsWith("0.")) s = s.slice(1)
28
+ else if (s.startsWith("-0.")) s = "-" + s.slice(2)
29
+ return s
30
+ }
31
+
32
+ /** Replace one argument's source text. */
33
+ export function setArg(code: string, arg: StrudelArg, text: string): string {
34
+ return applyEdits(code, [{ start: arg.start, end: arg.end, text }])
35
+ }
36
+
37
+ /**
38
+ * Set a method's first argument (`.lpf(800)`), adding `.name(value)` at the end of the chain when it
39
+ * isn't there yet. `value` is source text: pass `formatNumber(n)` or a quoted string.
40
+ */
41
+ export function setMethod(code: string, chain: StrudelChain, name: string, value: string): string {
42
+ const call = findCall(chain, name)
43
+ if (!call) return applyEdits(code, [{ start: chain.end, end: chain.end, text: `.${name}(${value})` }])
44
+ const first = call.args[0]
45
+ if (first) return applyEdits(code, [{ start: first.start, end: first.end, text: value }])
46
+ return applyEdits(code, [{ start: call.argsRange.start, end: call.argsRange.end, text: value }])
47
+ }
48
+
49
+ /** Remove every `.name(...)` from the chain. */
50
+ export function removeMethod(code: string, chain: StrudelChain, name: string): string {
51
+ const edits = chain.methods.filter((m) => m.name === name).map((m) => ({ ...m.methodRange, text: "" }))
52
+ return edits.length ? applyEdits(code, edits) : code
53
+ }
54
+
55
+ /** Set the tempo in cycles per second, keeping `setcpm` code in cpm; adds `setcps(…)` when there is none. */
56
+ export function setTempo(song: StrudelSong, cps: number): string {
57
+ const { code, tempo } = song
58
+ if (!tempo) return `setcps(${formatNumber(cps)})\n${code}`
59
+ const value = tempo.unit === "cpm" ? formatNumber(cps * 60) : formatNumber(cps)
60
+ const first = tempo.call.args[0]
61
+ return first
62
+ ? applyEdits(code, [{ start: first.start, end: first.end, text: value }])
63
+ : applyEdits(code, [{ start: tempo.call.argsRange.start, end: tempo.call.argsRange.end, text: value }])
64
+ }
65
+
66
+ /** Rewrite one atom of a mini-notation string (e.g. a step of a drum row: "hh" ⇄ "~"). */
67
+ export function setMiniAtom(code: string, mini: Extract<StrudelArg, { kind: "mini" }>, atom: MiniAtom, text: string): string {
68
+ return applyEdits(code, [{ start: mini.contentStart + atom.start, end: mini.contentStart + atom.end, text }])
69
+ }
70
+
71
+ /** Replace a mini-notation string's contents. */
72
+ export function setMiniSource(code: string, mini: Extract<StrudelArg, { kind: "mini" }>, source: string): string {
73
+ return applyEdits(code, [{ start: mini.contentStart, end: mini.end - 1, text: source }])
74
+ }
75
+
76
+ export interface StrudelMix {
77
+ /** Track indexes to silence. */
78
+ muted?: ReadonlySet<number>
79
+ /** When non-empty, only these tracks play. */
80
+ solo?: ReadonlySet<number>
81
+ }
82
+
83
+ /** Whether a track sounds under a mix (its own `_$:` mute included). */
84
+ export function isAudible(track: StrudelTrack, mix: StrudelMix = {}): boolean {
85
+ if (track.muted || mix.muted?.has(track.index)) return false
86
+ return !mix.solo?.size || mix.solo.has(track.index)
87
+ }
88
+
89
+ /** The code to play for a mix: silenced tracks become `silence`; the written code is untouched. */
90
+ export function mixCode(song: StrudelSong, mix: StrudelMix = {}): string {
91
+ const edits = song.tracks
92
+ .filter((t) => !t.muted && !isAudible(t, mix))
93
+ .map((t) => ({ start: t.start, end: t.end, text: "silence" }))
94
+ return edits.length ? applyEdits(song.code, edits) : song.code
95
+ }
@@ -0,0 +1,19 @@
1
+ export { MiniSyntaxError, miniAtoms, miniLayers, parseMini } from "./mini"
2
+ export type {
3
+ MiniAlternation,
4
+ MiniAtom,
5
+ MiniGroup,
6
+ MiniLayers,
7
+ MiniNode,
8
+ MiniOp,
9
+ MiniPolymeter,
10
+ MiniSequence,
11
+ MiniSlice,
12
+ MiniStep,
13
+ MiniSubcycle,
14
+ Range,
15
+ } from "./mini"
16
+ export { StrudelSyntaxError, chainCalls, findCall, parseStrudel } from "./program"
17
+ export type { StrudelArg, StrudelCall, StrudelChain, StrudelSong, StrudelTempo, StrudelTrack, TrackKind } from "./program"
18
+ export { applyEdits, formatNumber, isAudible, mixCode, removeMethod, setArg, setMethod, setMiniAtom, setMiniSource, setTempo } from "./edit"
19
+ export type { CodeEdit, StrudelMix } from "./edit"
@@ -0,0 +1,319 @@
1
+ /* Strudel mini-notation ("bd*2 [~ sd] <c e g>") as a syntax tree with source ranges.
2
+ Written from the grammar Strudel documents (krill), not from its parser: every node carries `start`/`end`
3
+ offsets into the string it was parsed from, so tools can rewrite exactly the characters a node covers. */
4
+
5
+ export interface Range {
6
+ start: number
7
+ end: number
8
+ }
9
+
10
+ /** A word: a sample or note name, a number, or a rest (`~`, `-`). */
11
+ export interface MiniAtom extends Range {
12
+ type: "atom"
13
+ value: string
14
+ rest: boolean
15
+ }
16
+
17
+ /** `[a b c]`: the steps share their parent's step. */
18
+ export interface MiniSubcycle extends Range {
19
+ type: "subcycle"
20
+ body: MiniGroup
21
+ }
22
+
23
+ /** `<a b c>`: one step per cycle (layers with `,` each alternate on their own). */
24
+ export interface MiniAlternation extends Range {
25
+ type: "alternation"
26
+ layers: MiniSequence[]
27
+ }
28
+
29
+ /** `{a b c, d e}%4`: layers stepped at a shared rate (the first layer's, or `%steps`). */
30
+ export interface MiniPolymeter extends Range {
31
+ type: "polymeter"
32
+ layers: MiniSequence[]
33
+ steps: MiniStep | null
34
+ }
35
+
36
+ export type MiniSlice = MiniAtom | MiniSubcycle | MiniAlternation | MiniPolymeter
37
+
38
+ export type MiniOp =
39
+ | (Range & { op: "fast" | "slow"; amount: MiniStep })
40
+ | (Range & { op: "replicate"; count: number | null })
41
+ | (Range & { op: "weight"; amount: number | null })
42
+ | (Range & { op: "degrade"; amount: number | null })
43
+ | (Range & { op: "euclid"; pulses: MiniStep; steps: MiniStep; rotation: MiniStep | null })
44
+ | (Range & { op: "tail"; value: MiniStep })
45
+ | (Range & { op: "range"; to: MiniStep })
46
+
47
+ /** One step of a sequence: a slice and the operators written after it (`bd*2`, `c@3`, `hh?`). */
48
+ export interface MiniStep extends Range {
49
+ type: "step"
50
+ slice: MiniSlice
51
+ ops: MiniOp[]
52
+ }
53
+
54
+ /** Steps played one after another within the cycle. `^` marks the sequence that sets the step count. */
55
+ export interface MiniSequence extends Range {
56
+ type: "sequence"
57
+ steps: MiniStep[]
58
+ marked: boolean
59
+ }
60
+
61
+ /** Sequences played together (`,`), picked at random each cycle (`|`), or concatenated as feet (`.`). */
62
+ export interface MiniLayers extends Range {
63
+ type: "stack" | "choose" | "feet"
64
+ layers: MiniSequence[]
65
+ }
66
+
67
+ export type MiniGroup = MiniSequence | MiniLayers
68
+ export type MiniNode = MiniGroup | MiniStep | MiniSlice
69
+
70
+ export class MiniSyntaxError extends Error {
71
+ constructor(message: string, readonly offset: number) {
72
+ super(`${message} at ${offset}`)
73
+ this.name = "MiniSyntaxError"
74
+ }
75
+ }
76
+
77
+ const WS = /[ \n\r\t ]/
78
+ const WORD = /[\p{L}\p{Nl}0-9~\-#.^_]/u
79
+ const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?/
80
+
81
+ /** Parse a mini-notation string. Throws `MiniSyntaxError` (with the offset) on invalid input. */
82
+ export function parseMini(source: string): MiniGroup {
83
+ let i = 0
84
+ const peek = (n = 0) => source[i + n] ?? ""
85
+ const skip = () => {
86
+ while (i < source.length && WS.test(source[i]!)) i++
87
+ }
88
+ const fail = (what: string): never => {
89
+ throw new MiniSyntaxError(i < source.length ? `Unexpected "${source[i]}", expected ${what}` : `Expected ${what}`, i)
90
+ }
91
+ const expect = (ch: string) => {
92
+ skip()
93
+ if (peek() !== ch) fail(`"${ch}"`)
94
+ i++
95
+ }
96
+ const number = (): number | null => {
97
+ const m = NUMBER.exec(source.slice(i))
98
+ if (!m) return null
99
+ i += m[0].length
100
+ return parseFloat(m[0])
101
+ }
102
+ // A separator `.` (feet) is a lone dot between spaces; inside a word it is part of the word ("0.5", "a.b").
103
+ const atFeetDot = () => peek() === "." && peek(1) !== "." && (i === 0 || WS.test(source[i - 1]!)) && (peek(1) === "" || WS.test(peek(1)))
104
+
105
+ function word(): MiniAtom | null {
106
+ const start = i
107
+ while (i < source.length && WORD.test(source[i]!)) {
108
+ // ".." starts a range operator, not part of the word.
109
+ if (source[i] === "." && source[i + 1] === ".") break
110
+ i++
111
+ }
112
+ const value = source.slice(start, i)
113
+ if (!value) return null
114
+ if (value === "." || value === "_") {
115
+ i = start
116
+ return null
117
+ }
118
+ return { type: "atom", value, rest: value === "~" || value === "-", start, end: i }
119
+ }
120
+
121
+ function slice(): MiniSlice | null {
122
+ skip()
123
+ const start = i
124
+ const ch = peek()
125
+ if (ch === "[") {
126
+ i++
127
+ const body = group(["]"])
128
+ expect("]")
129
+ return { type: "subcycle", body, start, end: i }
130
+ }
131
+ if (ch === "<") {
132
+ i++
133
+ const layers = commaLayers([">"])
134
+ expect(">")
135
+ return { type: "alternation", layers, start, end: i }
136
+ }
137
+ if (ch === "{") {
138
+ i++
139
+ const layers = commaLayers(["}"])
140
+ expect("}")
141
+ let steps: MiniStep | null = null
142
+ if (peek() === "%") {
143
+ i++
144
+ steps = bare() ?? fail("a step count after %")
145
+ }
146
+ return { type: "polymeter", layers, steps, start, end: i }
147
+ }
148
+ if (atFeetDot()) return null
149
+ return word()
150
+ }
151
+
152
+ // `*`, `/`, `:`, `..` and `%` take a bare slice: in "bd*2!" the `!` replicates bd, not 2.
153
+ function bare(): MiniStep | null {
154
+ const s = slice()
155
+ return s && { type: "step", slice: s, ops: [], start: s.start, end: s.end }
156
+ }
157
+
158
+ // A slice with its operators. `@`, `_` and `!` may stand apart ("a _ _", "a ! !"); the rest attach directly.
159
+ function step(): MiniStep | null {
160
+ const s = slice()
161
+ if (!s) return null
162
+ const ops: MiniOp[] = []
163
+ for (;;) {
164
+ const before = i
165
+ skip()
166
+ const start = i
167
+ const ch = peek()
168
+ if (ch === "@" || (ch === "_" && !WORD.test(peek(1)))) {
169
+ i++
170
+ ops.push({ op: "weight", amount: ch === "@" ? number() : null, start, end: i })
171
+ continue
172
+ }
173
+ if (ch === "!") {
174
+ i++
175
+ ops.push({ op: "replicate", count: number(), start, end: i })
176
+ continue
177
+ }
178
+ i = before
179
+ const at = i
180
+ const c = peek()
181
+ if (c === "*" || c === "/") {
182
+ i++
183
+ const amount = bare() ?? fail("an amount")
184
+ ops.push({ op: c === "*" ? "fast" : "slow", amount, start: at, end: i })
185
+ } else if (c === "?") {
186
+ i++
187
+ ops.push({ op: "degrade", amount: number(), start: at, end: i })
188
+ } else if (c === ":") {
189
+ i++
190
+ const value = bare() ?? fail("a value after :")
191
+ ops.push({ op: "tail", value, start: at, end: i })
192
+ } else if (c === "." && peek(1) === ".") {
193
+ i += 2
194
+ const to = bare() ?? fail("a range end")
195
+ ops.push({ op: "range", to, start: at, end: i })
196
+ } else if (c === "(") {
197
+ i++
198
+ const pulses = step() ?? fail("pulses")
199
+ expect(",")
200
+ const steps = step() ?? fail("steps")
201
+ skip()
202
+ let rotation: MiniStep | null = null
203
+ if (peek() === ",") {
204
+ i++
205
+ rotation = step()
206
+ }
207
+ expect(")")
208
+ ops.push({ op: "euclid", pulses, steps, rotation, start: at, end: i })
209
+ } else {
210
+ break
211
+ }
212
+ }
213
+ return { type: "step", slice: s, ops, start: s.start, end: i }
214
+ }
215
+
216
+ function sequence(closers: string[]): MiniSequence {
217
+ skip()
218
+ const start = i
219
+ let marked = false
220
+ if (peek() === "^") {
221
+ marked = true
222
+ i++
223
+ }
224
+ const steps: MiniStep[] = []
225
+ for (;;) {
226
+ skip()
227
+ const ch = peek()
228
+ if (!ch || closers.includes(ch) || ch === "," || ch === "|" || atFeetDot()) break
229
+ const s = step()
230
+ if (!s) fail("a step")
231
+ steps.push(s!)
232
+ }
233
+ if (!steps.length) fail("a step")
234
+ return { type: "sequence", steps, marked, start, end: steps[steps.length - 1]!.end }
235
+ }
236
+
237
+ function commaLayers(closers: string[]): MiniSequence[] {
238
+ const layers = [sequence(closers)]
239
+ for (;;) {
240
+ skip()
241
+ if (peek() !== ",") return layers
242
+ i++
243
+ layers.push(sequence(closers))
244
+ }
245
+ }
246
+
247
+ function group(closers: string[]): MiniGroup {
248
+ const first = sequence(closers)
249
+ skip()
250
+ const sep = atFeetDot() ? "." : peek()
251
+ if (sep !== "," && sep !== "|" && sep !== ".") return first
252
+ const type = sep === "," ? "stack" : sep === "|" ? "choose" : "feet"
253
+ const layers = [first]
254
+ for (;;) {
255
+ skip()
256
+ const next = atFeetDot() ? "." : peek()
257
+ if (next !== sep) break
258
+ i++
259
+ layers.push(sequence(closers))
260
+ }
261
+ return { type, layers, start: first.start, end: layers[layers.length - 1]!.end }
262
+ }
263
+
264
+ const root = group([])
265
+ skip()
266
+ if (i < source.length) fail("end of pattern")
267
+ return root
268
+ }
269
+
270
+ /**
271
+ * Every atom in the tree, in source order. `{ ops: false }` skips operator arguments (the 2 in `bd*2`, the 3,8
272
+ * in `bd(3,8)`), leaving only the values the pattern plays.
273
+ */
274
+ export function miniAtoms(node: MiniNode, { ops = true }: { ops?: boolean } = {}): MiniAtom[] {
275
+ const out: MiniAtom[] = []
276
+ const visit = (n: MiniNode | MiniOp | null | undefined) => {
277
+ if (!n) return
278
+ if ("op" in n) {
279
+ if (!ops) return
280
+ if (n.op === "fast" || n.op === "slow") visit(n.amount)
281
+ else if (n.op === "euclid") (visit(n.pulses), visit(n.steps), visit(n.rotation))
282
+ else if (n.op === "tail") visit(n.value)
283
+ else if (n.op === "range") visit(n.to)
284
+ return
285
+ }
286
+ switch (n.type) {
287
+ case "atom":
288
+ out.push(n)
289
+ break
290
+ case "step":
291
+ visit(n.slice)
292
+ n.ops.forEach(visit)
293
+ break
294
+ case "sequence":
295
+ n.steps.forEach(visit)
296
+ break
297
+ case "stack":
298
+ case "choose":
299
+ case "feet":
300
+ case "alternation":
301
+ n.layers.forEach(visit)
302
+ break
303
+ case "polymeter":
304
+ n.layers.forEach(visit)
305
+ visit(n.steps)
306
+ break
307
+ case "subcycle":
308
+ visit(n.body)
309
+ break
310
+ }
311
+ }
312
+ visit(node)
313
+ return out
314
+ }
315
+
316
+ /** The layers of a group: a sequence is one layer. */
317
+ export function miniLayers(group: MiniGroup): MiniSequence[] {
318
+ return group.type === "sequence" ? [group] : group.layers
319
+ }