@hardlydifficult/document-generator 1.1.8 → 1.1.10

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 +245 -87
  2. package/package.json +12 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/document-generator
2
2
 
3
- Platform-agnostic document builder with chainable API and built-in output methods.
3
+ Platform-agnostic document builder with chainable API and multi-format output (Markdown, Slack, plain text).
4
4
 
5
5
  ## Installation
6
6
 
@@ -31,91 +31,269 @@ console.log(document.toSlackText());
31
31
  console.log(document.toPlainText());
32
32
  ```
33
33
 
34
- ## API
34
+ ## Core Blocks
35
35
 
36
- ### `new Document()`
36
+ Build documents by chaining block methods. All methods return `this` for fluent composition.
37
37
 
38
- Create a new document builder. All methods are chainable.
38
+ ```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
48
+ ```
49
+
50
+ ### Inline Markdown
51
+
52
+ Text blocks support standard markdown formatting that auto-converts per platform:
53
+
54
+ ```typescript
55
+ new Document().text('This has **bold**, *italic*, and ~~strikethrough~~ text.');
56
+ ```
57
+
58
+ - **Markdown/Discord:** `**bold**`, `*italic*`, `~~strike~~` (unchanged)
59
+ - **Slack:** Converts to `*bold*`, `_italic_`, `~strike~`
60
+ - **Plain text:** Formatting stripped, text only
61
+
62
+ ## Structured Content
39
63
 
40
- ### Chainable Methods
64
+ ### Sections
41
65
 
42
- | Method | Description |
43
- |--------|-------------|
44
- | `.header(text)` | Add a header/title |
45
- | `.text(content)` | Add text paragraph (supports **bold**, *italic*, ~~strike~~) |
46
- | `.list(items)` | Add a bulleted list |
47
- | `.divider()` | Add a horizontal divider |
48
- | `.context(text)` | Add footer/context text |
49
- | `.link(text, url)` | Add a hyperlink |
50
- | `.code(content)` | Add code (auto-detects inline vs multiline) |
51
- | `.image(url, alt?)` | Add an image |
52
- | `.section(title, content?, options?)` | Add a section (legacy header+divider, or bold title + body/list) |
53
- | `.field(label, value, options?)` | Add a single key-value line (e.g., `**ETA:** Tomorrow`) |
66
+ Add titled sections with optional content (string or list of items):
54
67
 
55
- ### `document.getBlocks(): Block[]`
68
+ ```typescript
69
+ // Legacy style: header + divider
70
+ doc.section("My Section");
71
+
72
+ // With string content
73
+ doc.section("Summary", "All systems operational");
56
74
 
57
- Get the internal block representation for custom processing.
75
+ // With list items
76
+ doc.section("Today", ["Ship feature", "Fix bug", "Review PR"]);
58
77
 
59
- ### String Output Methods
78
+ // With empty state
79
+ doc.section("Blockers", [], { emptyText: "None." });
60
80
 
61
- | Method | Description |
62
- |--------|-------------|
63
- | `.toMarkdown()` | Render as standard markdown |
64
- | `.toSlackText()` | Render as Slack mrkdwn string |
65
- | `.toSlack()` | Alias for `.toSlackText()` |
66
- | `.toPlainText()` | Render as plain text (markdown stripped) |
67
- | `.render(format)` | Render via explicit format (`"markdown"`, `"slack"`, `"plaintext"`) |
81
+ // Ordered list
82
+ doc.section("Steps", ["First", "Second"], { ordered: true });
83
+ ```
68
84
 
69
- ## Inline Markdown Support
85
+ ### Fields and Key-Value Pairs
70
86
 
71
- Text blocks support standard inline markdown that gets auto-converted per platform:
87
+ Add single or multiple key-value lines:
72
88
 
73
89
  ```typescript
