@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.
- package/README.md +245 -87
- 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
|
|
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
|
-
##
|
|
34
|
+
## Core Blocks
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Build documents by chaining block methods. All methods return `this` for fluent composition.
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
###
|
|
64
|
+
### Sections
|
|
41
65
|
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
// With list items
|
|
76
|
+
doc.section("Today", ["Ship feature", "Fix bug", "Review PR"]);
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
// With empty state
|
|
79
|
+
doc.section("Blockers", [], { emptyText: "None." });
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
85
|
+
### Fields and Key-Value Pairs
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
Add single or multiple key-value lines:
|
|
72
88
|
|
|
73
89
|
```typescript
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
Convert documents to different formats with a single method call:
|
|
83
152
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
new Document().code('const x = 1'); // → `const x = 1`
|
|
174
|
+
import { Document } from "@hardlydifficult/document-generator";
|
|
91
175
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
##
|
|
199
|
+
## Constructor Options
|
|
101
200
|
|
|
102
|
-
|
|
201
|
+
Initialize a document with pre-populated content:
|
|
103
202
|
|
|
104
|
-
|
|
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
|
-
|
|
214
|
+
## Utility Methods
|
|
107
215
|
|
|
108
|
-
|
|
216
|
+
### `isEmpty()`
|
|
109
217
|
|
|
110
|
-
|
|
218
|
+
Check if document has no blocks:
|
|
111
219
|
|
|
112
|
-
|
|
220
|
+
```typescript
|
|
221
|
+
const doc = new Document();
|
|
222
|
+
doc.isEmpty(); // true
|
|
113
223
|
|
|
114
|
-
|
|
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
|
|
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
|
-
##
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 | `` | `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.
|
|
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
|
}
|