@hardlydifficult/document-generator 1.1.11 → 1.1.13

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 (2) hide show
  1. package/README.md +182 -190
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -11,284 +11,260 @@ npm install @hardlydifficult/document-generator
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { Document } from '@hardlydifficult/document-generator';
14
+ import { Document } from "@hardlydifficult/document-generator";
15
15
 
16
- const document = new Document()
17
- .header("Weekly Report")
18
- .text("Summary of this week's **key highlights**.")
19
- .list(["Completed feature A", "Fixed bug B", "Started project C"])
20
- .divider()
21
- .link("View details", "https://example.com")
22
- .context("Generated on 2025-01-15");
16
+ const doc = new Document({
17
+ header: "Release Notes",
18
+ sections: [
19
+ { title: "New Features", content: "Ship onboarding flow\nFix retry bug" },
20
+ ],
21
+ context: { Network: "mainnet", Status: "active" },
22
+ });
23
23
 
24
- // Output as markdown
25
- console.log(document.toMarkdown());
24
+ console.log(doc.toMarkdown());
25
+ // # Release Notes
26
+ //
27
+ // **New Features**
28
+ // Ship onboarding flow
29
+ // Fix retry bug
30
+ //
31
+ // ---
32
+ //
33
+ // *Network: mainnet, Status: active*
34
+
35
+ console.log(doc.toSlack());
36
+ // *Release Notes*
37
+ //
38
+ // **New Features**
39
+ // Ship onboarding flow
40
+ // Fix retry bug
41
+ //
42
+ // ────────────────
43
+ //
44
+ // Network: mainnet, Status: active
45
+ ```
26
46
 
27
- // Output as Slack mrkdwn
28
- console.log(document.toSlackText());
47
+ ## Core Document API
29
48
 
30
- // Output as plain text
31
- console.log(document.toPlainText());
32
- ```
49
+ The `Document` class provides a fluent, chainable builder for structured content.
33
50
 
34
- ## Core Blocks
51
+ ### Constructor
35
52
 
36
- Build documents by chaining block methods. All methods return `this` for fluent composition.
53
+ Creates a new document, optionally initialized with header, sections, and context.
37
54
 
38
55
  ```typescript
39
- const doc = new Document()
40
- .header("Title") // # Title
41
- .text("Paragraph with **bold** text") // Supports inline markdown
42
- .list(["Item 1", "Item 2"]) // Bulleted list
43
- .divider() // Horizontal line
44
- .link("Click here", "https://example.com") // Hyperlink
45
- .code("const x = 1;") // Inline or multiline code
46
- .image("https://example.com/img.png") // Image with optional alt text
47
- .context("Footer text"); // Italicized context
56
+ const doc = new Document({
57
+ header: "My Report",
58
+ sections: [
59
+ { title: "Summary", content: "All systems operational" },
60
+ { content: "No issues found" },
61
+ ],
62
+ context: { Network: "mainnet", Status: "active" },
63
+ });
48
64
  ```
49
65
 
50
- ### Inline Markdown
66
+ ### Block Methods
51
67
 
52
- Text blocks support standard markdown formatting that auto-converts per platform:
68
+ These methods add specific block types and return `this` for chaining.
53
69
 
54
70
  ```typescript
55
- new Document().text('This has **bold**, *italic*, and ~~strikethrough~~ text.');
71
+ const doc = new Document()
72
+ .header("Title")
73
+ .text("Content")
74
+ .list(["Item 1", "Item 2"])
75
+ .divider()
76
+ .context("Context text")
77
+ .link("Visit Site", "https://example.com")
78
+ .code("const x = 1;")
79
+ .image("https://example.com/image.png", "Alt text");
56
80
  ```
57
81
 
58
- - **Markdown/Discord:** `**bold**`, `*italic*`, `~~strike~~` (unchanged)
59
- - **Slack:** Converts to `*bold*`, `_italic_`, `~strike~`
60
- - **Plain text:** Formatting stripped, text only
82
+ ### Convenience Methods
61
83
 
