prose-writer 0.1.2 → 0.2.3

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.
@@ -0,0 +1,1036 @@
1
+ class SafeString {
2
+ value;
3
+ constructor(value) {
4
+ this.value = value;
5
+ }
6
+ toString() {
7
+ return this.value;
8
+ }
9
+ valueOf() {
10
+ return this.value;
11
+ }
12
+ }
13
+ const escapeXmlText = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
14
+ const escapeMarkdownLineStart = (line) => {
15
+ if (/^\s*#{1,6}\s/.test(line)) {
16
+ return line.replace(/^(\s*)(#{1,6})/, '$1\\$2');
17
+ }
18
+ if (/^\s*>/.test(line)) {
19
+ return line.replace(/^(\s*)>/, '$1\\>');
20
+ }
21
+ if (/^\s*([-+*])\s/.test(line)) {
22
+ return line.replace(/^(\s*)([-+*])/, '$1\\$2');
23
+ }
24
+ if (/^\s*\d+\.\s/.test(line)) {
25
+ return line.replace(/^(\s*)(\d+)\./, '$1$2\\.');
26
+ }
27
+ return line;
28
+ };
29
+ const escapeMarkdownText = (value) => {
30
+ const escaped = escapeXmlText(value)
31
+ .replace(/([\\`*_~()|!])/g, '\\$1')
32
+ .replace(/\[/g, '\\[')
33
+ .replace(/\]/g, '\\]');
34
+ return escaped
35
+ .split('\n')
36
+ .map((line) => escapeMarkdownLineStart(line))
37
+ .join('\n');
38
+ };
39
+ const wrapInlineCode = (value) => {
40
+ const matches = value.match(/`+/g);
41
+ const fenceSize = (matches ? Math.max(...matches.map((m) => m.length)) : 0) + 1;
42
+ const fence = '`'.repeat(fenceSize);
43
+ const needsPadding = /^\s|\s$/.test(value);
44
+ const content = needsPadding ? ` ${value} ` : value;
45
+ return `${fence}${content}${fence}`;
46
+ };
47
+ const escapeLinkDestination = (url) => {
48
+ const encoded = encodeURI(url);
49
+ return encoded.replace(/[()]/g, '\\$&');
50
+ };
51
+ const sanitizeLinkDestination = (url) => {
52
+ const trimmed = url.trim();
53
+ const lower = trimmed.toLowerCase();
54
+ if (/^[a-z][a-z0-9+.-]*:/.test(lower) && !/^(https?|mailto):/.test(lower)) {
55
+ return '#';
56
+ }
57
+ return escapeLinkDestination(trimmed);
58
+ };
59
+ const toContentString = (content, trim = 'none') => {
60
+ if (content instanceof SafeString) {
61
+ return { value: content.toString(), isTrusted: true };
62
+ }
63
+ if (content instanceof ProseWriter) {
64
+ const raw = content.toString();
65
+ if (trim === 'end')
66
+ return { value: raw.trimEnd(), isTrusted: true };
67
+ if (trim === 'both')
68
+ return { value: raw.trim(), isTrusted: true };
69
+ return { value: raw, isTrusted: true };
70
+ }
71
+ return { value: String(content), isTrusted: false };
72
+ };
73
+ const toSafeInlineText = (content) => {
74
+ const { value, isTrusted } = toContentString(content, 'both');
75
+ return isTrusted ? value : escapeMarkdownText(value);
76
+ };
77
+ const asSafeString = (value) => new SafeString(value);
78
+ const safeBold = (content) => asSafeString(`**${toSafeInlineText(content)}**`);
79
+ const safeItalic = (content) => asSafeString(`*${toSafeInlineText(content)}*`);
80
+ const safeStrike = (content) => asSafeString(`~~${toSafeInlineText(content)}~~`);
81
+ const safeInlineCode = (content) => {
82
+ const { value } = toContentString(content, 'both');
83
+ return asSafeString(wrapInlineCode(value));
84
+ };
85
+ const safeLink = (text, url) => {
86
+ const { value, isTrusted } = toContentString(text, 'both');
87
+ const safeText = isTrusted ? value : escapeMarkdownText(value);
88
+ const safeUrl = sanitizeLinkDestination(url);
89
+ return asSafeString(`[${safeText}](${safeUrl})`);
90
+ };
91
+ const safeImage = (alt, url) => {
92
+ const { value, isTrusted } = toContentString(alt, 'both');
93
+ const safeAlt = isTrusted ? value : escapeMarkdownText(value);
94
+ const safeUrl = sanitizeLinkDestination(url);
95
+ return asSafeString(`![${safeAlt}](${safeUrl})`);
96
+ };
97
+ const formatValidationIssues = (issues) => {
98
+ if (issues.length === 0) {
99
+ return 'No validation issues were provided.';
100
+ }
101
+ return issues
102
+ .map((issue, index) => {
103
+ const path = issue.path ? `${issue.path}: ` : '';
104
+ return `${index + 1}. ${path}${issue.message}`;
105
+ })
106
+ .join('\n');
107
+ };
108
+ export class ValidationError extends Error {
109
+ format;
110
+ issues;
111
+ constructor(format, issues, label) {
112
+ const title = label ?? `${format.toUpperCase()} validation failed`;
113
+ super(`${title}\n${formatValidationIssues(issues)}`);
114
+ this.name = 'ValidationError';
115
+ this.format = format;
116
+ this.issues = issues;
117
+ }
118
+ }
119
+ export const createJsonSchemaValidator = (adapter) => {
120
+ return ({ format, data, schema }) => {
121
+ if (format !== 'json' || schema === undefined) {
122
+ return { valid: true };
123
+ }
124
+ return adapter(schema, data);
125
+ };
126
+ };
127
+ export const createYamlParserAdapter = (parser) => {
128
+ return (input) => parser(input);
129
+ };
130
+ /**
131
+ * A chainable prose writer for building formatted text/markdown strings.
132
+ */
133
+ export class ProseWriter {
134
+ parts = [];
135
+ _skipNextPadding = false;
136
+ safeMode;
137
+ constructor(content, options = {}) {
138
+ this.safeMode = options.safe ?? false;
139
+ if (content !== undefined) {
140
+ this.parts.push(content.endsWith('\n') ? content : content + '\n');
141
+ }
142
+ }
143
+ createChildWriter() {
144
+ return new ProseWriter(undefined, { safe: this.safeMode });
145
+ }
146
+ /**
147
+ * Appends content to the prose.
148
+ * Multiple arguments are joined with a space.
149
+ * Chaining write() calls creates separate paragraphs by default.
150
+ */
151
+ write(...content) {
152
+ const joined = content
153
+ .map((c) => {
154
+ const { value, isTrusted } = toContentString(c, 'end');
155
+ if (this.safeMode && !isTrusted) {
156
+ return escapeMarkdownText(value);
157
+ }
158
+ return value;
159
+ })
160
+ .join(' ');
161
+ if (this.parts.length === 0 && joined.length === 0) {
162
+ return this;
163
+ }
164
+ const p = this.padding;
165
+ this.parts.push(p + joined.trim() + '\n');
166
+ return this;
167
+ }
168
+ unorderedList(...args) {
169
+ if (args.length === 1 && typeof args[0] === 'function') {
170
+ const { builder, getItems } = this.createListBuilder();
171
+ args[0](builder);
172
+ const items = getItems();
173
+ const listContent = items
174
+ .map((item) => {
175
+ if (item instanceof ProseWriter) {
176
+ return item
177
+ .toString()
178
+ .trimEnd()
179
+ .split('\n')
180
+ .map((line) => ` ${line}`)
181
+ .join('\n');
182
+ }
183
+ return `- ${item}`;
184
+ })
185
+ .join('\n');
186
+ this.parts.push(`${this.padding}${listContent}\n\n`);
187
+ return this;
188
+ }
189
+ const items = args;
190
+ const listContent = items
191
+ .map((item) => {
192
+ if (item instanceof ProseWriter) {
193
+ return item
194
+ .toString()
195
+ .trimEnd()
196
+ .split('\n')
197
+ .map((line) => ` ${line}`)
198
+ .join('\n');
199
+ }
200
+ const { value, isTrusted } = toContentString(item);
201
+ const text = this.safeMode && !isTrusted ? escapeMarkdownText(value) : value;
202
+ return `- ${text}`;
203
+ })
204
+ .join('\n');
205
+ this.parts.push(`${this.padding}${listContent}\n\n`);
206
+ return this;
207
+ }
208
+ list(...args) {
209
+ if (args.length === 1 && typeof args[0] === 'function') {
210
+ return this.unorderedList(args[0]);
211
+ }
212
+ return this.unorderedList(...args);
213
+ }
214
+ orderedList(...args) {
215
+ if (args.length === 1 && typeof args[0] === 'function') {
216
+ const { builder, getItems } = this.createListBuilder();
217
+ args[0](builder);
218
+ let index = 1;
219
+ const items = getItems();
220
+ const listContent = items
221
+ .map((item) => {
222
+ if (item instanceof ProseWriter) {
223
+ return item
224
+ .toString()
225
+ .trimEnd()
226
+ .split('\n')
227
+ .map((line) => ` ${line}`)
228
+ .join('\n');
229
+ }
230
+ return `${index++}. ${item}`;
231
+ })
232
+ .join('\n');
233
+ this.parts.push(`${this.padding}${listContent}\n\n`);
234
+ return this;
235
+ }
236
+ let index = 1;
237
+ const items = args;
238
+ const listContent = items
239
+ .map((item) => {
240
+ if (item instanceof ProseWriter) {
241
+ return item
242
+ .toString()
243
+ .trimEnd()
244
+ .split('\n')
245
+ .map((line) => ` ${line}`)
246
+ .join('\n');
247
+ }
248
+ const { value, isTrusted } = toContentString(item);
249
+ const text = this.safeMode && !isTrusted ? escapeMarkdownText(value) : value;
250
+ return `${index++}. ${text}`;
251
+ })
252
+ .join('\n');
253
+ this.parts.push(`${this.padding}${listContent}\n\n`);
254
+ return this;
255
+ }
256
+ tasks(...args) {
257
+ if (args.length === 1 && typeof args[0] === 'function') {
258
+ const { builder, getItems } = this.createListBuilder();
259
+ args[0](builder);
260
+ const items = getItems();
261
+ const listContent = items
262
+ .map((item) => {
263
+ if (item instanceof ProseWriter) {
264
+ return item
265
+ .toString()
266
+ .trimEnd()
267
+ .split('\n')
268
+ .map((line) => ` ${line}`)
269
+ .join('\n');
270
+ }
271
+ return `- ${item}`;
272
+ })
273
+ .join('\n');
274
+ this.parts.push(`${this.padding}${listContent}\n\n`);
275
+ return this;
276
+ }
277
+ const items = args.map((arg) => {
278
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[1] === 'boolean') {
279
+ const [content, checked] = arg;
280
+ const checkbox = checked ? '[x] ' : '[ ] ';
281
+ const text = this.createChildWriter().write(content).toString().trimEnd();
282
+ return checkbox + text;
283
+ }
284
+ const checkbox = '[ ] ';
285
+ const text = this.createChildWriter()
286
+ .write(arg)
287
+ .toString()
288
+ .trimEnd();
289
+ return checkbox + text;
290
+ });
291
+ const listContent = items.map((item) => `- ${item}`).join('\n');
292
+ this.parts.push(`${this.padding}${listContent}\n\n`);
293
+ return this;
294
+ }
295
+ /**
296
+ * Appends a GitHub Alert (callout).
297
+ */
298
+ callout(type, content) {
299
+ let contentString;
300
+ if (typeof content === 'function') {
301
+ const writer = this.createChildWriter();
302
+ content(writer.enhanced);
303
+ contentString = writer.toString().trimEnd();
304
+ }
305
+ else {
306
+ const { value, isTrusted } = toContentString(content);
307
+ contentString = this.safeMode && !isTrusted ? escapeMarkdownText(value) : value;
308
+ }
309
+ const lines = contentString.split('\n');
310
+ const alertLines = [`[!${type.toUpperCase()}]`, ...lines];
311
+ const alertContent = alertLines.map((line) => `> ${line}`).join('\n');
312
+ this.parts.push(`${this.padding}${alertContent}\n\n`);
313
+ return this;
314
+ }
315
+ /**
316
+ * Appends a markdown heading at the specified level.
317
+ */
318
+ heading(level, ...content) {
319
+ const hashes = '#'.repeat(level);
320
+ const joined = content
321
+ .map((part) => {
322
+ const { value, isTrusted } = toContentString(part);
323
+ if (this.safeMode && !isTrusted) {
324
+ return escapeMarkdownText(value);
325
+ }
326
+ return value;
327
+ })
328
+ .join(' ');
329
+ this.parts.push(`${this.padding}${hashes} ${joined}\n\n`);
330
+ return this;
331
+ }
332
+ /**
333
+ * Appends a blockquote with the given lines.
334
+ * Lines are separated by an empty blockquote line.
335
+ */
336
+ blockquote(...lines) {
337
+ const quotedLines = lines
338
+ .map((line) => {
339
+ const { value, isTrusted } = toContentString(line);
340
+ if (this.safeMode && !isTrusted) {
341
+ return escapeMarkdownText(value);
342
+ }
343
+ return value;
344
+ })
345
+ .map((line) => `> ${line}`)
346
+ .join('\n>\n');
347
+ this.parts.push(`${this.padding}${quotedLines}\n\n`);
348
+ return this;
349
+ }
350
+ /**
351
+ * Appends a fenced code block with the specified language.
352
+ */
353
+ codeblock(language, content) {
354
+ let code;
355
+ if (typeof content === 'function') {
356
+ const writer = new ProseWriter();
357
+ content(writer.enhanced);
358
+ code = writer.toString().trim();
359
+ }
360
+ else {
361
+ code = content;
362
+ }
363
+ this.parts.push(`${this.padding}\`\`\`${language}\n${code}\n\`\`\`\n\n`);
364
+ return this;
365
+ }
366
+ /**
367
+ * Appends a horizontal separator.
368
+ */
369
+ get separator() {
370
+ this.parts.push(`${this.padding}---\n\n`);
371
+ return this;
372
+ }
373
+ /**
374
+ * Appends a JSON code block.
375
+ * If data is not a string, it will be stringified with formatting.
376
+ */
377
+ json(data, options = {}) {
378
+ this.validateOutput('json', data, options);
379
+ const jsonString = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
380
+ return this.codeblock('json', jsonString);
381
+ }
382
+ /**
383
+ * Appends the content from another ProseWriter instance.
384
+ * Enables composition of prompts from reusable pieces.
385
+ */
386
+ append(writer) {
387
+ const content = writer.toString();
388
+ if (content.length > 0) {
389
+ this.parts.push(this.padding + content);
390
+ }
391
+ return this;
392
+ }
393
+ /**
394
+ * Returns a version of this writer with inline formatters as methods.
395
+ * This is passed to builder functions to allow using formatters without imports.
396
+ */
397
+ get enhanced() {
398
+ const formatters = this.safeMode
399
+ ? {
400
+ bold: safeBold,
401
+ italic: safeItalic,
402
+ code: safeInlineCode,
403
+ inline: safeInlineCode,
404
+ link: safeLink,
405
+ strike: safeStrike,
406
+ image: safeImage,
407
+ }
408
+ : {
409
+ bold,
410
+ italic,
411
+ code,
412
+ inline,
413
+ link,
414
+ strike,
415
+ image,
416
+ };
417
+ return new Proxy(this, {
418
+ get(target, prop) {
419
+ if (typeof prop === 'string' && prop in formatters) {
420
+ return formatters[prop];
421
+ }
422
+ const val = Reflect.get(target, prop, target);
423
+ if (typeof val === 'function') {
424
+ return val.bind(target);
425
+ }
426
+ return val;
427
+ },
428
+ });
429
+ }
430
+ /**
431
+ * Conditionally executes a builder function.
432
+ * If the condition is truthy, the builder is called with this instance.
433
+ */
434
+ when(condition, builder) {
435
+ if (condition) {
436
+ builder(this.enhanced);
437
+ }
438
+ return this;
439
+ }
440
+ /**
441
+ * Executes a builder function with this instance.
442
+ * Useful for logical grouping in chains.
443
+ */
444
+ with(builder) {
445
+ builder(this.enhanced);
446
+ return this;
447
+ }
448
+ /**
449
+ * Wraps content in XML-style tags.
450
+ * Useful for Claude and other models that respond well to XML delimiters.
451
+ */
452
+ tag(name, content) {
453
+ let contentString;
454
+ if (typeof content === 'function') {
455
+ const writer = this.createChildWriter();
456
+ content(writer.enhanced);
457
+ contentString = writer.toString();
458
+ }
459
+ else {
460
+ const { value, isTrusted } = toContentString(content);
461
+ if (this.safeMode && !isTrusted) {
462
+ contentString = escapeMarkdownText(value);
463
+ }
464
+ else {
465
+ contentString = value;
466
+ }
467
+ }
468
+ this.parts.push(`${this.padding}<${name}>\n${contentString.trimEnd()}\n</${name}>\n`);
469
+ return this;
470
+ }
471
+ /**
472
+ * Appends inline code (wrapped in backticks).
473
+ */
474
+ code(content) {
475
+ const formatted = this.safeMode ? safeInlineCode(content) : code(content);
476
+ this.parts.push(this.padding + formatted + '\n');
477
+ return this;
478
+ }
479
+ /**
480
+ * Creates a semantic section with a heading and content built by the builder function.
481
+ * @param name - The section heading text
482
+ * @param builder - A function that receives a fresh ProseWriter to build section content
483
+ * @param level - Optional heading level (defaults to 2)
484
+ */
485
+ section(name, builder, level = 2) {
486
+ const sectionWriter = this.createChildWriter();
487
+ builder(sectionWriter.enhanced);
488
+ const hashes = '#'.repeat(level);
489
+ const title = this.safeMode ? escapeMarkdownText(name) : name;
490
+ this.parts.push(`${this.padding}${hashes} ${title}\n\n${sectionWriter.toString()}`);
491
+ return this;
492
+ }
493
+ /**
494
+ * Creates a copy of this ProseWriter with the same content.
495
+ * Useful for creating variations of a base prompt.
496
+ */
497
+ clone() {
498
+ const cloned = new ProseWriter(undefined, { safe: this.safeMode });
499
+ cloned.parts = [...this.parts];
500
+ return cloned;
501
+ }
502
+ /**
503
+ * Appends a markdown table with headers and rows.
504
+ * Type-safe: Each row can be an array of strings matching the header length,
505
+ * or an object where keys match the header names.
506
+ */
507
+ table(headers, rows) {
508
+ const safeHeaders = this.safeMode
509
+ ? headers.map((header) => escapeMarkdownText(header))
510
+ : headers;
511
+ const headerRow = `| ${safeHeaders.join(' | ')} |`;
512
+ const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
513
+ const dataRows = rows
514
+ .map((row) => {
515
+ const values = Array.isArray(row)
516
+ ? row.map((val) => {
517
+ const text = String(val ?? '');
518
+ return this.safeMode ? escapeMarkdownText(text) : text;
519
+ })
520
+ : headers.map((h) => {
521
+ const val = row[h];
522
+ if (val instanceof ProseWriter)
523
+ return val.toPlainText();
524
+ const text = String(val ?? '');
525
+ return this.safeMode ? escapeMarkdownText(text) : text;
526
+ });
527
+ return `| ${values.join(' | ')} |`;
528
+ })
529
+ .join('\n');
530
+ this.parts.push(`${this.padding}${headerRow}\n${separatorRow}\n${dataRows}\n\n`);
531
+ return this;
532
+ }
533
+ /**
534
+ * Appends a definition list with key-value pairs.
535
+ * Each key is bolded, followed by a colon and the value.
536
+ */
537
+ definitions(obj) {
538
+ const entries = Object.entries(obj)
539
+ .map(([key, value]) => {
540
+ if (this.safeMode) {
541
+ return `${safeBold(key)}: ${escapeMarkdownText(value)}`;
542
+ }
543
+ return `**${key}**: ${value}`;
544
+ })
545
+ .join('\n');
546
+ this.parts.push(`${this.padding}${entries}\n\n`);
547
+ return this;
548
+ }
549
+ /**
550
+ * Embeds a schema or output contract in the prompt.
551
+ */
552
+ schema(schemaValue, options = {}) {
553
+ const { format = 'json', title, level = 2, tag } = options;
554
+ if (title) {
555
+ this.heading(level, title);
556
+ }
557
+ if (tag) {
558
+ const schemaWriter = this.createChildWriter();
559
+ if (format === 'json') {
560
+ schemaWriter.json(schemaValue);
561
+ }
562
+ else {
563
+ schemaWriter.yaml(schemaValue);
564
+ }
565
+ return this.tag(tag, schemaWriter);
566
+ }
567
+ if (format === 'json') {
568
+ return this.json(schemaValue);
569
+ }
570
+ return this.yaml(schemaValue);
571
+ }
572
+ /**
573
+ * Appends bold text.
574
+ */
575
+ bold(content) {
576
+ const formatted = this.safeMode ? safeBold(content) : bold(content);
577
+ this.parts.push(this.padding + formatted + '\n');
578
+ return this;
579
+ }
580
+ /**
581
+ * Appends italic text.
582
+ */
583
+ italic(content) {
584
+ const formatted = this.safeMode ? safeItalic(content) : italic(content);
585
+ this.parts.push(this.padding + formatted + '\n');
586
+ return this;
587
+ }
588
+ /**
589
+ * Appends strikethrough text.
590
+ */
591
+ strike(content) {
592
+ const formatted = this.safeMode ? safeStrike(content) : strike(content);
593
+ this.parts.push(this.padding + formatted + '\n');
594
+ return this;
595
+ }
596
+ /**
597
+ * Appends raw content without any processing.
598
+ */
599
+ raw(content) {
600
+ this.parts.push(content);
601
+ return this;
602
+ }
603
+ /**
604
+ * Appends a markdown link.
605
+ */
606
+ link(text, url) {
607
+ const formatted = this.safeMode ? safeLink(text, url) : link(text, url);
608
+ this.parts.push(this.padding + formatted + '\n');
609
+ return this;
610
+ }
611
+ /**
612
+ * Appends a markdown image.
613
+ */
614
+ image(alt, url) {
615
+ const formatted = this.safeMode ? safeImage(alt, url) : image(alt, url);
616
+ this.parts.push(this.padding + formatted + '\n');
617
+ return this;
618
+ }
619
+ /**
620
+ * Appends an HTML comment.
621
+ */
622
+ comment(content) {
623
+ this.parts.push(`${this.padding}<!-- ${content} -->\n\n`);
624
+ return this;
625
+ }
626
+ /**
627
+ * Appends a YAML code block.
628
+ * If data is not a string, it will be converted to YAML format.
629
+ */
630
+ yaml(data, options = {}) {
631
+ this.validateOutput('yaml', data, options);
632
+ const yamlString = typeof data === 'string' ? data : this.toYamlString(data);
633
+ return this.codeblock('yaml', yamlString);
634
+ }
635
+ /**
636
+ * Converts a value to a simple YAML string representation.
637
+ */
638
+ toYamlString(data, indent = 0) {
639
+ const prefix = ' '.repeat(indent);
640
+ if (data === null || data === undefined) {
641
+ return 'null';
642
+ }
643
+ if (typeof data === 'string') {
644
+ // Quote strings that contain special characters
645
+ if (/[:\n#"']/.test(data) || data === '') {
646
+ return `"${data.replace(/"/g, '\\"')}"`;
647
+ }
648
+ return data;
649
+ }
650
+ if (typeof data === 'number' || typeof data === 'boolean') {
651
+ return String(data);
652
+ }
653
+ if (Array.isArray(data)) {
654
+ if (data.length === 0)
655
+ return '[]';
656
+ return data
657
+ .map((item) => `${prefix}- ${this.toYamlString(item, indent + 1)}`)
658
+ .join('\n');
659
+ }
660
+ if (typeof data === 'object') {
661
+ const entries = Object.entries(data);
662
+ if (entries.length === 0)
663
+ return '{}';
664
+ return entries
665
+ .map(([key, value]) => {
666
+ const valueStr = this.toYamlString(value, indent + 1);
667
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
668
+ return `${prefix}${key}:\n${valueStr}`;
669
+ }
670
+ if (Array.isArray(value)) {
671
+ return `${prefix}${key}:\n${valueStr}`;
672
+ }
673
+ return `${prefix}${key}: ${valueStr}`;
674
+ })
675
+ .join('\n');
676
+ }
677
+ // Fallback for any other types - use JSON stringify
678
+ return JSON.stringify(data) ?? 'null';
679
+ }
680
+ /**
681
+ * Wraps content with custom delimiters.
682
+ */
683
+ delimit(open, close, content) {
684
+ const contentString = content instanceof ProseWriter ? content.toString() : content;
685
+ this.parts.push(`${this.padding}${open}\n${contentString}\n${close}\n`);
686
+ return this;
687
+ }
688
+ /**
689
+ * Returns a new ProseWriter with consecutive newlines collapsed to double newlines.
690
+ */
691
+ compact() {
692
+ const content = this.toString().replace(/\n{3,}/g, '\n\n');
693
+ return new ProseWriter(content, { safe: this.safeMode });
694
+ }
695
+ /**
696
+ * Returns a new ProseWriter with leading and trailing whitespace removed.
697
+ */
698
+ trim() {
699
+ const content = this.toString().trim();
700
+ return new ProseWriter(content, { safe: this.safeMode });
701
+ }
702
+ /**
703
+ * Estimates the number of tokens in the prose.
704
+ * By default, uses a rough approximation of ~4 characters per token.
705
+ * An optional counter function can be provided for more accurate counting.
706
+ */
707
+ tokens(counter) {
708
+ const content = this.toString();
709
+ if (counter) {
710
+ return counter(content);
711
+ }
712
+ // Rough estimation: ~4 characters per token for English text
713
+ return Math.ceil(content.length / 4);
714
+ }
715
+ /**
716
+ * Iterates over items and applies a builder function for each.
717
+ */
718
+ each(items, builder) {
719
+ items.forEach((item, index) => {
720
+ builder(item, this.enhanced, index);
721
+ });
722
+ return this;
723
+ }
724
+ /**
725
+ * Converts the prose to plain text by stripping markdown formatting.
726
+ */
727
+ toPlainText() {
728
+ let text = this.toString();
729
+ // Remove code blocks (preserve content)
730
+ text = text.replace(/```[\s\S]*?```/g, (match) => {
731
+ const lines = match.split('\n');
732
+ return lines.slice(1, -1).join('\n');
733
+ });
734
+ // Remove inline code backticks
735
+ text = text.replace(/`([^`]+)`/g, '$1');
736
+ // Remove headings (keep text)
737
+ text = text.replace(/^#{1,6}\s+(.*)$/gm, '$1');
738
+ // Remove bold
739
+ text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
740
+ // Remove italic
741
+ text = text.replace(/\*([^*]+)\*/g, '$1');
742
+ // Remove links (keep text)
743
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
744
+ // Remove blockquote markers
745
+ text = text.replace(/^>\s?/gm, '');
746
+ // Remove horizontal rules
747
+ text = text.replace(/^---$/gm, '');
748
+ // Remove list markers
749
+ text = text.replace(/^[-*]\s+/gm, '');
750
+ text = text.replace(/^\d+\.\s+/gm, '');
751
+ // Remove XML tags (keep content)
752
+ text = text.replace(/<[^>]+>/g, '');
753
+ // Remove table formatting
754
+ text = text.replace(/\|/g, ' ');
755
+ text = text.replace(/^[\s-]+$/gm, '');
756
+ // Collapse multiple spaces
757
+ text = text.replace(/ {2,}/g, ' ');
758
+ // Collapse multiple newlines
759
+ text = text.replace(/\n{3,}/g, '\n\n');
760
+ return text.trim();
761
+ }
762
+ /**
763
+ * Runs output validation if a validator is provided.
764
+ */
765
+ validateOutput(format, data, options) {
766
+ if (!options.validate)
767
+ return;
768
+ let validationData = data;
769
+ if (format === 'json' && typeof data === 'string') {
770
+ try {
771
+ validationData = JSON.parse(data);
772
+ }
773
+ catch (error) {
774
+ const message = error instanceof Error ? error.message : 'Invalid JSON string';
775
+ throw new ValidationError(format, [{ path: '$', message: `Invalid JSON string: ${message}` }], options.label);
776
+ }
777
+ }
778
+ if (format === 'yaml' && typeof data === 'string' && options.parseYaml) {
779
+ try {
780
+ validationData = options.parseYaml(data);
781
+ }
782
+ catch (error) {
783
+ const message = error instanceof Error ? error.message : 'Invalid YAML string';
784
+ throw new ValidationError(format, [{ path: '$', message: `Invalid YAML string: ${message}` }], options.label);
785
+ }
786
+ }
787
+ const result = options.validate({
788
+ format,
789
+ data: validationData,
790
+ schema: options.schema,
791
+ });
792
+ if (result.valid)
793
+ return;
794
+ throw new ValidationError(format, result.issues ?? [], options.label);
795
+ }
796
+ /**
797
+ * Returns necessary newline padding if there is existing content.
798
+ * Ensures exactly two newlines (a paragraph break) before a block element.
799
+ */
800
+ get padding() {
801
+ if (this._skipNextPadding || this.parts.length === 0) {
802
+ this._skipNextPadding = false;
803
+ return '';
804
+ }
805
+ const lastPart = this.parts[this.parts.length - 1];
806
+ if (lastPart.endsWith('\n\n'))
807
+ return '';
808
+ if (lastPart.endsWith('\n'))
809
+ return '\n';
810
+ return '\n\n';
811
+ }
812
+ /**
813
+ * Internal helper to create a list builder.
814
+ */
815
+ createListBuilder() {
816
+ const items = [];
817
+ const lb = {
818
+ item: (...content) => {
819
+ items.push(this.createChildWriter()
820
+ .write(...content)
821
+ .toString()
822
+ .trimEnd());
823
+ return lb;
824
+ },
825
+ task: (checked, ...content) => {
826
+ const checkbox = checked ? '[x] ' : '[ ] ';
827
+ const text = this.createChildWriter()
828
+ .write(...content)
829
+ .toString()
830
+ .trimEnd();
831
+ items.push(checkbox + text);
832
+ return lb;
833
+ },
834
+ todo: (...content) => lb.task(false, ...content),
835
+ done: (...content) => lb.task(true, ...content),
836
+ unorderedList: (...args) => {
837
+ const sub = this.createChildWriter();
838
+ if (args.length === 1 && typeof args[0] === 'function') {
839
+ sub.unorderedList(args[0]);
840
+ }
841
+ else {
842
+ sub.unorderedList(...args);
843
+ }
844
+ items.push(sub);
845
+ return lb;
846
+ },
847
+ list: (...args) => {
848
+ const sub = this.createChildWriter();
849
+ if (args.length === 1 && typeof args[0] === 'function') {
850
+ sub.list(args[0]);
851
+ }
852
+ else {
853
+ sub.list(...args);
854
+ }
855
+ items.push(sub);
856
+ return lb;
857
+ },
858
+ orderedList: (...args) => {
859
+ const sub = this.createChildWriter();
860
+ if (args.length === 1 && typeof args[0] === 'function') {
861
+ sub.orderedList(args[0]);
862
+ }
863
+ else {
864
+ sub.orderedList(...args);
865
+ }
866
+ items.push(sub);
867
+ return lb;
868
+ },
869
+ comment: (content) => {
870
+ const sub = this.createChildWriter();
871
+ sub.comment(content);
872
+ items.push(sub);
873
+ return lb;
874
+ },
875
+ bold: this.safeMode ? safeBold : bold,
876
+ italic: this.safeMode ? safeItalic : italic,
877
+ code: this.safeMode ? safeInlineCode : code,
878
+ inline: this.safeMode ? safeInlineCode : inline,
879
+ strike: this.safeMode ? safeStrike : strike,
880
+ link: this.safeMode ? safeLink : link,
881
+ image: this.safeMode ? safeImage : image,
882
+ };
883
+ return { builder: lb, getItems: () => items };
884
+ }
885
+ /**
886
+ * Creates an empty ProseWriter instance.
887
+ */
888
+ static empty() {
889
+ return new ProseWriter();
890
+ }
891
+ /**
892
+ * Joins multiple ProseWriter instances into one.
893
+ */
894
+ static join(...writers) {
895
+ const result = new ProseWriter();
896
+ for (const writer of writers) {
897
+ result.parts.push(writer.toString());
898
+ }
899
+ return result;
900
+ }
901
+ /**
902
+ * Converts the accumulated prose to a string.
903
+ */
904
+ toString() {
905
+ return this.parts.join('');
906
+ }
907
+ /**
908
+ * Returns the primitive value (string) for type coercion.
909
+ */
910
+ [Symbol.toPrimitive](hint) {
911
+ if (hint === 'number') {
912
+ return Number.NaN;
913
+ }
914
+ return this.toString();
915
+ }
916
+ /**
917
+ * Returns the string tag for the object.
918
+ */
919
+ get [Symbol.toStringTag]() {
920
+ return 'ProseWriter';
921
+ }
922
+ }
923
+ /**
924
+ * Creates a new ProseWriter instance with the given content.
925
+ */
926
+ const buildWrite = (safeMode) => {
927
+ const createWriter = () => new ProseWriter(undefined, { safe: safeMode });
928
+ return Object.assign((...content) => {
929
+ return createWriter().write(...content);
930
+ }, {
931
+ with: (builder) => {
932
+ return createWriter().with(builder);
933
+ },
934
+ unorderedList: (...args) => {
935
+ const pw = createWriter();
936
+ if (args.length === 1 && typeof args[0] === 'function') {
937
+ return pw.unorderedList(args[0]);
938
+ }
939
+ return pw.unorderedList(...args);
940
+ },
941
+ list: (...args) => {
942
+ const pw = createWriter();
943
+ if (args.length === 1 && typeof args[0] === 'function') {
944
+ return pw.list(args[0]);
945
+ }
946
+ return pw.list(...args);
947
+ },
948
+ orderedList: (...args) => {
949
+ const pw = createWriter();
950
+ if (args.length === 1 && typeof args[0] === 'function') {
951
+ return pw.orderedList(args[0]);
952
+ }
953
+ return pw.orderedList(...args);
954
+ },
955
+ tasks: (...args) => {
956
+ const pw = createWriter();
957
+ if (args.length === 1 && typeof args[0] === 'function') {
958
+ return pw.tasks(args[0]);
959
+ }
960
+ return pw.tasks(...args);
961
+ },
962
+ callout: (type, content) => {
963
+ return createWriter().callout(type, content);
964
+ },
965
+ schema: (schemaValue, options) => {
966
+ return createWriter().schema(schemaValue, options);
967
+ },
968
+ heading: (level, ...content) => {
969
+ return createWriter().heading(level, ...content);
970
+ },
971
+ blockquote: (...lines) => {
972
+ return createWriter().blockquote(...lines);
973
+ },
974
+ codeblock: (language, content) => {
975
+ return createWriter().codeblock(language, content);
976
+ },
977
+ json: (data, options) => {
978
+ return createWriter().json(data, options);
979
+ },
980
+ yaml: (data, options) => {
981
+ return createWriter().yaml(data, options);
982
+ },
983
+ definitions: (obj) => {
984
+ return createWriter().definitions(obj);
985
+ },
986
+ section: (name, builder, level) => {
987
+ return createWriter().section(name, builder, level);
988
+ },
989
+ tag: (name, content) => {
990
+ return createWriter().tag(name, content);
991
+ },
992
+ comment: (content) => {
993
+ return createWriter().comment(content);
994
+ },
995
+ raw: (content) => {
996
+ return createWriter().raw(content);
997
+ },
998
+ append: (writer) => {
999
+ return createWriter().append(writer);
1000
+ },
1001
+ when: (condition, builder) => {
1002
+ return createWriter().when(condition, builder);
1003
+ },
1004
+ });
1005
+ };
1006
+ const safeWrite = buildWrite(true);
1007
+ export const write = Object.assign(buildWrite(false), { safe: safeWrite });
1008
+ /**
1009
+ * Appends bold formatting to a string.
1010
+ */
1011
+ export const bold = (content) => `**${content instanceof ProseWriter ? content.toString().trim() : content}**`;
1012
+ /**
1013
+ * Appends italic formatting to a string.
1014
+ */
1015
+ export const italic = (content) => `*${content instanceof ProseWriter ? content.toString().trim() : content}*`;
1016
+ /**
1017
+ * Appends inline code formatting to a string.
1018
+ */
1019
+ export const code = (content) => `\`${content instanceof ProseWriter ? content.toString().trim() : content}\``;
1020
+ /**
1021
+ * Alias for code().
1022
+ */
1023
+ export const inline = code;
1024
+ /**
1025
+ * Appends strikethrough formatting to a string.
1026
+ */
1027
+ export const strike = (content) => `~~${content instanceof ProseWriter ? content.toString().trim() : content}~~`;
1028
+ /**
1029
+ * Creates a markdown link string.
1030
+ */
1031
+ export const link = (text, url) => `[${text instanceof ProseWriter ? text.toString().trim() : text}](${url})`;
1032
+ /**
1033
+ * Creates a markdown image string.
1034
+ */
1035
+ export const image = (alt, url) => `![${alt instanceof ProseWriter ? alt.toString().trim() : alt}](${url})`;
1036
+ //# sourceMappingURL=prose-writer.js.map