@hardlydifficult/text 1.0.29 → 1.0.31

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 +209 -231
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/text
2
2
 
3
- Text utilities for error formatting, template replacement, text chunking, slugification, duration formatting, YAML/JSON conversion, link generation, and file tree rendering.
3
+ Collection of text utility functions for error formatting, template replacement, chunking, slugification, duration formatting, YAML/JSON conversion, link generation, and file tree rendering.
4
4
 
5
5
  ## Installation
6
6
 
@@ -12,77 +12,89 @@ npm install @hardlydifficult/text
12
12
 
13
13
  ```typescript
14
14
  import {
15
+ formatError,
15
16
  replaceTemplate,
16
17
  chunkText,
17
18
  slugify,
18
19
  formatDuration,
20
+ formatWithLineNumbers,
19
21
  buildFileTree,
20
22
  convertFormat,
21
- createLinker,
22
23
  healYaml,
24
+ createLinker,
23
25
  escapeFence,
26
+ stripAnsi
24
27
  } from "@hardlydifficult/text";
25
28
 
29
+ // Format an error with context
30
+ formatError(new Error("File not found"), "Failed to load");
31
+ // "Failed to load: File not found"
32
+
26
33
  // Replace template placeholders
27
34
  replaceTemplate("Hello {{name}}!", { name: "World" });
28
35
  // "Hello World!"
29
36
 
30
- // Split long text into chunks
31
- chunkText("This is a long text", 10);
32
- // ["This is a", "long text"]
37
+ // Split text into chunks
38
+ chunkText("line1\nline2\nline3", 10);
39
+ // ["line1\nline2", "line3"]
33
40
 
34
- // Convert to URL-safe slugs
35
- slugify("My Feature Name!");
36
- // "my-feature-name"
41
+ // Create URL-safe slugs
42
+ slugify("My Feature Name!", 10);
43
+ // "my-feature"
37
44
 
38
- // Format durations
45
+ // Format duration as human-readable string
39
46
  formatDuration(125_000);
40
47
  // "2m 5s"
41
48
 
42
- // Build file trees
43
- buildFileTree(["src/index.ts", "README.md"]);
44
- // "```\nsrc/\n index.ts\n\nREADME.md\n```"
49
+ // Add line numbers to text
50
+ formatWithLineNumbers("foo\nbar", 10);
51
+ // "10: foo\n11: bar"
45
52
 
46
- // Convert between JSON and YAML
47
- convertFormat('{"name":"Alice"}', "yaml");
48
- // "name: Alice\n"
49
- convertFormat("name: Alice", "json");
50
- // "{\n \"name\": \"Alice\"\n}"
53
+ // Build a file tree from paths
54
+ buildFileTree(["src/index.ts", "src/utils.ts"], { format: "plain" });
55
+ // "src/\n index.ts\n utils.ts"
51
56
 
52
- // Apply link rules to text
53
- const linker = createLinker().linear("my-org");
54
- linker.apply("Fix ENG-533", { platform: "markdown" });
55
- // "Fix [ENG-533](https://linear.app/my-org/issue/ENG-533)"
57
+ // Convert between JSON and YAML
58
+ convertFormat('{"key": "value"}', "yaml");
59
+ // "key: value\n"
56
60
 
57
61
  // Heal malformed YAML
58
- healYaml("```yaml\nkey: value\n```");
59
- // "key: value"
62
+ healYaml('description: "Text with: colon"');
63
+ // 'description: "Text with: colon"'
64
+
65
+ // Linkify issue references
66
+ const linker = createLinker().linear("fairmint");
67
+ linker.apply("Fix ENG-533", { format: "slack" });
68
+ // "Fix <https://linear.app/fairmint/issue/ENG-533|ENG-533>"
69
+
70
+ // Escape markdown fences
71
+ escapeFence("code with ``` backticks").fence;
72
+ // "````"
73
+
74
+ // Strip ANSI codes
75
+ stripAnsi("\x1b[31mRed text\x1b[0m");
76
+ // "Red text"
60
77
  ```
61
78
 
62
- ## Error Formatting
79
+ ## Error Handling
63
80
 
64
- Consistent error handling utilities for message extraction and formatting.
81
+ Consistent error message extraction and formatting utilities for user-facing and logging contexts.
65
82
 
66
- ### `getErrorMessage`
83
+ ### getErrorMessage
67
84
 
68
- Extract a message string from an unknown error.
85
+ Extracts a string message from any error-like value.
69
86
 
70
87
  ```typescript
