@hraness/dawg 0.0.0-stage → 0.3.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.
- package/CHANGELOG.md +126 -0
- package/DAWG.md +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +292 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic printer: score → `song.ts` + `tracks/<slug>/track.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Output is already in prettier's shape (80 columns, double quotes, trailing
|
|
5
|
+
* commas, two-space indent, objects kept expanded) so formatting it again is
|
|
6
|
+
* a no-op, and printing the evaluation of a printed project reproduces the
|
|
7
|
+
* same bytes. Note ids are never printed: `song()` derives them from content.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
|
|
11
|
+
import { midiToPitch } from "../pitch.ts";
|
|
12
|
+
import {
|
|
13
|
+
isSamplerInstrument,
|
|
14
|
+
samplerVoiceSlots,
|
|
15
|
+
TrackScore,
|
|
16
|
+
type AutomationPoint,
|
|
17
|
+
type Note,
|
|
18
|
+
type SampleRef,
|
|
19
|
+
type Sampler,
|
|
20
|
+
type Track,
|
|
21
|
+
} from "../score.ts";
|
|
22
|
+
import { trackSlug } from "../slug.ts";
|
|
23
|
+
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
24
|
+
|
|
25
|
+
const WIDTH = 80;
|
|
26
|
+
const INDENT = " ";
|
|
27
|
+
|
|
28
|
+
/** One file the printer produces, path relative to the project root. */
|
|
29
|
+
export type PrintedFile = Readonly<{ path: string; text: string }>;
|
|
30
|
+
|
|
31
|
+
/** Everything a project needs for a score: `song.ts` first, then one file per track in score order. */
|
|
32
|
+
export type PrintedProject = Readonly<{
|
|
33
|
+
files: readonly PrintedFile[];
|
|
34
|
+
/** Track id → `tracks/<dir>/track.ts`. */
|
|
35
|
+
trackFiles: ReadonlyMap<string, string>;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
/** Directory under `tracks/` for each track, unique in score order. */
|
|
39
|
+
export function trackDirectories(
|
|
40
|
+
score: TrackScore,
|
|
41
|
+
): ReadonlyMap<string, string> {
|
|
42
|
+
const used = new Set<string>();
|
|
43
|
+
const out = new Map<string, string>();
|
|
44
|
+
for (const track of score.tracks) {
|
|
45
|
+
const base = trackSlug(track.name);
|
|
46
|
+
let dir = base;
|
|
47
|
+
for (let n = 2; used.has(dir); n += 1) dir = `${base}-${n}`;
|
|
48
|
+
used.add(dir);
|
|
49
|
+
out.set(track.id, dir);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Prints the whole project. Pure; returns frozen data. */
|
|
55
|
+
export function printProject(score: TrackScore): PrintedProject {
|
|
56
|
+
const dirs = trackDirectories(score);
|
|
57
|
+
const trackFiles = new Map<string, string>();
|
|
58
|
+
const files: PrintedFile[] = [
|
|
59
|
+
Object.freeze({ path: "song.ts", text: printSong(score, dirs) }),
|
|
60
|
+
];
|
|
61
|
+
for (const track of score.tracks) {
|
|
62
|
+
const path = `tracks/${dirs.get(track.id)!}/track.ts`;
|
|
63
|
+
trackFiles.set(track.id, path);
|
|
64
|
+
files.push(Object.freeze({ path, text: printTrack(score, track) }));
|
|
65
|
+
}
|
|
66
|
+
return Object.freeze({ files: Object.freeze(files), trackFiles });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const RESERVED = new Set([
|
|
70
|
+
"break",
|
|
71
|
+
"case",
|
|
72
|
+
"catch",
|
|
73
|
+
"class",
|
|
74
|
+
"const",
|
|
75
|
+
"continue",
|
|
76
|
+
"debugger",
|
|
77
|
+
"default",
|
|
78
|
+
"delete",
|
|
79
|
+
"do",
|
|
80
|
+
"else",
|
|
81
|
+
"enum",
|
|
82
|
+
"export",
|
|
83
|
+
"extends",
|
|
84
|
+
"false",
|
|
85
|
+
"finally",
|
|
86
|
+
"for",
|
|
87
|
+
"function",
|
|
88
|
+
"if",
|
|
89
|
+
"import",
|
|
90
|
+
"in",
|
|
91
|
+
"instanceof",
|
|
92
|
+
"new",
|
|
93
|
+
"null",
|
|
94
|
+
"return",
|
|
95
|
+
"super",
|
|
96
|
+
"switch",
|
|
97
|
+
"this",
|
|
98
|
+
"throw",
|
|
99
|
+
"true",
|
|
100
|
+
"try",
|
|
101
|
+
"typeof",
|
|
102
|
+
"var",
|
|
103
|
+
"void",
|
|
104
|
+
"while",
|
|
105
|
+
"with",
|
|
106
|
+
"yield",
|
|
107
|
+
"let",
|
|
108
|
+
"static",
|
|
109
|
+
"implements",
|
|
110
|
+
"interface",
|
|
111
|
+
"package",
|
|
112
|
+
"private",
|
|
113
|
+
"protected",
|
|
114
|
+
"public",
|
|
115
|
+
"await",
|
|
116
|
+
"song",
|
|
117
|
+
"track",
|
|
118
|
+
"note",
|
|
119
|
+
"seq",
|
|
120
|
+
"hit",
|
|
121
|
+
"hits",
|
|
122
|
+
"every",
|
|
123
|
+
"sampler",
|
|
124
|
+
"slices",
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
/** Import identifiers for each track, unique in score order. */
|
|
128
|
+
export function trackIdentifiers(
|
|
129
|
+
score: TrackScore,
|
|
130
|
+
dirs: ReadonlyMap<string, string> = trackDirectories(score),
|
|
131
|
+
): ReadonlyMap<string, string> {
|
|
132
|
+
const used = new Set<string>();
|
|
133
|
+
const out = new Map<string, string>();
|
|
134
|
+
for (const track of score.tracks) {
|
|
135
|
+
let base = dirs.get(track.id)!.replace(/-/g, "_");
|
|
136
|
+
if (!/^[a-z_]/.test(base) || RESERVED.has(base)) base = `track_${base}`;
|
|
137
|
+
let name = base;
|
|
138
|
+
for (let n = 2; used.has(name); n += 1) name = `${base}_${n}`;
|
|
139
|
+
used.add(name);
|
|
140
|
+
out.set(track.id, name);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `song.ts` for a score. */
|
|
146
|
+
export function printSong(
|
|
147
|
+
score: TrackScore,
|
|
148
|
+
dirs: ReadonlyMap<string, string> = trackDirectories(score),
|
|
149
|
+
): string {
|
|
150
|
+
const ids = trackIdentifiers(score, dirs);
|
|
151
|
+
const lines: string[] = ['import { song } from "dawg";'];
|
|
152
|
+
for (const track of score.tracks)
|
|
153
|
+
lines.push(
|
|
154
|
+
`import ${ids.get(track.id)!} from "./tracks/${dirs.get(track.id)!}/track.ts";`,
|
|
155
|
+
);
|
|
156
|
+
lines.push("");
|
|
157
|
+
const entries: string[] = [
|
|
158
|
+
`tempo: ${num(score.tempoBpm)}`,
|
|
159
|
+
`meter: [${num(score.beatsPerBar)}, 4]`,
|
|
160
|
+
`bars: ${num(score.bars)}`,
|
|
161
|
+
];
|
|
162
|
+
if (score.key !== null) entries.push(`key: ${str(score.key)}`);
|
|
163
|
+
if (score.ticksPerBeat !== 480)
|
|
164
|
+
entries.push(`ticksPerBeat: ${num(score.ticksPerBeat)}`);
|
|
165
|
+
entries.push(
|
|
166
|
+
`tracks: ${list(
|
|
167
|
+
score.tracks.map((track) => ids.get(track.id)!),
|
|
168
|
+
INDENT,
|
|
169
|
+
"tracks: ".length,
|
|
170
|
+
1,
|
|
171
|
+
)}`,
|
|
172
|
+
);
|
|
173
|
+
lines.push("export default song({");
|
|
174
|
+
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
175
|
+
lines.push("});", "");
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `tracks/<slug>/track.ts` for one track of a score. */
|
|
180
|
+
export function printTrack(score: TrackScore, track: Track): string {
|
|
181
|
+
const notes = score.notes.filter((note) => note.trackId === track.id);
|
|
182
|
+
const kit = isDrumInstrument(track.instrument);
|
|
183
|
+
const voiceSlots =
|
|
184
|
+
isSamplerInstrument(track.instrument) && track.sampler
|
|
185
|
+
? samplerVoiceSlots(track.sampler)
|
|
186
|
+
: undefined;
|
|
187
|
+
// Keyed samplers have no slots: their notes print as named pitches.
|
|
188
|
+
const slots = voiceSlots && voiceSlots.size > 0 ? voiceSlots : undefined;
|
|
189
|
+
const voiceFor = new Map<number, string>();
|
|
190
|
+
if (kit) for (const info of DRUM_VOICES) voiceFor.set(info.pitch, info.voice);
|
|
191
|
+
if (slots) for (const [voice, slot] of slots) voiceFor.set(slot, voice);
|
|
192
|
+
const used = new Set<string>(["track"]);
|
|
193
|
+
const printed = notes.map((note) => {
|
|
194
|
+
const voice = voiceFor.get(note.pitch);
|
|
195
|
+
if (voice !== undefined) {
|
|
196
|
+
used.add("hit");
|
|
197
|
+
return printHit(score, note, voice);
|
|
198
|
+
}
|
|
199
|
+
used.add("note");
|
|
200
|
+
return printNote(score, note, !kit && !slots);
|
|
201
|
+
});
|
|
202
|
+
if (track.sampler) used.add("sampler");
|
|
203
|
+
|
|
204
|
+
const entries: string[] = [
|
|
205
|
+
`id: ${str(track.id)}`,
|
|
206
|
+
`name: ${str(track.name)}`,
|
|
207
|
+
];
|
|
208
|
+
if (track.sampler) {
|
|
209
|
+
entries.push(`instrument: ${printSampler(track.sampler, INDENT)}`);
|
|
210
|
+
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
211
|
+
if (track.muted) entries.push("muted: true");
|
|
212
|
+
if (track.solo) entries.push("solo: true");
|
|
213
|
+
if (track.volume !== 1) entries.push(`volume: ${num(track.volume)}`);
|
|
214
|
+
if (track.pan !== 0) entries.push(`pan: ${num(track.pan)}`);
|
|
215
|
+
if (track.filter)
|
|
216
|
+
entries.push(
|
|
217
|
+
`filter: ${obj(
|
|
218
|
+
[
|
|
219
|
+
["cutoff", num(track.filter.cutoff)],
|
|
220
|
+
["resonance", num(track.filter.resonance)],
|
|
221
|
+
],
|
|
222
|
+
INDENT,
|
|
223
|
+
"filter: ".length,
|
|
224
|
+
1,
|
|
225
|
+
)}`,
|
|
226
|
+
);
|
|
227
|
+
if (track.delay)
|
|
228
|
+
entries.push(
|
|
229
|
+
`delay: ${obj(
|
|
230
|
+
[
|
|
231
|
+
["beats", num(track.delay.beats)],
|
|
232
|
+
["feedback", num(track.delay.feedback)],
|
|
233
|
+
["mix", num(track.delay.mix)],
|
|
234
|
+
],
|
|
235
|
+
INDENT,
|
|
236
|
+
"delay: ".length,
|
|
237
|
+
1,
|
|
238
|
+
)}`,
|
|
239
|
+
);
|
|
240
|
+
if (track.reverb)
|
|
241
|
+
entries.push(
|
|
242
|
+
`reverb: ${obj(
|
|
243
|
+
[
|
|
244
|
+
["mix", num(track.reverb.mix)],
|
|
245
|
+
["size", num(track.reverb.size)],
|
|
246
|
+
],
|
|
247
|
+
INDENT,
|
|
248
|
+
"reverb: ".length,
|
|
249
|
+
1,
|
|
250
|
+
)}`,
|
|
251
|
+
);
|
|
252
|
+
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
253
|
+
["volume", track.volumeAutomation],
|
|
254
|
+
["pan", track.panAutomation],
|
|
255
|
+
["filter", track.filterAutomation],
|
|
256
|
+
["resonance", track.resonanceAutomation],
|
|
257
|
+
["delayFeedback", track.delayFeedbackAutomation],
|
|
258
|
+
["delayMix", track.delayMixAutomation],
|
|
259
|
+
];
|
|
260
|
+
const automation = lanes.filter(([, points]) => points && points.length > 0);
|
|
261
|
+
if (automation.length > 0) {
|
|
262
|
+
const inner = INDENT + INDENT;
|
|
263
|
+
const body = automation.map(([lane, points]) => {
|
|
264
|
+
const items = points!.map(
|
|
265
|
+
(point) =>
|
|
266
|
+
`[${num(point.tick / score.ticksPerBeat)}, ${num(point.value)}]`,
|
|
267
|
+
);
|
|
268
|
+
// Prettier forces a break on arrays of 2+ arrays that each hold 2+ items.
|
|
269
|
+
return `${inner}${lane}: ${list(items, inner, `${lane}: `.length, 1, items.length > 1)},`;
|
|
270
|
+
});
|
|
271
|
+
entries.push(`automation: {\n${body.join("\n")}\n${INDENT}}`);
|
|
272
|
+
}
|
|
273
|
+
entries.push(`notes: ${list(printed, INDENT, "notes: ".length, 1)}`);
|
|
274
|
+
|
|
275
|
+
const names = ["track", "note", "seq", "hit", "hits", "every", "sampler"]
|
|
276
|
+
.filter((name) => used.has(name))
|
|
277
|
+
.join(", ");
|
|
278
|
+
const lines = [
|
|
279
|
+
`import { ${names} } from "dawg";`,
|
|
280
|
+
"",
|
|
281
|
+
"export default track({",
|
|
282
|
+
];
|
|
283
|
+
for (const entry of entries) lines.push(`${INDENT}${entry},`);
|
|
284
|
+
lines.push("});", "");
|
|
285
|
+
return lines.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function printNote(score: TrackScore, note: Note, named: boolean): string {
|
|
289
|
+
const pitch = named ? str(midiToPitch(note.pitch)) : num(note.pitch);
|
|
290
|
+
const args = [pitch, num(note.startTick / score.ticksPerBeat)];
|
|
291
|
+
const length = note.durationTicks / score.ticksPerBeat;
|
|
292
|
+
if (length !== 1 || note.velocity !== DEFAULT_VELOCITY)
|
|
293
|
+
args.push(num(length));
|
|
294
|
+
if (note.velocity !== DEFAULT_VELOCITY) args.push(num(note.velocity));
|
|
295
|
+
return `note(${args.join(", ")})`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function printHit(score: TrackScore, note: Note, voice: string): string {
|
|
299
|
+
const args = [str(voice), num(note.startTick / score.ticksPerBeat)];
|
|
300
|
+
const length = note.durationTicks / score.ticksPerBeat;
|
|
301
|
+
if (note.velocity !== DEFAULT_VELOCITY || length !== DEFAULT_HIT_LENGTH)
|
|
302
|
+
args.push(num(note.velocity));
|
|
303
|
+
if (length !== DEFAULT_HIT_LENGTH) args.push(num(length));
|
|
304
|
+
return `hit(${args.join(", ")})`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function printSampler(sampler: Sampler, indent: string): string {
|
|
308
|
+
const inner = indent + INDENT;
|
|
309
|
+
const voices = Object.keys(sampler.voices)
|
|
310
|
+
.sort()
|
|
311
|
+
.map((name) => {
|
|
312
|
+
const key = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : str(name);
|
|
313
|
+
return `${inner}${key}: ${printSample(sampler.voices[name]!, inner, `${key}: `.length)},`;
|
|
314
|
+
});
|
|
315
|
+
const body = `{\n${voices.join("\n")}\n${indent}}`;
|
|
316
|
+
if (sampler.mode === "oneshot") return `sampler(${body})`;
|
|
317
|
+
// Prettier breaks every argument out when an earlier one contains a break.
|
|
318
|
+
const shifted = `{\n${voices.map((line) => INDENT + line).join("\n")}\n${inner}}`;
|
|
319
|
+
return `sampler(\n${inner}${shifted},\n${inner}{ mode: ${str(sampler.mode)} },\n${indent})`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
323
|
+
const entries: [string, string][] = [["src", str(ref.src)]];
|
|
324
|
+
if (ref.root !== undefined)
|
|
325
|
+
entries.push(["root", str(midiToPitch(ref.root))]);
|
|
326
|
+
if (ref.begin !== undefined) entries.push(["begin", num(ref.begin)]);
|
|
327
|
+
if (ref.end !== undefined) entries.push(["end", num(ref.end)]);
|
|
328
|
+
if (ref.gain !== undefined) entries.push(["gain", num(ref.gain)]);
|
|
329
|
+
if (ref.speed !== undefined) entries.push(["speed", num(ref.speed)]);
|
|
330
|
+
if (ref.loop !== undefined) entries.push(["loop", String(ref.loop)]);
|
|
331
|
+
if (ref.choke !== undefined) entries.push(["choke", str(ref.choke)]);
|
|
332
|
+
if (entries.length === 1) return str(ref.src);
|
|
333
|
+
return obj(entries, indent, prefix, 1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Object literal: inline when the line fits, expanded otherwise (prettier keeps both). */
|
|
337
|
+
function obj(
|
|
338
|
+
entries: readonly (readonly [string, string])[],
|
|
339
|
+
indent: string,
|
|
340
|
+
prefix: number,
|
|
341
|
+
trailing: number,
|
|
342
|
+
): string {
|
|
343
|
+
const inline = `{ ${entries.map(([k, v]) => `${k}: ${v}`).join(", ")} }`;
|
|
344
|
+
if (indent.length + prefix + inline.length + trailing <= WIDTH) return inline;
|
|
345
|
+
const inner = indent + INDENT;
|
|
346
|
+
return `{\n${entries.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Array literal: inline when the line fits (and no forced break), else one item per line. */
|
|
350
|
+
function list(
|
|
351
|
+
items: readonly string[],
|
|
352
|
+
indent: string,
|
|
353
|
+
prefix: number,
|
|
354
|
+
trailing: number,
|
|
355
|
+
forceBreak = false,
|
|
356
|
+
): string {
|
|
357
|
+
if (items.length === 0) return "[]";
|
|
358
|
+
const inline = `[${items.join(", ")}]`;
|
|
359
|
+
if (
|
|
360
|
+
!forceBreak &&
|
|
361
|
+
!inline.includes("\n") &&
|
|
362
|
+
indent.length + prefix + inline.length + trailing <= WIDTH
|
|
363
|
+
)
|
|
364
|
+
return inline;
|
|
365
|
+
const inner = indent + INDENT;
|
|
366
|
+
return `[\n${items.map((item) => `${inner}${item},`).join("\n")}\n${indent}]`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Number the way prettier normalizes literals. */
|
|
370
|
+
export function num(value: number): string {
|
|
371
|
+
if (!Number.isFinite(value)) throw new RangeError(`cannot print ${value}`);
|
|
372
|
+
return String(value)
|
|
373
|
+
.replace(/e\+/, "e")
|
|
374
|
+
.replace(/^(-?)\./, "$10.");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Double-quoted string unless it holds more `"` than `'`, like prettier. */
|
|
378
|
+
export function str(value: string): string {
|
|
379
|
+
const doubles = (value.match(/"/g) ?? []).length;
|
|
380
|
+
const singles = (value.match(/'/g) ?? []).length;
|
|
381
|
+
const quote = doubles > singles ? "'" : '"';
|
|
382
|
+
const escaped = value
|
|
383
|
+
.replace(/\\/g, "\\\\")
|
|
384
|
+
.replace(new RegExp(quote, "g"), `\\${quote}`)
|
|
385
|
+
.replace(/\n/g, "\\n")
|
|
386
|
+
.replace(/\r/g, "\\r")
|
|
387
|
+
.replace(/\t/g, "\\t")
|
|
388
|
+
.replace(
|
|
389
|
+
/[\x00-\x1f\u2028\u2029]/g,
|
|
390
|
+
(c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
391
|
+
);
|
|
392
|
+
return `${quote}${escaped}${quote}`;
|
|
393
|
+
}
|