74
- new Document().text('This has **bold**, *italic*, and ~~strikethrough~~ text.');
90
+ // Single field
91
+ 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
104
+ ```
105
+
106
+ ### Truncated Lists
107
+
108
+ Display a limited number of items with an "X more" indicator:
109
+
110
+ ```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_
120
+
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
+ });
129
+ ```
130
+
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
138
+
139
+ doc.timestamp({ emoji: false });
140
+ // Output: 2024-02-04T12:00:00.000Z
141
+
142
+ doc.timestamp({ label: "Generated" });
143
+ // Output: Generated 2024-02-04T12:00:00.000Z
144
+
145
+ doc.timestamp({ date: new Date("2025-01-01") });
146
+ // Output: 🕐 2025-01-01T00:00:00.000Z
75
147
  ```
76
148
 
77
- - Standard markdown: `**bold**`, `*italic*`, `~~strike~~`
78
- - Slack: Converted to `*bold*`, `_italic_`, `~strike~`
79
- - Discord: Uses standard markdown (no conversion needed)
80
- - Plain text: Formatting stripped
149
+ ## Output Formats
81
150
 
82
- **Note:** Use `.code()` and `.link()` methods for code and links—not markdown syntax.
151
+ Convert documents to different formats with a single method call:
83
152
 
84
- ## Code Blocks
153
+ ```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
167
+ ```
85
168
 
86
- The `.code()` method auto-detects format:
169
+ ## Linkification
170
+
171
+ Transform text in document blocks (headers, paragraphs, lists, context) while preserving code and explicit links:
87
172
 
88
173
  ```typescript
89
- // Single line inline code
90
- new Document().code('const x = 1'); // → `const x = 1`
174
+ import { Document } from "@hardlydifficult/document-generator";
91
175
 
92
- // Multiline code block
93
- new Document().code('const x = 1;\nconst y = 2;');
94
- // ```
95
- // const x = 1;
96
- // const y = 2;
97
- // ```
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
+ );
98
197
  ```
99
198
 
100
- ## Outputters
199
+ ## Constructor Options
101
200
 
102
- ### `toMarkdown(blocks): string`
201
+ Initialize a document with pre-populated content:
103
202
 
104
- Convert to standard markdown format.
203
+ ```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
+ ```
105
213
 
106
- ### `toSlackText(blocks): string` / `toSlack(blocks): string`
214
+ ## Utility Methods
107
215
 
108
- Convert to Slack mrkdwn format.
216
+ ### `isEmpty()`
109
217
 
110
- ### `toPlainText(blocks): string`
218
+ Check if document has no blocks:
111
219
 
112
- Convert to plain text, stripping all formatting.
220
+ ```typescript
221
+ const doc = new Document();
222
+ doc.isEmpty(); // true
113
223
 
114
- Instance methods are recommended for typical usage; free outputter functions are useful when you already have a `Block[]` array.
224
+ doc.text("Content");
225
+ doc.isEmpty(); // false
226
+ ```
227
+
228
+ ### `clone()`
229
+
230
+ Create a shallow copy of the document:
231
+
232
+ ```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
238
+ ```
239
+
240
+ ### `getBlocks()`
241
+
242
+ Access the raw block array for custom processing:
243
+
244
+ ```typescript
245
+ const blocks = doc.getBlocks();
246
+ // blocks: Block[]
247
+ ```
248
+
249
+ ### `Document.truncate(text, maxLength)`
250
+
251
+ Static utility to truncate text with ellipsis:
252
+
253
+ ```typescript
254
+ Document.truncate("Hello world", 8); // "Hello..."
255
+ Document.truncate("Hi", 10); // "Hi"
256
+ ```
257
+
258
+ ## Direct Outputter Functions
259
+
260
+ For cases where you already have a `Block[]` array, use outputter functions directly:
261
+
262
+ ```typescript
263
+ import { toMarkdown, toSlackText, toPlainText } from '@hardlydifficult/document-generator';
264
+
265
+ const blocks = [
266
+ { type: 'header', text: 'Title' },
267
+ { type: 'text', content: 'Body' }
268
+ ];
269
+
270
+ toMarkdown(blocks); // # Title\n\nBody\n\n
271
+ toSlackText(blocks); // *Title*\n\nBody\n\n
272
+ toPlainText(blocks); // TITLE\n\nBody\n\n
273
+ ```
274
+
275
+ ## Markdown Conversion Utilities
276
+
277
+ Convert or strip markdown formatting for custom outputters:
278
+
279
+ ```typescript
280
+ import { convertMarkdown, stripMarkdown } from '@hardlydifficult/document-generator';
281
+
282
+ // Convert to platform-specific format
283
+ convertMarkdown("**bold** and *italic*", "slack");
284
+ // → "*bold* and _italic_"
285
+
286
+ convertMarkdown("**bold** and *italic*", "markdown");
287
+ // → "**bold** and *italic*"
288
+
289
+ // Strip all markdown
290
+ stripMarkdown("**bold** and *italic*");
291
+ // → "bold and italic"
292
+ ```
115
293
 
116
294
  ## Block Types
117
295
 
118
- Internal block types for custom processing:
296
+ Internal block structure for advanced use cases:
119
297
 
120
298
  ```typescript
121
299
  type Block =
@@ -150,36 +328,16 @@ const report = new Document()
150
328
  await channel.postMessage(report);
151
329
  ```
152
330
 
153
- ## Linkifying Text Blocks
154
-
155
- Apply an issue/PR linker to document text-bearing blocks (`header`, `text`, `list`, `context`) while leaving explicit `code` and `link` blocks unchanged.
156
-
157
- ```typescript
158
- import { Document } from "@hardlydifficult/document-generator";
159
- import { createLinker } from "@hardlydifficult/text";
160
-
161
- const linker = createLinker()
162
- .linear("fairmint")
163
- .githubPr("Fairmint/api");
164
-
165
- const doc = new Document()
166
- .header("Sprint Update ENG-533")
167
- .text("Shipped ENG-533 and reviewed PR#42")
168
- .code("ENG-533 inside code is untouched")
169
- .link("PR", "https://github.com/Fairmint/api/pull/42")
170
- .linkify(linker, { platform: "slack" });
171
- ```
172
-
173
- `linkify()` also accepts a simple `(text) => text` transformer function.
174
-
175
- ## Structured Sections and Fields
176
-
177
- For status updates and standups, use `section` and `field` to keep client code concise:
178
-
179
- ```typescript
180
- const detail = new Document()
181
- .section("Yesterday", ["Reviewed PR #42", "Fixed flaky test"])
182
- .section("Today", ["Ship ENG-533", "Pair on migration"])
183
- .section("Blockers", [], { emptyText: "None." })
184
- .field("ETA", "Tomorrow");
185
- ```
331
+ ## Appendix: Platform Differences
332
+
333
+ | Feature | Markdown | Slack | Plain Text |
334
+ |---------|----------|-------|-----------|
335
+ | **Bold** | `**text**` | `*text*` | text |
336
+ | *Italic* | `*text*` | `_text_` | text |
337
+ | ~~Strike~~ | `~~text~~` | `~text~` | text |
338
+ | Headers | `# Title` | `*Title*` | TITLE |
339
+ | Dividers | `---` | `────────────────` | `────────────────` |
340
+ | Links | `[text](url)` | `<url\|text>` | `text (url)` |
341
+ | Code (inline) | `` `code` `` | `` `code` `` | code |
342
+ | Code (block) | ` ```code``` ` | ` ```code``` ` | code |
343
+ | Images | `![alt](url)` | `Image: <url\|alt>` | `[Image: alt]` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/document-generator",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -17,5 +17,16 @@
17
17
  "typescript": "5.9.3",
18
18
  "@types/node": "25.2.3",
19
19
  "vitest": "4.0.18"
20
+ },
21
+ "type": "commonjs",
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.js"
30
+ }
20
31
  }
21
32
  }