71
88
  import { getErrorMessage } from "@hardlydifficult/text";
72
89
 
73
- getErrorMessage(new Error("something went wrong"));
74
- // "something went wrong"
75
-
76
- getErrorMessage("plain string error");
77
- // "plain string error"
78
-
79
- getErrorMessage(42);
80
- // "42"
90
+ getErrorMessage(new Error("Oops")); // "Oops"
91
+ getErrorMessage("plain string"); // "plain string"
92
+ getErrorMessage(42); // "42"
81
93
  ```
82
94
 
83
- ### `formatError`
95
+ ### formatError
84
96
 
85
- Format an error for user-facing output with optional context.
97
+ Formats an error with optional context prefix.
86
98
 
87
99
  ```typescript
88
100
  import { formatError } from "@hardlydifficult/text";
@@ -94,27 +106,24 @@ formatError(new Error("disk full"), "Failed to save");
94
106
  // "Failed to save: disk full"
95
107
  ```
96
108
 
97
- ### `formatErrorForLog`
109
+ ### formatErrorForLog
98
110
 
99
- Format an error for logging (includes more detail for non-Error types).
111
+ Formats an error for logging (returns message for Error instances, stringifies others).
100
112
 
101
113
  ```typescript
102
114
  import { formatErrorForLog } from "@hardlydifficult/text";
103
115
 
104
- formatErrorForLog(new Error("timeout"));
105
- // "timeout"
106
-
107
- formatErrorForLog({ code: 500 });
108
- // "[object Object]"
116
+ formatErrorForLog(new Error("timeout")); // "timeout"
117
+ formatErrorForLog({ code: 500 }); // "[object Object]"
109
118
  ```
110
119
 
111
120
  ## Template Replacement
112
121
 
113
- Simple string interpolation using `{{variable}}` syntax.
122
+ Simple template utility for placeholder replacement using `{{variable}}` syntax.
114
123
 
115
- ### `replaceTemplate`
124
+ ### replaceTemplate
116
125
 
117
- Replace template placeholders with values.
126
+ Replaces `{{variable}}` placeholders with provided values.
118
127
 
119
128
  ```typescript
120
129
  import { replaceTemplate } from "@hardlydifficult/text";
@@ -122,176 +131,173 @@ import { replaceTemplate } from "@hardlydifficult/text";
122
131
  replaceTemplate("Hello {{name}}!", { name: "World" });
123
132
  // "Hello World!"
124
133
 
125
- replaceTemplate("{{greeting}}, {{name}}!", {
126
- greeting: "Hi",
127
- name: "Alice",
128
- });
129
- // "Hi, Alice!"
130
-
131
- replaceTemplate("Hello {{name}}!", {});
132
- // "Hello {{name}}!"
134
+ replaceTemplate("{{greeting}}, {{name}}!", { greeting: "Hi" });
135
+ // "Hi, {{name}}!" // missing key leaves placeholder unchanged
133
136
  ```
134
137
 
135
- ### `extractPlaceholders`
138
+ ### extractPlaceholders
136
139
 
137
- Extract all placeholder names from a template.
140
+ Extracts unique placeholder names from a template.
138
141
 
139
142
  ```typescript
140
143
  import { extractPlaceholders } from "@hardlydifficult/text";
141
144
 
142
145
  extractPlaceholders("{{a}} and {{b}} and {{a}} again");
