@hardlydifficult/text 1.0.14 → 1.0.16
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 +174 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,14 +8,24 @@ Text utilities for error formatting, template replacement, text chunking, slugif
|
|
|
8
8
|
npm install @hardlydifficult/text
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import * as text from "@hardlydifficult/text";
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
const
|
|
18
|
-
console.log(
|
|
16
|
+
// Template replacement
|
|
17
|
+
const greeting = text.replaceTemplate("Hello {{name}}!", { name: "World" });
|
|
18
|
+
console.log(greeting); // "Hello World!"
|
|
19
|
+
|
|
20
|
+
// Text chunking
|
|
21
|
+
const chunks = text.chunkText("Line 1\nLine 2\nLong line", 15);
|
|
22
|
+
console.log(chunks); // ["Line 1\nLine 2", "Long line"]
|
|
23
|
+
|
|
24
|
+
// Slugification
|
|
25
|
+
console.log(text.slugify("My Feature Name!")); // "my-feature-name"
|
|
26
|
+
|
|
27
|
+
// Duration formatting
|
|
28
|
+
console.log(text.formatDuration(125_000)); // "2m 5s"
|
|
19
29
|
```
|
|
20
30
|
|
|
21
31
|
## Error Handling
|
|
@@ -85,7 +95,7 @@ extractPlaceholders("{{name}} is in {{place}}"); // ["name", "place"]
|
|
|
85
95
|
|
|
86
96
|
### `chunkText(text: string, maxLength: number): string[]`
|
|
87
97
|
|
|
88
|
-
Split text into chunks of at most `maxLength` characters, preferring line breaks and spaces.
|
|
98
|
+
Split text into chunks of at most `maxLength` characters, preferring line breaks and spaces for natural breaks.
|
|
89
99
|
|
|
90
100
|
```typescript
|
|
91
101
|
import { chunkText } from "@hardlydifficult/text";
|
|
@@ -97,7 +107,7 @@ const chunks = chunkText(text, 20);
|
|
|
97
107
|
|
|
98
108
|
### `slugify(input: string, maxLength?: number): string`
|
|
99
109
|
|
|
100
|
-
Convert a string to a URL/filename-safe slug.
|
|
110
|
+
Convert a string to a URL/filename-safe slug by lowercasing, replacing non-alphanumeric characters with hyphens, and optionally truncating at hyphen boundaries.
|
|
101
111
|
|
|
102
112
|
```typescript
|
|
103
113
|
import { slugify } from "@hardlydifficult/text";
|
|
@@ -123,7 +133,7 @@ formatWithLineNumbers("hello\nworld", 10);
|
|
|
123
133
|
|
|
124
134
|
### `formatDuration(ms: number): string`
|
|
125
135
|
|
|
126
|
-
Format milliseconds as a human-readable duration string.
|
|
136
|
+
Format milliseconds as a human-readable duration string, showing up to two units and using "<1s" for sub-second values.
|
|
127
137
|
|
|
128
138
|
```typescript
|
|
129
139
|
import { formatDuration } from "@hardlydifficult/text";
|
|
@@ -138,7 +148,7 @@ formatDuration(90_000_000); // "1d 1h"
|
|
|
138
148
|
|
|
139
149
|
### `convertFormat(content: string, to: "json" | "yaml"): string`
|
|
140
150
|
|
|
141
|
-
Convert between JSON and YAML string formats with automatic input detection.
|
|
151
|
+
Convert between JSON and YAML string formats with automatic input format detection.
|
|
142
152
|
|
|
143
153
|
```typescript
|
|
144
154
|
import { convertFormat } from "@hardlydifficult/text";
|
|
@@ -156,7 +166,7 @@ convertFormat("name: Alice\nage: 30", "json");
|
|
|
156
166
|
// }
|
|
157
167
|
```
|
|
158
168
|
|
|
159
|
-
### `formatYaml(data:
|
|
169
|
+
### `formatYaml(data: unknown): string`
|
|
160
170
|
|
|
161
171
|
Serialize data to clean YAML, using block literals for long strings containing colons.
|
|
162
172
|
|
|
@@ -169,25 +179,20 @@ formatYaml({ message: "Hello: World" });
|
|
|
169
179
|
|
|
170
180
|
### `healYaml(dirtyYaml: string): string`
|
|
171
181
|
|
|
172
|
-
Clean malformed YAML by stripping code fences and quoting problematic
|
|
182
|
+
Clean malformed YAML by stripping markdown code fences and quoting problematic scalar values containing colons.
|
|
173
183
|
|
|
174
184
|
```typescript
|
|
175
185
|
import { healYaml } from "@hardlydifficult/text";
|
|
176
186
|
|
|
177
|
-
healYaml('
|
|
187
|
+
healYaml('```yaml\nmessage: Hello: World\n```');
|
|
188
|
+
// "message: >\n Hello: World"
|
|
178
189
|
```
|
|
179
190
|
|
|
180
191
|
## Link Generation
|
|
181
192
|
|
|
182
|
-
### `createLinker(
|
|
193
|
+
### `createLinker(initialRules?: LinkRule[]): Linker`
|
|
183
194
|
|
|
184
|
-
Create a configurable linker to transform text patterns into platform-specific links.
|
|
185
|
-
|
|
186
|
-
Supports:
|
|
187
|
-
- Plain strings
|
|
188
|
-
- Idempotent re-runs
|
|
189
|
-
- Automatic skipping of code spans and existing links
|
|
190
|
-
- Deterministic conflict resolution (priority, then longest match, then rule order)
|
|
195
|
+
Create a configurable linker to transform text patterns into platform-specific links. The linker is idempotent by default, skips code spans and existing links, and resolves overlapping matches deterministically.
|
|
191
196
|
|
|
192
197
|
```typescript
|
|
193
198
|
import { createLinker } from "@hardlydifficult/text";
|
|
@@ -203,13 +208,80 @@ linker.linkText("Fix ENG-533 and PR#42", { format: "markdown" });
|
|
|
203
208
|
// "[ENG-533](https://linear.app/fairmint/issue/ENG-533) and [PR#42](https://github.com/Fairmint/api/pull/42)"
|
|
204
209
|
```
|
|
205
210
|
|
|
206
|
-
|
|
211
|
+
#### Linker Methods
|
|
212
|
+
|
|
213
|
+
**`.linear(workspace: string, options?: { name?: string; priority?: number }): Linker`**
|
|
214
|
+
|
|
215
|
+
Add a rule for Linear issue references (e.g., `ENG-533`).
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const linker = createLinker().linear("fairmint");
|
|
219
|
+
linker.linkText("Fix ENG-533", { format: "markdown" });
|
|
220
|
+
// "[ENG-533](https://linear.app/fairmint/issue/ENG-533)"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**`.githubPr(repository: string, options?: { name?: string; priority?: number }): Linker`**
|
|
224
|
+
|
|
225
|
+
Add a rule for GitHub pull request references (e.g., `PR#42`).
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const linker = createLinker().githubPr("Fairmint/api");
|
|
229
|
+
linker.linkText("Merge PR#42", { format: "markdown" });
|
|
230
|
+
// "[PR#42](https://github.com/Fairmint/api/pull/42)"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**`.custom(pattern: RegExp, toHref: string | LinkHrefBuilder, options?: { name?: string; priority?: number }): Linker`**
|
|
234
|
+
|
|
235
|
+
Add a custom rule with a regex pattern and URL template or callback.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const linker = createLinker().custom(
|
|
239
|
+
/\bINC-\d+\b/g,
|
|
240
|
+
({ match }) => `https://incident.io/${match}`
|
|
241
|
+
);
|
|
242
|
+
linker.linkText("Resolve INC-99", { format: "markdown" });
|
|
243
|
+
// "[INC-99](https://incident.io/INC-99)"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**`.linkText(input: string, options?: LinkerApplyOptions): string`**
|
|
247
|
+
|
|
248
|
+
Apply all configured rules to the input text and return formatted links.
|
|
249
|
+
|
|
250
|
+
**`.apply(input: string, options?: LinkerApplyOptions): string`**
|
|
251
|
+
|
|
252
|
+
Alias for `linkText()`.
|
|
253
|
+
|
|
254
|
+
#### Link Options
|
|
255
|
+
|
|
256
|
+
| Option | Type | Default | Description |
|
|
257
|
+
|--------|------|---------|-------------|
|
|
258
|
+
| `format` | `"slack" \| "discord" \| "markdown" \| "plaintext"` | `"markdown"` | Target output format |
|
|
259
|
+
| `platform` | `"slack" \| "discord" \| "markdown" \| "plaintext"` | `"markdown"` | Alias for `format` |
|
|
260
|
+
| `skipCode` | `boolean` | `true` | Skip linkification inside code spans (backticks and fenced blocks) |
|
|
261
|
+
| `skipExistingLinks` | `boolean` | `true` | Skip linkification inside existing links (Slack, Markdown, and plain URLs) |
|
|
262
|
+
|
|
263
|
+
#### URL Template Syntax
|
|
264
|
+
|
|
265
|
+
When using a string template for `href` or `toHref`, the following substitutions are available:
|
|
266
|
+
|
|
267
|
+
- `$0` or `$&` — Full regex match
|
|
268
|
+
- `$1`, `$2`, etc. — Capture groups
|
|
269
|
+
- `$$` — Literal `$`
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const linker = createLinker().custom(
|
|
273
|
+
/\b([A-Z]{2,6})-(\d+)\b/g,
|
|
274
|
+
"https://example.com/issues/$1/$2"
|
|
275
|
+
);
|
|
276
|
+
linker.linkText("ENG-533", { format: "markdown" });
|
|
277
|
+
// "[ENG-533](https://example.com/issues/ENG/533)"
|
|
278
|
+
```
|
|
207
279
|
|
|
208
280
|
## File Tree Rendering
|
|
209
281
|
|
|
210
|
-
### `buildFileTree(files: string[], options?:
|
|
282
|
+
### `buildFileTree(files: string[], options?: BuildTreeOptions): string`
|
|
211
283
|
|
|
212
|
-
Build and render a hierarchical file tree with depth-based truncation and
|
|
284
|
+
Build and render a hierarchical file tree with depth-based truncation and optional annotations.
|
|
213
285
|
|
|
214
286
|
```typescript
|
|
215
287
|
import { buildFileTree } from "@hardlydifficult/text";
|
|
@@ -218,10 +290,88 @@ const files = [
|
|
|
218
290
|
"src/index.ts",
|
|
219
291
|
"src/utils.ts",
|
|
220
292
|
"src/components/Button.tsx",
|
|
293
|
+
"README.md",
|
|
221
294
|
];
|
|
222
295
|
|
|
223
296
|
buildFileTree(files);
|
|
224
|
-
//
|
|
297
|
+
// src/
|
|
298
|
+
// index.ts
|
|
299
|
+
// utils.ts
|
|
300
|
+
// components/
|
|
301
|
+
// Button.tsx
|
|
302
|
+
//
|
|
303
|
+
// README.md
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Tree Options
|
|
307
|
+
|
|
308
|
+
| Option | Type | Default | Description |
|
|
309
|
+
|--------|------|---------|-------------|
|
|
310
|
+
| `maxLevel2` | `number` | `10` | Maximum children to show at depth 2 |
|
|
311
|
+
| `maxLevel3` | `number` | `3` | Maximum children to show at depth 3+ |
|
|
312
|
+
| `annotations` | `Map<string, string>` | — | Descriptions to append to entries (e.g., `"src/index.ts" → "Main entry point"`) |
|
|
313
|
+
| `details` | `Map<string, string[]>` | — | Extra indented lines to show under file entries (e.g., key sections) |
|
|
314
|
+
| `collapseDirs` | `string[]` | — | Directory names to collapse with a summary instead of expanding |
|
|
315
|
+
|
|
316
|
+
#### Annotations Example
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const annotations = new Map([
|
|
320
|
+
["src", "Source code"],
|
|
321
|
+
["src/index.ts", "Main entry point"],
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
buildFileTree(["src/index.ts", "src/utils.ts"], { annotations });
|
|
325
|
+
// src/ — Source code
|
|
326
|
+
// index.ts — Main entry point
|
|
327
|
+
// utils.ts
|
|
225
328
|
```
|
|
226
329
|
|
|
227
|
-
|
|
330
|
+
#### Details Example
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const details = new Map([
|
|
334
|
+
[
|
|
335
|
+
"src/index.ts",
|
|
336
|
+
[
|
|
337
|
+
"> main (5-20): App entry point.",
|
|
338
|
+
"> shutdown (22-35): Cleanup handler.",
|
|
339
|
+
],
|
|
340
|
+
],
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
buildFileTree(["src/index.ts"], { details });
|
|
344
|
+
// src/
|
|
345
|
+
// index.ts
|
|
346
|
+
// > main (5-20): App entry point.
|
|
347
|
+
// > shutdown (22-35): Cleanup handler.
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### Collapsed Directories Example
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
buildFileTree(
|
|
354
|
+
[
|
|
355
|
+
"node_modules/package-a/index.js",
|
|
356
|
+
"node_modules/package-b/index.js",
|
|
357
|
+
"src/index.ts",
|
|
358
|
+
],
|
|
359
|
+
{ collapseDirs: ["node_modules"] }
|
|
360
|
+
);
|
|
361
|
+
// node_modules/
|
|
362
|
+
// (2 files across 2 dirs)
|
|
363
|
+
//
|
|
364
|
+
// src/
|
|
365
|
+
// index.ts
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### `FILE_TREE_DEFAULTS`
|
|
369
|
+
|
|
370
|
+
Default truncation limits for file tree rendering.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { FILE_TREE_DEFAULTS } from "@hardlydifficult/text";
|
|
374
|
+
|
|
375
|
+
console.log(FILE_TREE_DEFAULTS.maxLevel2); // 10
|
|
376
|
+
console.log(FILE_TREE_DEFAULTS.maxLevel3); // 3
|
|
377
|
+
```
|