prose-writer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  A zero-dependency, chainable TypeScript library for building formatted text and markdown strings. Perfect for constructing LLM prompts, generating documentation, or any scenario where you need to programmatically build structured text.
4
4
 
5
+ ## In one sentence
6
+
7
+ Prose Writer is a fluent builder for producing markdown-friendly strings without template literal sprawl.
8
+
9
+ ## When to use it
10
+
11
+ - You need readable, composable prompt or document builders in code
12
+ - You want conditionals, loops, and reusable pieces without `.map().join()` or manual concatenation
13
+ - You need safe handling of untrusted input or structured output sections (JSON/YAML)
14
+
15
+ ## How it works (3 steps)
16
+
17
+ 1. `write()` creates a builder (optionally with initial text).
18
+ 2. Chain block helpers like `.section()`, `.list()`, `.tag()`, `.codeblock()`, `.json()`.
19
+ 3. Call `.toString()` or `String(writer)` to get the final text.
20
+
21
+ ```typescript
22
+ import { write } from 'prose-writer';
23
+
24
+ const prompt = write('You are a helpful assistant.')
25
+ .section('Guidelines', (w) => w.list('Be concise', 'Cite sources'))
26
+ .tag('input', userText)
27
+ .toString();
28
+ ```
29
+
30
+ Each `write()` call ends with a newline, so chained calls become separate paragraphs (a blank line between). To keep content on the same line, pass multiple strings to a single `write()` call. Most block helpers add blank lines around themselves so the output reads like markdown paragraphs. Use `write()` with no args to insert an extra blank line.
31
+
5
32
  ## Why Prose Writer?
6
33
 
7
34
  Building prompts for LLMs in code is _painful_. You end up with wild stuff like this.
@@ -23,7 +50,27 @@ ${context}
23
50
 
24
51
  Template literals become unreadable. String concatenation is worse. And when you need conditionals, variables, or reusable pieces? Good luck.
25
52
 
26
- **prose-writer** fixes this:
53
+ Compared to concatenating arrays of strings, Prose Writer keeps structure and spacing inside the builder instead of scattering `join()` logic, manual newlines, and nested maps across your code. Compared to giant template strings, it avoids brittle whitespace and makes conditional or reusable sections readable and composable.
54
+
55
+ Array concatenation version:
56
+
57
+ ```typescript
58
+ const prompt = [
59
+ `You are a ${role}.`,
60
+ '',
61
+ '## Guidelines',
62
+ ...guidelines.map((g) => `- ${g}`),
63
+ '',
64
+ '## Examples',
65
+ ...examples.map((ex, i) => `### Example ${i + 1}\n${ex}`),
66
+ '',
67
+ '<context>',
68
+ context,
69
+ '</context>',
70
+ ].join('\n');
71
+ ```
72
+
73
+ Prose Writer version:
27
74
 
28
75
  ```typescript
29
76
  const prompt = write(`You are a ${role}.`)
@@ -42,7 +89,6 @@ const prompt = write(`You are a ${role}.`)
42
89
  - **Composable** - Build prompts from reusable pieces with `.append()` and `.clone()`
43
90
  - **Logical grouping** - Use `.with(builder)` to group related operations
44
91
  - **Conditional logic** - Add sections conditionally with `.when(condition, builder)`
45
- - **Template variables** - Use `{{placeholders}}` and `.fill()` for dynamic content
46
92
  - **LLM-optimized** - Built-in `.tag()` for XML delimiters (Claude loves these), `.json()` and `.yaml()` for structured output instructions
47
93
  - **Batch operations** - Iterate with `.each()` instead of awkward `.map().join()` chains
48
94
  - **Token awareness** - Estimate prompt size with `.tokens()`
@@ -52,7 +98,8 @@ const prompt = write(`You are a ${role}.`)
52
98
  ### Real-world example
53
99
 
54
100
  ```typescript
55
- import { write, bold, code } from 'prose-writer';
101
+ import { write } from 'prose-writer';
102
+ import { bold, code } from 'prose-writer/markdown';
56
103
 
57
104
  // Define reusable components
58
105
  const codeReviewPersona = write('You are a', bold('senior software engineer.')).write(
@@ -65,6 +112,9 @@ const outputFormat = write('').definitions({
65
112
  suggestions: 'Recommended improvements',
66
113
  });
67
114
 
115
+ const language = 'TypeScript';
116
+ const framework = 'React';
117
+
68
118
  // Build the prompt
69
119
  const reviewPrompt = write('')
70
120
  .append(codeReviewPersona)
@@ -78,7 +128,8 @@ const reviewPrompt = write('')
78
128
  .when(strictMode, (w) => w.write('Be extremely thorough. Miss nothing.'))
79
129
  .section('Output Format', (w) => w.append(outputFormat))
80
130
  .tag('code', userCode)
81
- .fill({ language: 'TypeScript', framework: 'React' });
131
+ .write('Language:', language)
132
+ .write('Framework:', framework);
82
133
  ```
83
134
 
84
135
  Stop fighting with template strings. Start writing prompts that are readable, maintainable, and composable.
@@ -96,10 +147,47 @@ npm install prose-writer
96
147
  pnpm add prose-writer
97
148
  ```
98
149
 
150
+ ## Exports
151
+
152
+ Core:
153
+
154
+ ```typescript
155
+ import { write, ProseWriter } from 'prose-writer';
156
+ ```
157
+
158
+ Markdown utilities:
159
+
160
+ ```typescript
161
+ import { bold, italic, code, strike, link, image } from 'prose-writer/markdown';
162
+ ```
163
+
164
+ Validation helpers:
165
+
166
+ ```typescript
167
+ import {
168
+ createJsonSchemaValidator,
169
+ createYamlParserAdapter,
170
+ ValidationError,
171
+ } from 'prose-writer/validation';
172
+ ```
173
+
174
+ Schema types:
175
+
176
+ ```typescript
177
+ import type { SchemaEmbedOptions } from 'prose-writer/schema';
178
+ ```
179
+
180
+ Safe writer:
181
+
182
+ ```typescript
183
+ import { write } from 'prose-writer/safe';
184
+ ```
185
+
99
186
  ## Quick Start
100
187
 
101
188
  ```typescript
102
- import { write, bold } from 'prose-writer';
189
+ import { write } from 'prose-writer';
190
+ import { bold } from 'prose-writer/markdown';
103
191
 
104
192
  const prompt = write('You are a', bold('helpful assistant.'))
105
193
  .write('Please help the user with their request.')
@@ -122,7 +210,8 @@ Please help the user with their request.
122
210
  Creates a new `ProseWriter` instance. Multiple arguments are joined with a space, and a newline is added at the end. Can be called with zero arguments to add a blank line between other `write()` calls.
123
211
 
124
212
  ```typescript
125
- import { write, bold, code } from 'prose-writer';
213
+ import { write } from 'prose-writer';
214
+ import { bold, code } from 'prose-writer/markdown';
126
215
 
127
216
  const text = write('Hello', bold('World')).toString();
128
217
  ```
@@ -133,6 +222,26 @@ Output:
133
222
  Hello **World**
134
223
  ```
135
224
 
225
+ ### `write.safe(...content: string[])`
226
+
227
+ Creates a `ProseWriter` that escapes untrusted input. Safe mode:
228
+
229
+ - Escapes Markdown punctuation and line-leading markers (lists, headings, blockquotes)
230
+ - Escapes XML-sensitive characters (`&`, `<`, `>`) in text and tags
231
+ - Sanitizes link text + destinations
232
+ - Wraps inline code with a backtick fence that can't be broken by user input
233
+
234
+ Use this when inserting user-generated content. To intentionally include raw Markdown, pass a `ProseWriter` instance or call `.raw()` to bypass escaping.
235
+ You can also import a safe-first writer: `import { write } from 'prose-writer/safe'`.
236
+
237
+ ```typescript
238
+ const prompt = write
239
+ .safe('User input:', userInput)
240
+ .tag('context', userInput)
241
+ .link('Source', userUrl)
242
+ .toString();
243
+ ```
244
+
136
245
  You can also start a chain using `write.with()` if you want to use the builder pattern immediately:
137
246
 
138
247
  ```typescript
@@ -150,20 +259,21 @@ Hello **World**
150
259
  ```
151
260
 
152
261
  ```typescript
153
- const multiLine = write('First line').write('Second line').toString();
262
+ const multiParagraph = write('First line').write('Second line').toString();
154
263
  ```
155
264
 
156
265
  Output:
157
266
 
158
267
  ```markdown
159
268
  First line
269
+
160
270
  Second line
161
271
  ```
162
272
 
163
- Adding a blank line between paragraphs:
273
+ Adding an extra blank line between paragraphs:
164
274
 
165
275
  ```typescript
166
- const multiParagraph = write('Paragraph 1').write().write('Paragraph 2').toString();
276
+ const spacedParagraphs = write('Paragraph 1').write().write('Paragraph 2').toString();
167
277
  ```
168
278
 
169
279
  Output:
@@ -176,7 +286,7 @@ Paragraph 2
176
286
 
177
287
  ### `.write(...content: string[])`
178
288
 
179
- Appends content to the prose. Multiple arguments are joined with a space, and a newline is added at the end. Returns `this` for chaining.
289
+ Appends content to the prose. Multiple arguments are joined with a space, and a newline is added at the end. Each chained call starts a new paragraph (blank line), so use a single `write()` call when you want one line.
180
290
 
181
291
  ```typescript
182
292
  write('User:').write('Hello', 'Assistant').toString();
@@ -189,6 +299,18 @@ User:
189
299
  Hello Assistant
190
300
  ```
191
301
 
302
+ Same line by passing multiple strings:
303
+
304
+ ```typescript
305
+ write('User:', 'Hello', 'Assistant').toString();
306
+ ```
307
+
308
+ Output:
309
+
310
+ ```markdown
311
+ User: Hello Assistant
312
+ ```
313
+
192
314
  ### Inline Utilities
193
315
 
194
316
  The following utilities return formatted strings and can be used within `write()` calls or anywhere else.
@@ -202,7 +324,8 @@ The following utilities return formatted strings and can be used within `write()
202
324
  - `image(alt: string, url: string)` - `![alt](url)`
203
325
 
204
326
  ```typescript
205
- import { write, bold, italic, code, link } from 'prose-writer';
327
+ import { write } from 'prose-writer';
328
+ import { bold, italic, code, link } from 'prose-writer/markdown';
206
329
 
207
330
  write(
208
331
  'Check out',
@@ -218,21 +341,6 @@ write(
218
341
 
219
342
  Obviously, you can just write regular Markdown here as well.
220
343
 
221
- ### `.nextLine()`
222
-
223
- Prevents the next block element or `write()` call from adding a paragraph break. Useful for placing content on consecutive lines.
224
-
225
- ```typescript
226
- write('Line 1').nextLine().write('Line 2').toString();
227
- ```
228
-
229
- Output:
230
-
231
- ```markdown
232
- Line 1
233
- Line 2
234
- ```
235
-
236
344
  ### `.unorderedList(...items: string[] | number[] | boolean[] | ProseWriter[])`
237
345
 
238
346
  ### `.list(...items: string[] | number[] | boolean[] | ProseWriter[])`
@@ -552,7 +660,7 @@ Section 1
552
660
  Section 2
553
661
  ```
554
662
 
555
- ### `.json(data: unknown)`
663
+ ### `.json(data: unknown, options?: ValidationOptions)`
556
664
 
557
665
  Appends a JSON code block. If the data is not a string, it will be stringified with formatting.
558
666
 
@@ -587,6 +695,17 @@ Output:
587
695
  ```
588
696
  ````
589
697
 
698
+ You can pass validation hooks via `options`:
699
+
700
+ ```typescript
701
+ write('').json(data, {
702
+ schema: outputSchema,
703
+ validate: ({ format, data, schema }) => {
704
+ // return { valid: true } or { valid: false, issues: [...] }
705
+ },
706
+ });
707
+ ```
708
+
590
709
  ### `.append(writer: ProseWriter)`
591
710
 
592
711
  Appends the content from another `ProseWriter` instance. Enables composition of prompts from reusable pieces.
@@ -694,7 +813,8 @@ function.
694
813
  // Recommended:
695
814
 
696
815
  ```typescript
697
- import { write, code } from 'prose-writer';
816
+ import { write } from 'prose-writer';
817
+ import { code } from 'prose-writer/markdown';
698
818
  write('Use the', code('calculateTotal'), 'function.').toString();
699
819
  ```
700
820
 
@@ -704,21 +824,6 @@ Output:
704
824
  Use the `calculateTotal` function.
705
825
  ```
706
826
 
707
- ### `.fill(variables: Record<string, string>)`
708
-
709
- Replaces template variables in the format `{{variableName}}` with provided values. Returns a new `ProseWriter` with the substitutions applied (does not modify the original).
710
-
711
- ```typescript
712
- const template = write('Hello, {{name}}! Welcome to {{place}}.');
713
- const result = template.fill({ name: 'Alice', place: 'Wonderland' }).toString();
714
- ```
715
-
716
- Output:
717
-
718
- ```markdown
719
- Hello, Alice! Welcome to Wonderland.
720
- ```
721
-
722
827
  ### `.section(name: string, builder: (writer) => void, level?: 1-6)`
723
828
 
724
829
  Creates a semantic section with a heading and content built by the builder function. The optional `level` parameter defaults to 2.
@@ -815,7 +920,8 @@ information.
815
920
  // Recommended:
816
921
 
817
922
  ```typescript
818
- import { write, bold } from 'prose-writer';
923
+ import { write } from 'prose-writer';
924
+ import { bold } from 'prose-writer/markdown';
819
925
  write('This is', bold('important'), 'information.').toString();
820
926
  ```
821
927
 
@@ -844,7 +950,8 @@ the following.
844
950
  // Recommended:
845
951
 
846
952
  ```typescript
847
- import { write, italic } from 'prose-writer';
953
+ import { write } from 'prose-writer';
954
+ import { italic } from 'prose-writer/markdown';
848
955
  write('Please', italic('note'), 'the following.').toString();
849
956
  ```
850
957
 
@@ -893,7 +1000,8 @@ New
893
1000
  // Recommended:
894
1001
 
895
1002
  ```typescript
896
- import { write, strike } from 'prose-writer';
1003
+ import { write } from 'prose-writer';
1004
+ import { strike } from 'prose-writer/markdown';
897
1005
  write('Price:', strike('$100'), '$80').toString();
898
1006
  ```
899
1007
 
@@ -949,7 +1057,8 @@ for details.
949
1057
  // Recommended:
950
1058
 
951
1059
  ```typescript
952
- import { write, link } from 'prose-writer';
1060
+ import { write } from 'prose-writer';
1061
+ import { link } from 'prose-writer/markdown';
953
1062
  write('See the', link('documentation', 'https://example.com'), 'for details.').toString();
954
1063
  ```
955
1064
 