143
146
  // ["a", "b"]
144
-
145
- extractPlaceholders("no placeholders here");
146
- // []
147
147
  ```
148
148
 
149
149
  ## Text Chunking
150
150
 
151
- Split long text into manageable chunks, preferring natural break points.
151
+ Splits long text into manageable chunks, preferring natural breaks.
152
+
153
+ ### chunkText
154
+
155
+ Splits text at line breaks or spaces, falling back to hard breaks when necessary.
152
156
 
153
157
  ```typescript
154
158
  import { chunkText } from "@hardlydifficult/text";
155
159
 
156
- chunkText("line one\nline two\nline three", 18);
157
- // ["line one\nline two", "line three"]
160
+ chunkText("word1 word2 word3", 12);
161
+ // ["word1 word2", "word3"]
158
162
 
159
- chunkText("word1 word2 word3 word4 word5", 17);
160
- // ["word1 word2 word3", "word4 word5"]
161
-
162
- chunkText("abcdefghijklmnopqrstuvwxyz", 10);
163
- // ["abcdefghij", "klmnopqrst", "uvwxyz"]
163
+ chunkText("line1\nline2\nline3", 10);
164
+ // ["line1\nline2", "line3"]
164
165
  ```
165
166
 
166
167
  ## Slugification
167
168
 
168
- Convert strings into URL/filename-safe slugs.
169
+ Converts strings into URL/filename-safe slugs.
169
170
 
170
- ```typescript
171
- import { slugify } from "@hardlydifficult/text";
171
+ ### slugify
172
172
 
173
- slugify("My Feature Name!");
174
- // "my-feature-name"
173
+ Lowercases, replaces non-alphanumeric characters with hyphens, and optionally truncates at hyphen boundaries.
175
174
 
176
- slugify("My Feature Name!", 10);
177
- // "my-feature"
175
+ ```typescript
176
+ import { slugify } from "@hardlydifficult/text";
178
177
 
179
- slugify(" Hello World ");
180
- // "hello-world"
178
+ slugify("My Feature Name!"); // "my-feature-name"
179
+ slugify("My Feature Name!", 10); // "my-feature"
180
+ slugify(" Hello World "); // "hello-world"
181
181
  ```
182
182
 
183
183
  ## Duration Formatting
184
184
 
185
- Format duration in milliseconds as a human-readable string.
185
+ Formats milliseconds as human-readable strings.
186
+
187
+ ### formatDuration
188
+
189
+ Renders durations with up to two units, skipping trailing zeros.
186
190
 
187
191
  ```typescript
188
192
  import { formatDuration } from "@hardlydifficult/text";
189
193
 
190
- formatDuration(125_000);
191
- // "2m 5s"
194
+ formatDuration(500); // "<1s"
195
+ formatDuration(125_000); // "2m 5s"
196
+ formatDuration(3_600_000); // "1h"
197
+ formatDuration(86_400_000); // "1d"
198
+ ```
192
199
 
193
- formatDuration(3_600_000);
194
- // "1h"
200
+ ## Line Number Formatting
195
201
 
196
- formatDuration(500);
197
- // "<1s"
198
- ```
202
+ Adds right-aligned line numbers to text.
199
203
 
200
- ## File Tree Rendering
204
+ ### formatWithLineNumbers
201
205
 
202
- Build and render hierarchical file trees with depth-based truncation, annotations, and collapsed directory summaries.
206
+ Adds line numbers with configurable starting value.
203
207
 
204
208
  ```typescript
205
- import { buildFileTree, FILE_TREE_DEFAULTS } from "@hardlydifficult/text";
209
+ import { formatWithLineNumbers } from "@hardlydifficult/text";
206
210
 
