@lp-labs/text-wrap 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.js +60 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +57 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Libin Prasanth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lp-labs/text-wrap
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Greedy paragraph-preserving word wrapping with supplied width measurement and explicit long-word policies.
|
|
4
|
+
|
|
5
|
+
Standalone original implementation; zero runtime, peer, or sibling-package dependencies. Node.js 18+, ESM and CommonJS with separate declaration trees. No browser globals are accessed at import time; compiled runtime uses only standard JavaScript APIs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @lp-labs/text-wrap
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { wrapText } from '@lp-labs/text-wrap';
|
|
17
|
+
console.log(wrapText('one two three', { width: 7 })); // ['one two', 'three']
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
CommonJS exposes the same named exports:
|
|
21
|
+
|
|
22
|
+
```cjs
|
|
23
|
+
const api = require('@lp-labs/text-wrap');
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Customization
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { wrapText } from '@lp-labs/text-wrap';
|
|
30
|
+
const measure = (text) => [...text].reduce((n, c) => n + (c === '界' ? 2 : 1), 0);
|
|
31
|
+
console.log(wrapText('界 a b', { width: 4, measure, longWord: 'error' }));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Semantics, limits and errors
|
|
35
|
+
|
|
36
|
+
`wrapText(text, options)` returns a fresh string array, without trailing newline characters. CRLF, LF and CR preserve paragraph boundaries, including empty paragraphs. Other whitespace between words is normalized to one ASCII space; leading/trailing whitespace is discarded. Empty text yields one empty line.
|
|
37
|
+
|
|
38
|
+
`width` is a positive safe integer. Default `measure` counts Unicode code points, **not wcwidth, display cells, graphemes, or terminal escape sequences**. It is called on complete candidate strings, including spaces, and must be deterministic and return a finite nonnegative number, with zero for empty text. Non-additive measurements are accepted, but greedy results need not be optimal.
|
|
39
|
+
|
|
40
|
+
`longWord` defaults to `split`: split at code-point boundaries; an individual point wider than the target throws. `overflow` retains an overlong word on its own line. `error` rejects it. A combining sequence may be divided. Default `maxInput` is 100,000 UTF-16 units and `maxLines` is 10,000 (both positive integers). Unsupported C0 controls and DEL throw TypeError; horizontal tabs/newlines are supported. Invalid widths, measurements and exhausted line budgets throw RangeError; callbacks propagate their own errors.
|
|
41
|
+
|
|
42
|
+
Work is greedy, with up to O(n²) total candidate text/measurement work for a long line or word; callback cost is additional. Output space is O(n + lines). No callback or input options are retained.
|
|
43
|
+
|
|
44
|
+
## Complete public declaration API
|
|
45
|
+
|
|
46
|
+
The following is the complete public ESM declaration surface; CommonJS has matching declarations and runtime exports. Optional properties are omitted rather than supplied as undefined under exactOptionalPropertyTypes.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export interface WrapOptions {
|
|
50
|
+
width: number;
|
|
51
|
+
measure?: (text: string) => number;
|
|
52
|
+
longWord?: 'split' | 'overflow' | 'error';
|
|
53
|
+
maxInput?: number;
|
|
54
|
+
maxLines?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Wrap whitespace-separated words; explicit newlines are retained. */
|
|
57
|
+
export declare function wrapText(text: string, options: WrapOptions): string[];
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Development and verification
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm ci --offline --ignore-scripts --no-audit --no-fund
|
|
64
|
+
npm run build
|
|
65
|
+
npm run typecheck
|
|
66
|
+
npm test
|
|
67
|
+
npm run format:check
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The offline command requires the dev tools already present in npm's cache. TypeScript ^5.9.3 and Prettier ^3.6.2 are dev-only. Build targets ES2022 with strict NodeNext declarations, noUncheckedIndexedAccess, exactOptionalPropertyTypes, noUnused checks, ES2022/DOM libs and no ambient type packages. Runtime tests import the ESM distribution directly and check CommonJS parity. Bare-package .mts/.cts consumer tests include negative type assertions. Published files are only dist, this README, LICENSE and package.json. No publication is performed by tests.
|
|
71
|
+
|
|
72
|
+
## Support
|
|
73
|
+
|
|
74
|
+
If this library is useful to you, consider supporting its maintenance with a [donation via PayPal](https://www.paypal.me/LibinPrasanth).
|
|
75
|
+
|
|
76
|
+
## Author
|
|
77
|
+
|
|
78
|
+
Created by [Libin Prasanth](https://www.linkedin.com/in/libinprasanth/). Connect on LinkedIn for updates and discussion.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface WrapOptions {
|
|
2
|
+
width: number;
|
|
3
|
+
measure?: (text: string) => number;
|
|
4
|
+
longWord?: 'split' | 'overflow' | 'error';
|
|
5
|
+
maxInput?: number;
|
|
6
|
+
maxLines?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Wrap whitespace-separated words; explicit newlines are retained. */
|
|
9
|
+
export declare function wrapText(text: string, options: WrapOptions): string[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wrapText = wrapText;
|
|
4
|
+
/** Wrap whitespace-separated words; explicit newlines are retained. */
|
|
5
|
+
function wrapText(text, options) {
|
|
6
|
+
const { width, measure = (value) => [...value].length, longWord = 'split', maxInput = 100_000, maxLines = 10_000, } = options;
|
|
7
|
+
for (const n of [width, maxInput, maxLines])
|
|
8
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
9
|
+
throw new RangeError('Limits must be positive integers.');
|
|
10
|
+
if (!['split', 'overflow', 'error'].includes(longWord))
|
|
11
|
+
throw new TypeError('Unknown longWord policy.');
|
|
12
|
+
if (typeof text !== 'string' ||
|
|
13
|
+
text.length > maxInput ||
|
|
14
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(text))
|
|
15
|
+
throw new TypeError('Invalid or oversized text.');
|
|
16
|
+
const size = (value) => {
|
|
17
|
+
const n = measure(value);
|
|
18
|
+
if (!Number.isFinite(n) || n < 0)
|
|
19
|
+
throw new RangeError('measure must return a finite nonnegative width.');
|
|
20
|
+
return n;
|
|
21
|
+
};
|
|
22
|
+
if (size('') !== 0)
|
|
23
|
+
throw new RangeError('Empty text must measure zero.');
|
|
24
|
+
const result = [];
|
|
25
|
+
const emit = (line) => {
|
|
26
|
+
if (result.length >= maxLines)
|
|
27
|
+
throw new RangeError('Line budget exceeded.');
|
|
28
|
+
result.push(line);
|
|
29
|
+
};
|
|
30
|
+
for (const paragraph of text.split(/\r\n|\r|\n/u)) {
|
|
31
|
+
let line = '';
|
|
32
|
+
for (const word of paragraph.split(/[^\S\r\n]+/u).filter(Boolean)) {
|
|
33
|
+
if (line && size(`${line} ${word}`) <= width) {
|
|
34
|
+
line += ` ${word}`;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (line) {
|
|
38
|
+
emit(line);
|
|
39
|
+
line = '';
|
|
40
|
+
}
|
|
41
|
+
if (size(word) <= width || longWord === 'overflow') {
|
|
42
|
+
line = word;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (longWord === 'error')
|
|
46
|
+
throw new RangeError('Word exceeds width.');
|
|
47
|
+
for (const point of word) {
|
|
48
|
+
if (size(point) > width)
|
|
49
|
+
throw new RangeError('One code point exceeds width.');
|
|
50
|
+
if (line && size(line + point) > width) {
|
|
51
|
+
emit(line);
|
|
52
|
+
line = '';
|
|
53
|
+
}
|
|
54
|
+
line += point;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
emit(line);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface WrapOptions {
|
|
2
|
+
width: number;
|
|
3
|
+
measure?: (text: string) => number;
|
|
4
|
+
longWord?: 'split' | 'overflow' | 'error';
|
|
5
|
+
maxInput?: number;
|
|
6
|
+
maxLines?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Wrap whitespace-separated words; explicit newlines are retained. */
|
|
9
|
+
export declare function wrapText(text: string, options: WrapOptions): string[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Wrap whitespace-separated words; explicit newlines are retained. */
|
|
2
|
+
export function wrapText(text, options) {
|
|
3
|
+
const { width, measure = (value) => [...value].length, longWord = 'split', maxInput = 100_000, maxLines = 10_000, } = options;
|
|
4
|
+
for (const n of [width, maxInput, maxLines])
|
|
5
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
6
|
+
throw new RangeError('Limits must be positive integers.');
|
|
7
|
+
if (!['split', 'overflow', 'error'].includes(longWord))
|
|
8
|
+
throw new TypeError('Unknown longWord policy.');
|
|
9
|
+
if (typeof text !== 'string' ||
|
|
10
|
+
text.length > maxInput ||
|
|
11
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(text))
|
|
12
|
+
throw new TypeError('Invalid or oversized text.');
|
|
13
|
+
const size = (value) => {
|
|
14
|
+
const n = measure(value);
|
|
15
|
+
if (!Number.isFinite(n) || n < 0)
|
|
16
|
+
throw new RangeError('measure must return a finite nonnegative width.');
|
|
17
|
+
return n;
|
|
18
|
+
};
|
|
19
|
+
if (size('') !== 0)
|
|
20
|
+
throw new RangeError('Empty text must measure zero.');
|
|
21
|
+
const result = [];
|
|
22
|
+
const emit = (line) => {
|
|
23
|
+
if (result.length >= maxLines)
|
|
24
|
+
throw new RangeError('Line budget exceeded.');
|
|
25
|
+
result.push(line);
|
|
26
|
+
};
|
|
27
|
+
for (const paragraph of text.split(/\r\n|\r|\n/u)) {
|
|
28
|
+
let line = '';
|
|
29
|
+
for (const word of paragraph.split(/[^\S\r\n]+/u).filter(Boolean)) {
|
|
30
|
+
if (line && size(`${line} ${word}`) <= width) {
|
|
31
|
+
line += ` ${word}`;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (line) {
|
|
35
|
+
emit(line);
|
|
36
|
+
line = '';
|
|
37
|
+
}
|
|
38
|
+
if (size(word) <= width || longWord === 'overflow') {
|
|
39
|
+
line = word;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (longWord === 'error')
|
|
43
|
+
throw new RangeError('Word exceeds width.');
|
|
44
|
+
for (const point of word) {
|
|
45
|
+
if (size(point) > width)
|
|
46
|
+
throw new RangeError('One code point exceeds width.');
|
|
47
|
+
if (line && size(line + point) > width) {
|
|
48
|
+
emit(line);
|
|
49
|
+
line = '';
|
|
50
|
+
}
|
|
51
|
+
line += point;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
emit(line);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lp-labs/text-wrap",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Greedy paragraph-preserving word wrapping with supplied width measurement and explicit long-word policies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"module": "./dist/esm/index.js",
|
|
9
|
+
"types": "./dist/esm/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./dist/esm/index.d.ts",
|
|
14
|
+
"default": "./dist/esm/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./dist/cjs/index.d.ts",
|
|
18
|
+
"default": "./dist/cjs/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/build.mjs",
|
|
34
|
+
"typecheck": "npm run build && node scripts/check-types.mjs",
|
|
35
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
36
|
+
"format": "prettier --write src scripts tests *.json README.md",
|
|
37
|
+
"format:check": "prettier --check src scripts tests *.json README.md",
|
|
38
|
+
"prepack": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"prettier": "^3.6.2"
|
|
43
|
+
},
|
|
44
|
+
"prettier": {
|
|
45
|
+
"singleQuote": true,
|
|
46
|
+
"semi": true,
|
|
47
|
+
"trailingComma": "all",
|
|
48
|
+
"printWidth": 100,
|
|
49
|
+
"endOfLine": "lf"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"author": {
|
|
56
|
+
"name": "Libin Prasanth",
|
|
57
|
+
"url": "https://www.linkedin.com/in/libinprasanth/"
|
|
58
|
+
},
|
|
59
|
+
"funding": {
|
|
60
|
+
"type": "paypal",
|
|
61
|
+
"url": "https://www.paypal.me/LibinPrasanth"
|
|
62
|
+
}
|
|
63
|
+
}
|