62
- ## Structured Content
84
+ #### `section(title, content?, options?)`
63
85
 
64
- ### Sections
65
-
66
- Add titled sections with optional content (string or list of items):
86
+ Renders section titles as bold with optional content.
67
87
 
68
88
  ```typescript
69
- // Legacy style: header + divider
70
- doc.section("My Section");
71
-
72
- // With string content
73
- doc.section("Summary", "All systems operational");
89
+ doc.section("Summary").text("All systems green");
90
+ // Output: **Summary**\nAll systems green
74
91
 
75
- // With list items
76
- doc.section("Today", ["Ship feature", "Fix bug", "Review PR"]);
92
+ doc.section("Today", ["Ship onboarding", "Fix flaky test"]);
93
+ // Output: **Today**\n- Ship onboarding\n- Fix flaky test
77
94
 
78
- // With empty state
79
95
  doc.section("Blockers", [], { emptyText: "None." });
80
-
81
- // Ordered list
82
- doc.section("Steps", ["First", "Second"], { ordered: true });
96
+ // Output: **Blockers**\n- None.
83
97
  ```
84
98
 
85
- ### Fields and Key-Value Pairs
99
+ | Option | Type | Default | Description |
100
+ |--------|------|---------|-------------|
101
+ | `emptyText` | `string` | _none_ | Fallback when content is empty |
102
+ | `ordered` | `boolean` | `false` | Render list as numbered instead of bullets |
103
+ | `divider` | `boolean` | `false` | Insert divider before section output |
104
+
105
+ #### `field(label, value, options?)`
86
106
 
87
- Add single or multiple key-value lines:
107
+ Renders a single key-value pair.
88
108
 
89
109
  ```typescript
90
- // Single field
91
110
  doc.field("ETA", "Tomorrow");
92
- // Output: **ETA**: Tomorrow
93
-
94
- // Multiple fields
95
- doc.keyValue({ Network: "mainnet", Status: "active" });
96
- // Output: **Network**: mainnet\n**Status**: active
97
-
98
- // With styling options
99
- doc.keyValue(
100
- { Name: "Alice", Role: "Admin" },
101
- { style: "bullet", separator: " =", bold: false }
102
- );
103
- // Output: • Name = Alice\n• Role = Admin
111
+ // Output: **ETA:** Tomorrow
112
+
113
+ doc.field("Blockers", "", { emptyText: "None." });
114
+ // Output: **Blockers:** None.
104
115
  ```
105
116
 
106
- ### Truncated Lists
117
+ | Option | Type | Default | Description |
118
+ |--------|------|---------|-------------|
119
+ | `emptyText` | `string` | _none_ | Fallback when value is empty |
120
+ | `separator` | `string` | `":"` | Separator between label and value |
121
+ | `bold` | `boolean` | `true` | Whether to bold the label |
107
122
 
108
- Display a limited number of items with an "X more" indicator:
123
+ #### `keyValue(data, options?)`
124
+
125
+ Formats an object as key-value lines.
109
126
 
110
127
  ```typescript
111
- doc.truncatedList(
112
- ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"],
113
- { limit: 3 }
114
- );
115
- // Output:
116
- // • Item 1
117
- // • Item 2
118
- // • Item 3
119
- // _... and 2 more_
128
+ doc.keyValue({ Name: "Alice", Role: "Admin" });
129
+ // Output: **Name:** Alice\n**Role:** Admin
120
130
 
121
- // Custom formatting
122
- const users = [{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }];
123
- doc.truncatedList(users, {
124
- limit: 2,
125
- format: (u) => u.name,
126
- moreText: (n) => `Plus ${n} others`,
127
- ordered: true
128
- });
131
+ doc.keyValue({ A: "1", B: "2" }, { style: "bullet" });
132
+ // Output: **A**: 1\n• **B**: 2
129
133
  ```
130
134
 