207
- buildFileTree(["src/index.ts", "src/utils.ts", "README.md"]);
208
- // "src/\n index.ts\n utils.ts\n\nREADME.md"
211
+ formatWithLineNumbers("foo\nbar\nbaz");
212
+ // "1: foo\n2: bar\n3: baz"
213
+
214
+ formatWithLineNumbers("hello\nworld", 10);
215
+ // "10: hello\n11: world"
209
216
  ```
210
217
 
211
- ### Options
218
+ ## File Tree Building
212
219
 
213
- | Parameter | Type | Description |
214
- |----------------|-------------------------------------------|-----------------------------------------------------------------------------|
215
- | `maxLevel2` | `number` | Maximum number of entries to show at level 2 (files in a directory) |
216
- | `maxLevel3` | `number` | Maximum number of entries to show at level 3 (files in subdirectories) |
217
- | `annotations` | `ReadonlyMap<string, string>` | Map of file/directory paths to annotation strings |
218
- | `details` | `ReadonlyMap<string, readonly string[]>` | Map of file paths to extra detail lines to show under entries |
219
- | `collapseDirs` | `readonly string[]` | Directory names to collapse with summary count |
220
- | `format` | `'plain' \| 'markdown'` | Output format. Defaults to `'markdown'`, which wraps the tree in a code fence for correct markdown rendering. Use `'plain'` when the caller already provides a fence (e.g. AI prompt templates). |
220
+ Builds a hierarchical file tree from flat paths with depth-based truncation.
221
221
 
222
- ### Examples
222
+ ### buildFileTree
223
223
 
224
- **Annotations**
224
+ Renders markdown-formatted (default) or plain file trees with annotations, details, and collapsed directory summaries.
225
225
 
226
226
  ```typescript
227
- const annotations = new Map([
228
- ["src/index.ts", "Main entry point"],
229
- ["src", "Source code directory"],
230
- ]);
227
+ import { buildFileTree, FILE_TREE_DEFAULTS } from "@hardlydifficult/text";
228
+ import type { BuildTreeOptions } from "@hardlydifficult/text";
231
229
 
232
- buildFileTree(["src/index.ts"], { annotations });
233
- // "```\nsrc/ — Source code directory\n index.ts — Main entry point\n```"
230
+ // Basic usage
231
+ buildFileTree(["src/index.ts", "README.md"]);
232
+ // "```\nsrc/\n index.ts\n\nREADME.md\n```"
233
+
234
+ // Options interface
235
+ interface BuildTreeOptions {
236
+ maxLevel2?: number; // Max children at depth 2 (default: 10)
237
+ maxLevel3?: number; // Max children at depth 3+ (default: 3)
238
+ annotations?: Map<string, string>;
239
+ details?: Map<string, readonly string[]>;
240
+ collapseDirs?: readonly string[];
241
+ lineCounts?: Map<string, number>;
242
+ format?: "plain" | "markdown"; // default: "markdown"
243
+ }
244
+
245
+ const paths = ["src/index.ts", "src/utils.ts"];
246
+ buildFileTree(paths, { format: "plain" });
247
+ // "src/\n index.ts\n utils.ts"
248
+
249
+ // With annotations
250
+ const annotations = new Map([["src/index.ts", "Main entry point"]]);
251
+ buildFileTree(paths, { annotations, format: "plain" });
252
+ // "src/\n index.ts — Main entry point\n utils.ts"
234
253
  ```
235
254
 
236
- **Details**
255
+ ### FILE_TREE_DEFAULTS
256
+
257
+ Default truncation limits for file tree rendering.
237
258
 
238
259
  ```typescript
239
- const details = new Map([
240
- ["src/index.ts", ["> main (5-20): App entry point."]],
241
- ]);
260
+ import { FILE_TREE_DEFAULTS } from "@hardlydifficult/text";
242
261
 
243
- buildFileTree(["src/index.ts"], { details });
244
- // "```\nsrc/\n index.ts\n > main (5-20): App entry point.\n```"
262
+ FILE_TREE_DEFAULTS; // { maxLevel2: 10, maxLevel3: 3 }
245
263
  ```
246
264
 
247
- **Collapsed directories**
265
+ ## Format Conversion
248
266
 
249
- ```typescript
250
- buildFileTree(
251
- ["src/index.ts", "test/unit/a.test.ts", "test/unit/b.test.ts"],
252
- { collapseDirs: ["test"] }
253
- );
254
- // "```\nsrc/\n index.ts\n\ntest/\n (2 files)\n```"
255
- ```
267
+ Bidirectional conversion between JSON and YAML.
256
268
 
257
- ## JSON/YAML Format Conversion
269
+ ### convertFormat
258
270
 
259
- Convert between JSON and YAML with automatic input detection and clean output formatting.
271
+ Auto-detects input format and converts to the requested format.
260
272
 
261
273
  ```typescript
262
- import { convertFormat } from "@hardlydifficult/text";
274
+ import { convertFormat, type TextFormat } from "@hardlydifficult/text";
263
275
 
264
- convertFormat('{"name":"Alice","age":30}', "yaml");
265
- // "name: Alice\nage: 30\n"
276
+ convertFormat('{"name": "Alice"}', "yaml");
277
+ // "name: Alice\n"
266
278
 
267
279
  convertFormat("name: Alice\nage: 30", "json");
268
280
  // "{\n \"name\": \"Alice\",\n \"age\": 30\n}"
269
281
  ```
270
282
 
271
- ### `TextFormat`
283
+ ## YAML Utilities
272
284
 
273
- Type alias for output format: `"json"` or `"yaml"`.
285
+ Serialization and repair utilities for YAML.
274
286
 
275
- ## YAML Formatting
287
+ ### formatYaml
276
288
 
277
- Serialize data to clean YAML with intelligent block literal selection for long strings.
289
+ Serializes data to clean YAML with block literals for long strings containing `": "`.
278
290
 
279
291
  ```typescript
280
292
  import { formatYaml } from "@hardlydifficult/text";
281
293
 
282
- formatYaml({
283
- purpose:
284
- "Core AI SDK implementation: LLM integrations (Anthropic Claude, Ollama), agent orchestration with streaming.",
285
- });
286
-
287
- // Uses block literal (|) for long strings containing ": "
288
- // purpose: |
289
- // Core AI SDK implementation: LLM integrations (Anthropic Claude, Ollama), agent orchestration with streaming.
294
+ formatYaml({ purpose: "Core AI SDK: LLM integrations." });
295
+ // "purpose: |\n Core AI SDK: LLM integrations.\n"
290
296
  ```
291
297
 
292
- ## YAML Healing
298
+ ### healYaml
293
299
 
294
- Clean and repair YAML output from LLMs by stripping code fences and quoting problematic scalar values.
300
+ Strips markdown fences and quotes scalar values containing colons.
295
301
 
296
302
  ```typescript
297
303
  import { healYaml } from "@hardlydifficult/text";
@@ -299,128 +305,100 @@ import { healYaml } from "@hardlydifficult/text";
299
305
  healYaml("```yaml\nkey: value\n```");
300
306
  // "key: value"
301
307
 
302
- healYaml('description: Development dependencies: Node types.');
303
- // 'description: "Development dependencies: Node types."'
308
+ healYaml("description: Text: with colons");
309
+ // 'description: "Text: with colons"'
304
310
  ```
305
311
 
306
- ## Link Generation
312
+ ## Linker (Text Linkification)
307
313
 
308
- Transform text with issue/PR references into formatted links across platforms like Slack, Discord, and Markdown.
314
+ Transforms text by replacing issue/PR references with formatted links.
309
315
 
310
- ### `createLinker`
316
+ ### Linker
311
317
 
312
- Create a linker instance with optional initial rules.
318
+ Stateful linker with configurable rules, idempotent linkification, and multi-platform support.
313
319
 
314
320
  ```typescript
315
- import { createLinker } from "@hardlydifficult/text";
316
-
317
- const linker = createLinker()
318
- .linear("my-org")
319
- .githubPr("my-org/my-repo");
320
-
321
- linker.apply("Fix ENG-533 and PR#42", { platform: "slack" });
322
- // "Fix <https://linear.app/my-org/issue/ENG-533|ENG-533> <https://github.com/my-org/my-repo/pull/42|PR#42>"
321
+ import { createLinker, LinkerPlatform, LinkerApplyOptions } from "@hardlydifficult/text";
322
+
323
+ interface LinkerApplyOptions {
324
+ format?: LinkerPlatform; // "slack" | "discord" | "markdown" | "plaintext"
325
+ platform?: LinkerPlatform;
326
+ skipCode?: boolean; // default: true
327
+ skipExistingLinks?: boolean; // default: true
328
+ linkifyPlainHref?: boolean; // default: false
329
+ }
323
330
  ```
324
331
 
325
- ### `Linker` Class
326
-
327
- Stateful linker that applies configured rules to text.
328
-
329
- **Methods:**
330
-
331
- | Method | Description |
332
- |----------------|------------------------------------------------------------------------|
333
- | `addRule(rule)`| Add a custom link rule |
334
- | `rule(...)` | Add a rule (supports fluent and named forms) |
335
- | `custom(...)` | Add a custom rule with regex pattern and href builder |
336
- | `linear(...)` | Add Linear issue reference rule (e.g., `ENG-533`) |
337
- | `githubPr(...)`| Add GitHub PR reference rule (e.g., `PR#42`) |
338
- | `apply(...)` | Apply linkification to text with options |
339
- | `linkText(...)`| Alias for `apply` (same behavior) |
332
+ ### createLinker
340
333
 
341
- ### Rules
334
+ Creates a linker with optional initial rules.
342
335
 
343
- | Parameter | Type | Description |
344
- |-----------|---------------------------|-----------------------------------------------------------------------------|
345
- | `pattern` | `RegExp` | Match pattern (global flag is enforced automatically) |
346
- | `href` | `string` | URL template (supports `$0`/`$&`, `$1..$N`) |
347
- | `toHref` | `string \| LinkHrefBuilder`| Either href template or callback; takes precedence over `href` |
348
- | `priority`| `number` | Higher priority wins for overlapping matches (default: `0`) |
349
-
350
- ### Options
336
+ ```typescript
337
+ import { createLinker } from "@hardlydifficult/text";
351
338
 
352
- | Parameter | Type | Description |
353
- |-----------------------|-----------------------------|-------------------------------------------------------------------------|
354
- | `format` / `platform` | `LinkerPlatform` | Output format: `"slack"`, `"discord"`, `"markdown"`, `"plaintext"` |
355
- | `skipCode` | `boolean` | Skip linkification inside code spans (default: `true`) |
356
- | `skipExistingLinks` | `boolean` | Skip linkification inside existing links (default: `true`) |
339
+ const linker = createLinker();
357
340
 
358
- ### Platforms
341
+ // Fluent API
342
+ linker
343
+ .linear("fairmint")
344
+ .githubPr("Fairmint/api")
345
+ .custom(/\bINC-\d+\b/g, ({ match }) => `https://incident.io/${match}`);
359
346
 
360
- | Platform | Format |
361
- |--------------|---------------------------------|
362
- | `slack` | `<href|text>` |
363
- | `discord` | `[text](href)` |
364
- | `markdown` | `[text](href)` |
365
- | `plaintext` | `href` (raw URL) |
347
+ // Linkify text
348
+ linker.linkText("Fix ENG-533 PR#42 INC-99", { format: "slack" });
349
+ // "Fix <https://linear.app/fairmint/issue/ENG-533|ENG-533> <https://github.com/Fairmint/api/pull/42|PR#42> <https://incident.io/INC-99|INC-99>"
350
+ ```
366
351
 
367
- ### Examples
352
+ ### Rule API
368
353
 
369
- **Custom rules**
354
+ Custom rules support patterns, href templates or callbacks, priorities, and match metadata.
370
355
 
371
356
  ```typescript
372
- const linker = createLinker().custom(
373
- /\bINC-\d+\b/g,
374
- ({ match }) => `https://incident.io/${match}`
375
- );
376
- linker.apply("Handle INC-99", { format: "slack" });
377
- // "Handle <https://incident.io/INC-99|INC-99>"
357
+ interface LinkRule {
358
+ id: string;
359
+ priority: number;
360
+ pattern: RegExp;
361
+ href: string | ((ctx: { match: string; groups?: string[] }) => string);
362
+ skipCode?: boolean;
363
+ skipExistingLinks?: boolean;
364
+ }
378
365
  ```
379
366
 
380
- **Priority-based resolution**
367
+ ### Methods
381
368
 
382
- ```typescript
383
- const linker = createLinker()
384
- .custom(/\bENG-\d+\b/g, "https://low.example/$0", { priority: 0 })
385
- .custom(/\bENG-533\b/g, "https://high.example/$0", { priority: 10 });
369
+ - `custom(pattern, href, options)`: Add custom rule
370
+ - `linear(orgOrProject)`: Link Linear issues
371
+ - `githubPr(repo)`: Link GitHub PRs
372
+ - `apply(text, options)`: Linkify and preserve format
373
+ - `linkText(text, options)`: Linkify plain text
374
+ - `linkMarkdown(text, options)`: Linkify markdown content
375
+ - `reset()`: Clear rules
386
376
 
387
- linker.apply("ENG-533 and ENG-534", { format: "markdown" });
388
- // "[ENG-533](https://high.example/ENG-533) and [ENG-534](https://low.example/ENG-534)"
389
- ```
390
-
391
- **Idempotent linkification**
377
+ ## Markdown Utilities
392
378
 
393
- ```typescript
394
- const linker = createLinker().linear("my-org");
395
- const first = linker.apply("Ship ENG-533", { format: "slack" });
396
- const second = linker.apply(first, { format: "slack" });
397
- // first === second (no double-linkification)
398
- ```
379
+ Tools for working with markdown fences and formatting.
399
380
 
400
- ## Text with Line Numbers
381
+ ### escapeFence
401
382
 
402
- Format text content with right-aligned line numbers.
383
+ Selects the minimal fence length to safely escape content.
403
384
 
404
385
  ```typescript
405
- import { formatWithLineNumbers } from "@hardlydifficult/text";
406
-
407
- formatWithLineNumbers("foo\nbar\nbaz");
408
- // 1: foo
409
- // 2: bar
410
- // 3: baz
386
+ import { escapeFence } from "@hardlydifficult/text";
411
387
 
412
- formatWithLineNumbers("hello\nworld", 10);
413
- // 10: hello
414
- // 11: world
388
+ escapeFence("hello"); // { fence: "```", content: "hello" }
389
+ escapeFence("code ``` here"); // { fence: "````", content: "code ``` here" }
415
390
  ```
416
391
 
417
- ## Escaping Markdown Fences
392
+ ### stripAnsi
418
393
 
419
- Escape markdown code fences by dynamically selecting a fence delimiter longer than any backtick sequence in the content.
394
+ Removes ANSI escape codes from strings.
420
395
 
421
396
  ```typescript
422
- import { escapeFence } from "@hardlydifficult/text";
397
+ import { stripAnsi } from "@hardlydifficult/text";
398
+
399
+ stripAnsi("\x1b[31mRed\x1b[0m"); // "Red"
400
+ ```
401
+
402
+ ## License
423
403
 
424
- escapeFence("Content with `` backticks");
425
- // { fence: "````", content: "Content with `` backticks" }
426
- ```
404
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/text",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -23,7 +23,7 @@
23
23
  "vitest": "4.0.18"
24
24
  },
25
25
  "engines": {
26
- "node": ">=18.0.0"
26
+ "node": ">=20.19.0"
27
27
  },
28
28
  "type": "commonjs",
29
29
  "exports": {