Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

[![npm](https://img.shields.io/npm/v/@tonybonet/truncate?label=npm&color=111318)](https://www.npmjs.com/package/@tonybonet/truncate)
[![downloads](https://img.shields.io/npm/dm/@tonybonet/truncate?label=downloads&color=111318)](https://www.npmjs.com/package/@tonybonet/truncate)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@tonybonet/truncate?color=111318)](https://bundlephobia.com/package/@tonybonet/truncate)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@tonybonet/truncate@0.4.2?color=111318)](https://bundlephobia.com/package/@tonybonet/truncate@0.4.2)
[![license](https://img.shields.io/npm/l/@tonybonet/truncate?color=111318)](LICENSE)

DOM-free core, grapheme-safe text truncation for JavaScript and TypeScript, powered by [`@chenglou/pretext`](https://github.com/chenglou/pretext). Fit copy by pixel width, line count, target string, explicit range, or measured height without layout reads; opt into element binding only when component code already owns a DOM-compatible node.
Expand Down Expand Up @@ -213,15 +213,15 @@ truncateByWidth(articleIntro, { font: "16px Inter", maxWidth: 260, ellipsis: " R

### Bind to a DOM-Compatible Element

When you already have a real DOM element, custom element, or DOM-compatible object, bind once. After that, calls do not take an element reference, `font`, or text. Width and other options can still override inferred defaults after binding, but the element must provide an initial width source.
When you already have a real DOM element, custom element, or DOM-compatible object, bind once. After that, calls do not take an element reference, `font`, or text. The library reads the element's computed typography internally and uses the element's rendered width as `maxWidth` by default. Pass `maxWidth` only when you want an operation-specific override; that override can be a number or CSS width such as `18rem`, `32em`, `40ch`, `50vw`, or `50vh`.

```ts
const title = document.querySelector("[data-truncate]")!;
const titleTruncator = createTruncator(title);

titleTruncator.truncate();
titleTruncator.truncate({ maxLines: 2 });
titleTruncator.truncateMiddle({ ellipsis: "....." });
titleTruncator.truncateMiddle({ maxWidth: "40ch", ellipsis: "....." });
```

The bound truncator avoids compounded truncation: if it wrote a previous result, the next call still uses the last source text unless external code changes `textContent`.
Expand All @@ -245,7 +245,7 @@ const label = {
createTruncator(label).truncate();
```

See [`docs/element-bound.md`](docs/element-bound.md) for the full adapter contract, width requirement, style inference rules, and no-DOM TypeScript notes.
See [`docs/element-bound.md`](docs/element-bound.md) for the full adapter contract, width requirement, `maxWidth` override rules, style inference rules, and no-DOM TypeScript notes.

## API

Expand Down Expand Up @@ -371,15 +371,15 @@ t.measureHeight("Hello\nworld", { maxWidth: 320 });

### `createTruncator(element)`

Component convenience for DOM or DOM-compatible code. It accepts any `DOMCompatibleElement`: normal DOM nodes, custom elements, or objects that expose `nodeType`, `textContent`, a width source, and optional `computedStyle`. It reads style and width at creation time, reads current `textContent` per call, and writes the truncated text back without compounding prior write-back.
Component convenience for DOM or DOM-compatible code. It accepts any `DOMCompatibleElement`: normal DOM nodes, custom elements, or objects that expose `nodeType`, `textContent`, a width source, and optional `computedStyle`. It reads typography and width at creation time, reads current `textContent` per call, and writes the truncated text back without compounding prior write-back. You do not pass `font` for normal element-bound calls; only pass `maxWidth` when overriding the element width for a specific operation.

```ts
const element = document.querySelector("[data-truncate]")!;
const t = createTruncator(element);

t.truncate();
t.truncate({ maxLines: 2 });
t.truncateStart({ ellipsis: " READ MORE" });
t.truncateStart({ maxWidth: "32ch", ellipsis: " READ MORE" });
```

For custom adapters, provide the DOM-compatible surface directly:
Expand Down Expand Up @@ -422,7 +422,7 @@ truncateByWidth("Hello", {
| --------------- | ------------------------ | ------------------------ | ----------------------------- |
| `font` | `string` | auto-detect in browser | measurement APIs |
| `selector` | `string` | - | font lookup |
| `maxWidth` | `CssWidth` | required | truncation and measurement |
| `maxWidth` | `CssWidth` | required unless bound | truncation and measurement |
| `ellipsis` | `string` | `…` | custom marker or suffix |
| `maxLines` | `number` | `1` | `truncate`, `truncateByLines` |
| `keepLines` | `number[]` | - | `truncate`, `truncateByLines` |
Expand Down
21 changes: 17 additions & 4 deletions docs/element-bound.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

`createTruncator(element)` is the explicit DOM boundary for this package. The core API remains text-first and DOM-free; element binding is for component code that already owns a node or a DOM-compatible adapter.

Use it when you want to bind once, infer defaults from the element, and then call truncation methods without passing `(element)` or `(text)` every time.
Use it when you want to bind once, infer defaults from the element, and then call truncation methods without passing `(element)`, `(text)`, or `font` every time. The bound API reads typography internally; callers usually only pass content options such as `maxLines`, `ellipsis`, or an operation-specific `maxWidth`.

```ts
import { createTruncator, type DOMCompatibleElement } from "@tonybonet/truncate";
Expand All @@ -12,7 +12,7 @@ const t = createTruncator(title);

t.truncate();
t.truncate({ maxLines: 2 });
t.truncateMiddle({ ellipsis: "....." });
t.truncateMiddle({ maxWidth: "40ch", ellipsis: "....." });
```

## Contract
Expand All @@ -24,6 +24,7 @@ t.truncateMiddle({ ellipsis: "....." });
- The result is written back to `element.textContent`.
- Repeated calls do not compound prior write-back; the bound truncator keeps the last external source text unless outside code changes `textContent`.
- If styles change, create a new bound truncator.
- Per-call `maxWidth` overrides are optional and can use supported CSS width units.

## What Counts as DOM-Compatible

Expand Down Expand Up @@ -82,7 +83,9 @@ t.truncate();
// label.textContent now contains the truncated result
```

## Width Requirement
## Width and `maxWidth`

For normal element-bound usage, you do not calculate or pass `maxWidth`. The library reads the element's own rendered width and uses that as the default `maxWidth`.

An element must provide width at bind time:

Expand All @@ -102,7 +105,17 @@ const missingWidth = {
createTruncator(missingWidth as unknown as DOMCompatibleElement);
```

Per-call `maxWidth` can override the cached width for an operation, but it does not replace the bind-time width requirement. The bound API needs a safe default before it can create the truncator.
Per-call `maxWidth` overrides the cached element width for one operation. It can be a number or a supported CSS width string, including `px`, `rem`, `em`, `ch`, `vw`, and `vh`. `em` and `ch` are resolved against the inferred element font, so you do not need to pass `font` just to use those units.

```ts
const t = createTruncator(document.querySelector("[data-truncate]")!);

t.truncate(); // uses the element width
t.truncate({ maxWidth: "32ch" }); // overrides width, keeps inferred font
t.truncateByLines({ maxWidth: "24em", maxLines: 2 });
```

A per-call override does not replace the bind-time width requirement. The bound API still needs a safe default width before it can create the truncator.

## Style Inference

Expand Down
8 changes: 8 additions & 0 deletions tests/element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,14 @@ test("explicit maxWidth overrides inferred width", () => {
expect(r.truncated).toBe(true);
});

test("explicit CSS maxWidth override uses inferred element font", () => {
const el = mockElement(LONG, { fontSize: "20px", fontFamily: "serif" }, 500);
const t = createTruncator(el);

expect(t.truncateByWidth({ maxWidth: "4em" }).truncated).toBe(true);
expect(t.truncateByWidth({ maxWidth: "4ch" }).truncated).toBe(true);
});

test("explicit lineHeight overrides inferred lineHeight", () => {
const el = mockElement(PARA, { lineHeight: "20px" }, 80);
const t = createTruncator(el);
Expand Down
9 changes: 5 additions & 4 deletions website/src/components/apiDocsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export const functionDocs: ApiFunctionDoc[] = [
title: "createTruncator",
badge: "factory",
description:
"Pre-binds shared text options, or binds to a DOM-compatible element/adapter for component code.",
"Pre-binds shared text options, or binds to a DOM-compatible element/adapter and infers its typography and width.",
signature: `createTruncator(config: Partial<TruncateOptions>): Truncator
createTruncator(element: DOMCompatibleElement): BoundTruncator`,
returns: "Truncator | BoundTruncator",
Expand All @@ -249,7 +249,8 @@ textTruncator.truncateByLines(longArticle, { maxWidth: 320, maxLines: 3 })

const element = document.querySelector("[data-truncate]")!
const elementTruncator = createTruncator(element)
elementTruncator.truncate({ maxLines: 2 })`,
elementTruncator.truncate({ maxLines: 2 })
elementTruncator.truncateMiddle({ maxWidth: "40ch" })`,
},
{
id: "fn-detectFont",
Expand Down Expand Up @@ -300,7 +301,7 @@ export const typeDocs: ApiTypeDoc[] = [
title: "BoundTruncator",
signature: `interface BoundTruncator {\n truncate(opts?: Partial<TruncateOptions>): TruncateResult\n truncateByWidth(opts?: Partial<TruncateOptions>): TruncateResult\n truncateByLines(opts?: Partial<TruncateOptions>): TruncateResult\n truncateStart(opts?: Partial<TruncateOptions>): TruncateResult\n truncateMiddle(opts?: Partial<TruncateOptions>): TruncateResult\n truncateAtOffset(opts?: Partial<TruncateOptions & { offset?: number }>): TruncateResult\n truncateRange(opts?: Partial<TruncateOptions & { start?: number; end?: number; context?: number; before?: number; after?: number }>): TruncateResult\n truncateAround(opts?: Partial<TruncateOptions & { target?: string; context?: number; before?: number; after?: number }>): TruncateResult\n measureHeight(opts?: Partial<MeasureOptions>): number\n}`,
description:
"Element-bound truncator returned by `createTruncator(element)`. Accepts DOM nodes, custom elements, or DOM-compatible objects; reads current textContent per call, writes back the result, and avoids compounding prior write-back. Recreate it when styles change.",
"Element-bound truncator returned by `createTruncator(element)`. Accepts DOM nodes, custom elements, or DOM-compatible objects; infers font/width at bind time, reads current textContent per call, writes back the result, and avoids compounding prior write-back. Pass maxWidth only to override the inferred width for one operation.",
},
{
id: "type-DOMCompatibleElement",
Expand Down Expand Up @@ -341,7 +342,7 @@ type DOMCompatibleElement = DOMNativeElementLike | DOMCompatibleAdapter`,
export const optionRows = [
["font", "string", "auto-detect", "All measurement APIs"],
["selector", "string", "—", "Font lookup"],
["maxWidth", "CssWidth", "required", "All truncation and measurement APIs"],
["maxWidth", "CssWidth", "required unless bound", "All truncation and measurement APIs"],
["lineHeight", "number", "20 for line truncation", "Lines and height"],
["maxLines", "number", "1", "truncateByLines"],
["keepLines", "number[]", "—", "truncateByLines"],
Expand Down
Loading