131
- ### Timestamps
132
-
133
- Add ISO timestamps with optional emoji and label:
134
-
135
- ```typescript
136
- doc.timestamp();
137
- // Output: 🕐 2024-02-04T12:00:00.000Z
135
+ | Option | Type | Default | Description |
136
+ |--------|------|---------|-------------|
137
+ | `style` | `"plain" \| "bullet" \| "numbered"` | `"plain"` | List style |
138
+ | `separator` | `string` | `":"` | Key-value separator |
139
+ | `bold` | `boolean` | `true` | Bold the keys |
138
140
 
139
- doc.timestamp({ emoji: false });
140
- // Output: 2024-02-04T12:00:00.000Z
141
+ #### `truncatedList(items, options?)`
141
142
 
142
- doc.timestamp({ label: "Generated" });
143
- // Output: Generated 2024-02-04T12:00:00.000Z
143
+ Renders a list with automatic truncation.
144
144
 
145
- doc.timestamp({ date: new Date("2025-01-01") });
146
- // Output: 🕐 2025-01-01T00:00:00.000Z
145
+ ```typescript
146
+ doc.truncatedList(["a", "b", "c", "d", "e"], { limit: 3 });
147
+ // Output:
148
+ // • a
149
+ // • b
150
+ // • c
151
+ // _... and 2 more_
147
152
  ```
148
153
 
149
- ## Output Formats
154
+ | Option | Type | Default | Description |
155
+ |--------|------|---------|-------------|
156
+ | `limit` | `number` | `10` | Maximum items to show |
157
+ | `format` | `(item, index) => string` | `String` | Custom formatter |
158
+ | `moreText` | `(remaining) => string` | `_... and N more_` | Truncation text |
159
+ | `ordered` | `boolean` | `false` | Numbered list |
160
+
161
+ #### `timestamp(options?)`
150
162
 
151
- Convert documents to different formats with a single method call:
163
+ Adds a timestamp in context format.
152
164
 
153
165
  ```typescript
154
- const doc = new Document()
155
- .header("Report")
156
- .text("Status: **active**");
157
-
158
- doc.toMarkdown(); // # Report\n\nStatus: **active**
159
- doc.toSlackText(); // *Report*\n\nStatus: *active*
160
- doc.toSlack(); // Alias for toSlackText()
161
- doc.toPlainText(); // REPORT\n\nStatus: active
162
-
163
- // Or use render() with explicit format
164
- doc.render("markdown"); // Standard markdown
165
- doc.render("slack"); // Slack mrkdwn
166
- doc.render("plaintext"); // Plain text
166
+ doc.timestamp(); // 🕐 2024-02-04T12:00:00.000Z
167
+ doc.timestamp({ emoji: false }); // 2024-02-04T12:00:00.000Z
168
+ doc.timestamp({ label: "Generated" }); // Generated 2024-02-04T...
167
169
  ```
168
170
 
169
- ## Linkification
170
-
171
- Transform text in document blocks (headers, paragraphs, lists, context) while preserving code and explicit links:
172
-
173
- ```typescript
174
- import { Document } from "@hardlydifficult/document-generator";
171
+ | Option | Type | Default | Description |
172
+ |--------|------|---------|-------------|
173
+ | `date` | `Date` | `new Date()` | Custom date |
174
+ | `emoji` | `boolean` | `true` | Include clock emoji |
175
+ | `label` | `string` | _none_ | Prefix label |
175
176
 
176
- const doc = new Document()
177
- .header("Sprint ENG-533")
178
- .text("Shipped ENG-533 and reviewed PR#42")
179
- .code("ENG-533 inside code is untouched")
180
- .link("PR", "https://github.com/example/pull/42");
181
-
182
- // Simple function transformer
183
- doc.linkify((text) => text.replace("ENG-533", "[ENG-533](...)"));
184
-
185
- // Linker-style object with platform awareness
186
- doc.linkify(
187
- {
188
- linkText: (text, { platform }) => {
189
- if (text.includes("ENG-")) {
190
- return `[${text}](https://linear.app/...)`;
191
- }
192
- return text;
193
- }
194
- },
195
- { platform: "slack" }
196
- );
197
- ```
177
+ ### Utility Methods
198
178
 
199
- ## Constructor Options
179
+ #### `linkify(transform, options?)`
200
180
 
201
- Initialize a document with pre-populated content:
181
+ Applies transformations to text-bearing blocks (headers, paragraphs, lists, context).
202
182
 
203
183
  ```typescript