@@ -959,7 +1068,7 @@ Output:
959
1068
  See the [documentation](https://example.com) for details.
960
1069
  ```
961
1070
 
962
- ### `.yaml(data: unknown)`
1071
+ ### `.yaml(data: unknown, options?: ValidationOptions)`
963
1072
 
964
1073
  Appends a YAML code block. If data is not a string, it will be converted to YAML format.
965
1074
 
@@ -996,6 +1105,74 @@ Wraps content with custom delimiters. Useful for models that respond to specific
996
1105
  write('Input:').delimit('###', '###', 'content here').toString();
997
1106
  ```
998
1107
 
1108
+ ### Structured Output Validation
1109
+
1110
+ `json()` and `yaml()` accept a `validate` hook. If validation fails, a `ValidationError` is thrown with diagnostic details.
1111
+ When validating JSON, string inputs are parsed first and will throw a `ValidationError` if they are invalid JSON. For YAML, you can supply a parser via `parseYaml` to parse strings before validation.
1112
+
1113
+ ```typescript
1114
+ import type { OutputValidator } from 'prose-writer/validation';
1115
+
1116
+ const validate: OutputValidator = ({ format, data, schema }) => {
1117
+ if (format !== 'json') return { valid: true };
1118
+ if (!schema) return { valid: true };
1119
+ // Your validation logic here
1120
+ return { valid: true };
1121
+ };
1122
+
1123
+ write('').json(payload, { schema: outputSchema, validate });
1124
+ ```
1125
+
1126
+ For YAML string inputs, pass a parser adapter:
1127
+
1128
+ ```typescript
1129
+ import { parse as parseYaml } from 'yaml';
1130
+ import { createYamlParserAdapter } from 'prose-writer/validation';
1131
+
1132
+ write('').yaml(payloadString, {
1133
+ validate,
1134
+ parseYaml: createYamlParserAdapter(parseYaml),
1135
+ });
1136
+ ```
1137
+
1138
+ #### JSON Schema via Adapter
1139
+
1140
+ `prose-writer` stays zero-deps, but you can plug in Ajv (or any validator) through an adapter.
1141
+
1142
+ ```typescript
1143
+ import Ajv from 'ajv';
1144
+ import { createJsonSchemaValidator } from 'prose-writer/validation';
1145
+
1146
+ const ajv = new Ajv();
1147
+ const validate = createJsonSchemaValidator((schema, data) => {
1148
+ const valid = ajv.validate(schema, data);
1149
+ if (valid) return { valid: true };
1150
+ return {
1151
+ valid: false,
1152
+ issues: (ajv.errors ?? []).map((error) => ({
1153
+ path: error.instancePath || '$',
1154
+ message: error.message ?? 'Invalid value',
1155
+ })),
1156
+ };
1157
+ });
1158
+
1159
+ write('').json(payload, { schema: outputSchema, validate });
1160
+ ```
1161
+
1162
+ #### Embedding Schemas in Prompts
1163
+
1164
+ ```typescript
1165
+ write('Return JSON that matches this schema:')
1166
+ .schema(outputSchema, { title: 'Output Schema', tag: 'output_schema' })
1167
+ .toString();
1168
+ ```
1169
+
1170
+ Recommended usage:
1171
+
1172
+ - Prefer JSON Schema for structured output formats.
1173
+ - Store schemas alongside prompt builders and include them in the prompt with `.schema()`.
1174
+ - Validate the object you send or receive, not just the stringified output.
1175
+
999
1176
  ### `.compact()`
1000
1177
 
1001
1178
  Returns a new ProseWriter with consecutive newlines (3+) collapsed to double newlines.
@@ -1080,7 +1257,8 @@ Features:
1080
1257
  Converts the prose to plain text by stripping all markdown formatting.
1081
1258
 
1082
1259
  ```typescript
1083
- import { write, bold } from 'prose-writer';
1260
+ import { write } from 'prose-writer';
1261
+ import { bold } from 'prose-writer/markdown';
1084
1262
 
1085
1263
  const prose = write('')
1086
1264
  .heading(1, 'Title')
@@ -1111,17 +1289,6 @@ const conclusion = write('').heading(2, 'Conclusion').write('Summary');
1111
1289
  const document = ProseWriter.join(intro, body, conclusion);
1112
1290
  ```
1113
1291
 
1114
- ### `ProseWriter.fromTemplate(template: string)`
1115
-
1116
- Static method that creates a ProseWriter from a template string.
1117
-
1118
- ```typescript
1119
- const prompt = ProseWriter.fromTemplate('Hello {{name}}, your role is {{role}}.').fill({
1120
- name: 'Alice',
1121
- role: 'developer',
1122
- });
1123
- ```
1124
-
1125
1292
  ### `.toString()`
1126
1293
 
1127
1294
  Converts the accumulated prose to a string.
@@ -1248,48 +1415,13 @@ const userPrompt = write('Please review the following code:')
1248
1415
  .toString();
1249
1416
  ```
1250
1417
 
1251
- ## Prompt Templates and Variations
1418
+ ## Prompt Variations
1252
1419
 
1253
- Use `fill()` for variable interpolation and `clone()` to create prompt variations:
1420
+ Use `clone()` to create variations without mutating the original prompt:
1254
1421
 
1255
1422
  ```typescript
1256
1423
  import { write } from 'prose-writer';
1257
1424
 
1258
- // Create a reusable template
1259
- const promptTemplate = write('You are a {{role}}.')
1260
- .section('Task', (w) => {
1261
- w.write('Help the user with {{task}}.');
1262
- })
1263
- .section('Guidelines', (w) => {
1264
- w.list('Be {{style}}', 'Focus on {{focus}}');
1265
- })
1266
- .section('Output', (w) => {
1267
- w.table(
1268
- ['Format', 'When to use'],
1269
- [
1270
- ['Code blocks', 'For code examples'],
1271
- ['Bullet points', 'For lists of items'],
1272
- ['Tables', 'For structured data'],
1273
- ],
1274
- );
1275
- });
1276
-
1277
- // Create variations for different use cases
1278
- const codeReviewPrompt = promptTemplate.fill({
1279
- role: 'senior software engineer',
1280
- task: 'code review',
1281
- style: 'thorough and constructive',
1282
- focus: 'code quality and best practices',
1283
- });
1284
-
1285
- const debuggingPrompt = promptTemplate.fill({
1286
- role: 'debugging expert',
1287
- task: 'finding and fixing bugs',
1288
- style: 'systematic and methodical',
1289
- focus: 'root cause analysis',
1290
- });
1291
-
1292
- // Or use clone() for structural variations
1293
1425
  const basePrompt = write('You are an AI assistant.').section('Core Guidelines', (w) => {
1294
1426
  w.list('Be helpful', 'Be accurate');
1295
1427
  });
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { bold, code, image, inline, italic, link, ProseWriter, strike, write, } from './prose-writer';
1
+ export { ProseWriter, write } from './prose-writer';