diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d30ab..1e446b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `text-clipper` has become a Deno-first library and is now available on [Jsr.io](https://jsr.io). Instructions for installation on Node.js/Bun are still included. +- Fix #18: Don't include spaces before the indicator. ## 2.2.0 diff --git a/src/index.ts b/src/index.ts index 2ad37b7..8847436 100644 --- a/src/index.ts +++ b/src/index.ts @@ -518,16 +518,22 @@ function clipHtml(string: string, maxLength: number, options: ClipHtmlOptions): // of words, but given this seems highly unlikely and the alternative is // doing another full parsing of the preceding text, this seems acceptable. break; - } else if (charCode === NEWLINE_CHAR_CODE || charCode === TAG_OPEN_CHAR_CODE) { + } else if ( + charCode === NEWLINE_CHAR_CODE || + charCode === TAG_OPEN_CHAR_CODE || + isWhiteSpace(charCode) + ) { i = j; break; - } else if (isWhiteSpace(charCode)) { - i = j + (indicator ? 1 : 0); - break; } } } + // Don't leave awkward whitespace before the ellipsis. + while (i > 0 && isWhiteSpace(string.charCodeAt(i - 1)!)) { + i--; + } + let result = string.slice(0, i); if (!isLineBreak(string, i)) { result += indicator; @@ -596,6 +602,11 @@ function clipPlainText(string: string, maxLength: number, options: CommonClipOpt } } + // Don't leave awkward whitespace before the ellipsis. + while (i > 0 && isWhiteSpace(string.charCodeAt(i - 1)!)) { + i--; + } + return string.slice(0, i) + (nextChar === "\n" ? "" : indicator); } else if (numLines > maxLines) { return string.slice(0, i); diff --git a/tests/unit/examples.test.ts b/tests/unit/examples.test.ts index 9c82a6a..c66c1e1 100644 --- a/tests/unit/examples.test.ts +++ b/tests/unit/examples.test.ts @@ -5,6 +5,6 @@ import clip from "../../src/index.ts"; Deno.test("examples: test examples from the README", () => { assertEquals(clip("foo", 3), "foo"); assertEquals(clip("foo", 2), "f…"); - assertEquals(clip("foo bar", 5), "foo …"); + assertEquals(clip("foo bar", 5), "foo…"); assertEquals(clip("foo\nbar", 5), "foo"); }); diff --git a/tests/unit/html.test.ts b/tests/unit/html.test.ts index 2c7480f..482aa31 100644 --- a/tests/unit/html.test.ts +++ b/tests/unit/html.test.ts @@ -8,10 +8,7 @@ Deno.test("html: test basic HTML", () => { assertEquals(clip("

Lorum ipsum

", 5, options), "

Loru\u2026

"); assertEquals(clip("

Lorum ipsum

", 5, options), "

Loru\u2026

"); assertEquals(clip("

Lorum ipsum

", 6, options), "

Lorum\u2026

"); - assertEquals( - clip("

Lorum ipsum

", 7, options), - "

Lorum \u2026

", - ); + assertEquals(clip("

Lorum ipsum

", 7, options), "

Lorum\u2026

"); assertEquals(clip("

Lorum\nipsum

", 5, options), "

Lorum

"); assertEquals(clip("

Lorum
ipsum

", 5, options), "

Lorum

"); @@ -44,7 +41,7 @@ Deno.test("html: test basic HTML", () => { assertEquals( clip('Just a link', 8, options), - 'Just a \u2026', + 'Just a\u2026', ); assertEquals( @@ -150,10 +147,10 @@ Deno.test("html: test plain text", () => { assertEquals(clip("Lorum ipsum", 5, options), "Loru\u2026"); assertEquals(clip("Lorum ipsum", 6, options), "Lorum\u2026"); - assertEquals(clip("Lorum ipsum", 7, options), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 8, options), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 9, options), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 10, options), "Lorum \u2026"); + assertEquals(clip("Lorum ipsum", 7, options), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 8, options), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 9, options), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 10, options), "Lorum\u2026"); assertEquals(clip("Lorum ipsum", 11, options), "Lorum ipsum"); assertEquals(clip("Lorum\nipsum", 10, options), "Lorum"); @@ -167,7 +164,7 @@ Deno.test("html: test word breaking", () => { assertEquals(clip("Lorum ipsum", 5, options), "Loru\u2026"); assertEquals(clip("Lorum ipsum", 6, options), "Lorum\u2026"); - assertEquals(clip("Lorum ipsum", 7, options), "Lorum \u2026"); + assertEquals(clip("Lorum ipsum", 7, options), "Lorum\u2026"); assertEquals(clip("Lorum ipsum", 8, options), "Lorum i\u2026"); assertEquals(clip("Lorum ipsum", 9, options), "Lorum ip\u2026"); assertEquals(clip("Lorum ipsum", 10, options), "Lorum ips\u2026"); @@ -178,7 +175,7 @@ Deno.test("html: test word breaking without indicator", () => { const options = { breakWords: true, html: true, indicator: "" }; assertEquals(clip("Lorum ipsum", 5, options), "Lorum"); - assertEquals(clip("Lorum ipsum", 6, options), "Lorum "); + assertEquals(clip("Lorum ipsum", 6, options), "Lorum"); assertEquals(clip("Lorum ipsum", 7, options), "Lorum i"); assertEquals(clip("Lorum ipsum", 8, options), "Lorum ip"); assertEquals(clip("Lorum ipsum", 9, options), "Lorum ips"); @@ -251,7 +248,7 @@ Deno.test("html: test ampersand", () => { assertEquals(clip("

&

", 1, options), ""); assertEquals(clip("

&

", 2, options), "

&

"); - assertEquals(clip("foo & bar", 5, options), "foo \u2026"); + assertEquals(clip("foo & bar", 5, options), "foo\u2026"); assertEquals(clip("foo & bar", 9, options), "foo & bar"); assertEquals(clip("foo&bar", 5, options), "foo&\u2026"); assertEquals(clip("foo&bar", 7, options), "foo&bar"); @@ -421,13 +418,13 @@ Deno.test("html: test strip tags", () => { ); assertEquals( clip(htmlWithImage, 12, { html: true, stripTags: ["img"] }), - "

Image and \u2026

", + "

Image and\u2026

", ); assertEquals( clip(htmlWithImage, 12, { html: true, stripTags: ["img", "p"] }), - "Image and \u2026", + "Image and\u2026", ); - assertEquals(clip(htmlWithImage, 12, { html: true, stripTags: true }), "Image and \u2026"); + assertEquals(clip(htmlWithImage, 12, { html: true, stripTags: true }), "Image and\u2026"); assertEquals( clip(htmlWithImage, 15, { html: true, stripTags: ["img"] }), "

Image and such

", @@ -455,11 +452,11 @@ Deno.test("html: test strip tags", () => { world`; - assertEquals(clip(htmlWithTable, 10, { html: true, stripTags: true }), "hello fb \u2026"); - assertEquals(clip(htmlWithTable, 16, { html: true, stripTags: true }), "hello fb fbfbfb "); + assertEquals(clip(htmlWithTable, 10, { html: true, stripTags: true }), "hello fb\u2026"); + assertEquals(clip(htmlWithTable, 16, { html: true, stripTags: true }), "hello fb fbfbfb\u2026"); assertEquals( clip(htmlWithTable, 24, { html: true, stripTags: true }), - "hello fb fbfbfb google \u2026", + "hello fb fbfbfb google\u2026", ); // SVG's `imageWeight` should not be counted when stripped: diff --git a/tests/unit/plain-text.test.ts b/tests/unit/plain-text.test.ts index 3304b97..01a953a 100644 --- a/tests/unit/plain-text.test.ts +++ b/tests/unit/plain-text.test.ts @@ -5,10 +5,10 @@ import clip from "../../src/index.ts"; Deno.test("plain-text: test basics", () => { assertEquals(clip("Lorum ipsum", 5), "Loru\u2026"); assertEquals(clip("Lorum ipsum", 6), "Lorum\u2026"); - assertEquals(clip("Lorum ipsum", 7), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 8), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 9), "Lorum \u2026"); - assertEquals(clip("Lorum ipsum", 10), "Lorum \u2026"); + assertEquals(clip("Lorum ipsum", 7), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 8), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 9), "Lorum\u2026"); + assertEquals(clip("Lorum ipsum", 10), "Lorum\u2026"); assertEquals(clip("Lorum ipsum", 11), "Lorum ipsum"); assertEquals(clip("Lorum\nipsum", 10), "Lorum"); @@ -32,7 +32,7 @@ Deno.test("plain-text: test word breaking", () => { assertEquals(clip("Lorum ipsum", 5, options), "Loru\u2026"); assertEquals(clip("Lorum ipsum", 6, options), "Lorum\u2026"); - assertEquals(clip("Lorum ipsum", 7, options), "Lorum \u2026"); + assertEquals(clip("Lorum ipsum", 7, options), "Lorum\u2026"); assertEquals(clip("Lorum ipsum", 8, options), "Lorum i\u2026"); assertEquals(clip("Lorum ipsum", 9, options), "Lorum ip\u2026"); assertEquals(clip("Lorum ipsum", 10, options), "Lorum ips\u2026"); @@ -43,7 +43,7 @@ Deno.test("plain-text: test word breaking without indicator", () => { const options = { breakWords: true, indicator: "" }; assertEquals(clip("Lorum ipsum", 5, options), "Lorum"); - assertEquals(clip("Lorum ipsum", 6, options), "Lorum "); + assertEquals(clip("Lorum ipsum", 6, options), "Lorum"); assertEquals(clip("Lorum ipsum", 7, options), "Lorum i"); assertEquals(clip("Lorum ipsum", 8, options), "Lorum ip"); assertEquals(clip("Lorum ipsum", 9, options), "Lorum ips");