204
- const doc = new Document({
205
- header: "Daily Report",
206
- sections: [
207
- { title: "Summary", content: "All systems operational" },
208
- { content: "No blockers" }
209
- ],
210
- context: { Network: "mainnet", Status: "active" }
211
- });
212
- ```
184
+ doc.linkify((text) => text.toUpperCase());
213
185
 
214
- ## Utility Methods
186
+ // Example with linkText-style transformer:
187
+ doc.linkify({ linkText: (t) => t.replace("ENG-533", "LINKED") }, { platform: "slack" });
188
+ ```
215
189
 
216
- ### `isEmpty()`
190
+ #### `isEmpty(): boolean`
217
191
 
218
- Check if document has no blocks:
192
+ Returns `true` if no blocks have been added.
219
193
 
220
194
  ```typescript
221
- const doc = new Document();
222
- doc.isEmpty(); // true
223
-
224
- doc.text("Content");
225
- doc.isEmpty(); // false
195
+ new Document().isEmpty(); // true
196
+ new Document().text("Content").isEmpty(); // false
226
197
  ```
227
198
 
228
- ### `clone()`
199
+ #### `clone(): Document`
229
200
 
230
- Create a shallow copy of the document:
201
+ Creates a shallow copy.
231
202
 
232
203
  ```typescript
233
- const original = new Document().header("Title").text("Content");
234
- const copy = original.clone();
235
-
236
- copy.text("Added to copy");
237
- // original still has 2 blocks, copy has 3
204
+ const doc1 = new Document().header("Title");
205
+ const doc2 = doc1.clone();
206
+ doc2.text("Extra"); // does not affect doc1
238
207
  ```
239
208
 
240
- ### `getBlocks()`
209
+ #### `getBlocks(): Block[]`
241
210
 
242
- Access the raw block array for custom processing:
211
+ Returns the internal blocks array for inspection.
243
212
 
244
213
  ```typescript
245
- const blocks = doc.getBlocks();
246
- // blocks: Block[]
214
+ const blocks = new Document().header("Title").getBlocks();
215
+ // [{ type: "header", text: "Title" }]
247
216
  ```
248
217
 
249
- ### `Document.truncate(text, maxLength)`
218
+ ### Output Methods
250
219
 
251
- Static utility to truncate text with ellipsis:
220
+ Render the document to different formats.
252
221
 
253
222
  ```typescript
254
- Document.truncate("Hello world", 8); // "Hello..."
255
- Document.truncate("Hi", 10); // "Hi"
223
+ const doc = new Document().text("Hello **world**");
224
+
225
+ doc.toMarkdown(); // "**world**" preserved
226
+ doc.toSlack(); // "*world*" (italic)
227
+ doc.toPlainText(); // "world" (stripped)
228
+ doc.render("slack"); // same as toSlack()
256
229
  ```
257
230
 
258
231
  ## Direct Outputter Functions
259
232
 
260
- For cases where you already have a `Block[]` array, use outputter functions directly:
233
+ Use the outputters directly without `Document` if you have raw blocks.
261
234
 
262
235
  ```typescript
263
- import { toMarkdown, toSlackText, toPlainText } from '@hardlydifficult/document-generator';
236
+ import { toMarkdown, toPlainText, toSlackText } from "@hardlydifficult/document-generator";
264
237
 
265
238
  const blocks = [
266
- { type: 'header', text: 'Title' },
267
- { type: 'text', content: 'Body' }
239
+ { type: "header", text: "Title" },
240
+ { type: "text", content: "Body with **bold**" },
268
241
  ];
269
242
 
270
- toMarkdown(blocks); // # Title\n\nBody\n\n
271
- toSlackText(blocks); // *Title*\n\nBody\n\n
272
- toPlainText(blocks); // TITLE\n\nBody\n\n
243
+ console.log(toMarkdown(blocks)); // # Title\n\nBody with **bold**
244
+ console.log(toSlackText(blocks)); // *Title*\n\nBody with *bold*
245
+ console.log(toPlainText(blocks)); // TITLE\n\nBody with bold
273
246
  ```
274
247
 
275
- ## Markdown Conversion Utilities
248
+ ## Markdown Conversion
276
249
 
277
- Convert or strip markdown formatting for custom outputters:
250
+ ### `convertMarkdown(text, platform)`
251
+
252
+ Converts inline markdown formatting to target platform syntax.
278
253
 
279
254
  ```typescript
280
- import { convertMarkdown, stripMarkdown } from '@hardlydifficult/document-generator';
255
+ convertMarkdown("**bold** and *italic*", "slack"); // "*bold* and _italic_"
256
+ convertMarkdown("~~strike~~", "discord"); // "~~strike~~"
257
+ convertMarkdown("**bold**", "plaintext"); // "bold"
258
+ ```
281
259
 
282
- // Convert to platform-specific format
283
- convertMarkdown("**bold** and *italic*", "slack");
284
- // → "*bold* and _italic_"
260
+ Supported platforms: `"markdown"`, `"slack"`, `"discord"`, `"plaintext"`
285
261
 
286
- convertMarkdown("**bold** and *italic*", "markdown");
287
- // → "**bold** and *italic*"
262
+ ### `stripMarkdown(text)`
288
263
 
289
- // Strip all markdown
290
- stripMarkdown("**bold** and *italic*");
291
- // → "bold and italic"
264
+ Strips all formatting and returns plain text.
265
+
266
+ ```typescript
267
+ stripMarkdown("**bold** and *italic* and ~~strike~~"); // "bold and italic and strike"
292
268
  ```
293
269
 
294
270
  ## Block Types
@@ -307,6 +283,22 @@ type Block =
307
283
  | { type: 'image'; url: string; alt?: string };
308
284
  ```
309
285
 
286
+ ## Types
287
+
288
+ All exported types are listed below:
289
+
290
+ | Name | Description |
291
+ |--|--|
292
+ | `Block` | Union type of all block types |
293
+ | `HeaderBlock`, `TextBlock`, `ListBlock`, `DividerBlock`, `ContextBlock`, `LinkBlock`, `CodeBlock`, `ImageBlock` | Block structures |
294
+ | `Platform` | `"markdown" \| "slack" \| "discord" \| "plaintext"` |
295
+ | `StringOutputFormat` | `"markdown" \| "slack" \| "plaintext"` |
296
+ | `DocumentOptions`, `DocumentSection` | Constructor options |
297
+ | `SectionOptions`, `FieldOptions`, `KeyValueOptions` | Formatting options |
298
+ | `TruncatedListOptions<T>` | Truncation configuration |
299
+ | `TimestampOptions` | Timestamp configuration |
300
+ | `DocumentLinkifier`, `DocumentLinkTransform`, `DocumentLinkifyOptions` | Link transformation types |
301
+
310
302
  ## Integration with @hardlydifficult/chat
311
303
 
312
304
  Documents integrate seamlessly with the chat package for Slack and Discord:
@@ -331,7 +323,7 @@ await channel.postMessage(report);
331
323
  ## Appendix: Platform Differences
332
324
 
333
325
  | Feature | Markdown | Slack | Plain Text |
334
- |---------|----------|-------|-----------|
326
+ |---------|---------|-------|-----------|
335
327
  | **Bold** | `**text**` | `*text*` | text |
336
328
  | *Italic* | `*text*` | `_text_` | text |
337
329
  | ~~Strike~~ | `~~text~~` | `~text~` | text |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/document-generator",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "type": "commonjs",
22
22
  "engines": {
23
- "node": ">=18.0.0"
23
+ "node": ">=20.19.0"
24
24
  },
25
25
  "exports": {
26
26
  ".": {