@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 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
- # Temporary Holding Version
1
+ # @lp-labs/text-wrap
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }