diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..dc53fe8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: "1.8.3" + + - name: Lint and formatting + run: biome ci . + + - name: Set up Deno + uses: denoland/setup-deno@v1 + with: + deno-version: 1.x + + - name: Run tests + run: deno test diff --git a/.gitignore b/.gitignore index b511493..7d427b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ dist node_modules -.vscode package-lock.json yarn.lock diff --git a/.npmignore b/.npmignore deleted file mode 100644 index f069d9c..0000000 --- a/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -.babelrc -.eslintrc -src -tests -text-clipper.sublime-* -yarn.lock diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index b8834a8..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -12.16.3 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cdcdfc9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: node_js -node_js: - - "12" diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..3bb06c3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["biomejs.biome", "denoland.vscode-deno"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..84a4199 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "editor.defaultFormatter": "biomejs.biome" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0824db3..30d30ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 3.0.0 + +- `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. + ## 2.2.0 - Implement #14: Add `stripTags` option. diff --git a/LICENSE b/LICENSE index ca53944..533b291 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Arend van Beelen jr., Speakap B.V. +Copyright (c) 2016-2024 Arend van Beelen jr., Speakap B.V. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f86ab8b..666ad11 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,6 @@ Fast and correct clip functions for HTML and plain text. -[![Build Status](https://travis-ci.org/arendjr/text-clipper.svg?branch=master)](https://travis-ci.org/arendjr/text-clipper) -[![text-clipper on NPM](https://img.shields.io/npm/v/text-clipper.svg)](https://www.npmjs.com/package/text-clipper) - ## Why use text-clipper? text-clipper offers the following advantages over similar libraries that allow clipping HTML: @@ -22,43 +19,46 @@ text-clipper offers the following advantages over similar libraries that allow c ## Usage -### Node.js +### Deno -First install the `text-clipper` package: +First install the package: ```sh -$ yarn add text-clipper # or: npm install --save text-clipper +$ deno add @arendjr/text-clipper ``` -If compatibility with Internet Explorer is required, make sure you have a polyfill for -`Array.prototype.includes()`. - Once installed, you can use it as follows: ```js -import clip from "text-clipper"; // or: const clip = require("text-clipper").default; +import clip from "@arendjr/text-clipper"; const clippedString = clip(string, 80); // returns a string of at most 80 characters const clippedHtml = clip(htmlString, 140, { html: true, maxLines: 5 }); ``` -### Deno +### Bun -When using Deno, you can import right away: +Install using the following command instead: -```js -import clip from "https://raw.githubusercontent.com/arendjr/text-clipper/master/mod.ts"; +```sh +$ bunx jsr add @arendjr/text-clipper ``` -And use it like this: +For usage instructions, see above. -```js -const clippedString = clip(string, 80); // returns a string of at most 80 characters +### Node.js -const clippedHtml = clip(htmlString, 140, { html: true, maxLines: 5 }); +Install using one of the following commands, depending on your package manager: + +```sh +$ npx jsr add @arendjr/text-clipper # If using NPM +$ yarn dlx jsr add @arendjr/text-clipper # If using Yarn +$ pnpm dlx jsr add @arendjr/text-clipper # If using PNPM ``` +For usage instructions, see above. + ## Options ### breakWords @@ -107,3 +107,13 @@ clip(input, 140, { html: true, stripTags: ["img", "svg"] }); ``` Tag names must be specified in lowercase. + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). + +## License + +Licensed under the MIT License. + +See [LICENSE](LICENSE). diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..f6946d7 --- /dev/null +++ b/deno.json @@ -0,0 +1,12 @@ +{ + "name": "@arendjr/text-clipper", + "version": "3.0.0", + "compilerOptions": { + "allowJs": true, + "strict": true + }, + "lock": false, + "publish": { + "include": ["src"] + } +} diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 7f7b484..0000000 --- a/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", -}; diff --git a/mod.ts b/mod.ts deleted file mode 100644 index 65362ff..0000000 --- a/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./src/index.ts"; diff --git a/package.json b/package.json deleted file mode 100644 index 7c72fca..0000000 --- a/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "text-clipper", - "version": "2.2.0", - "description": "Fast and correct clip functions for HTML and plain text.", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, - "scripts": { - "build": "tsup", - "lint": "biome check", - "prepublish": "yarn build", - "test": "jest" - }, - "author": "Speakap B.V.", - "repository": { - "type": "git", - "url": "https://github.com/arendjr/text-clipper.git" - }, - "files": ["dist"], - "keywords": ["clip", "html", "string", "text", "trim", "truncate"], - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "1.8.3", - "@tsconfig/node18": "^18.2.2", - "@types/jest": "^25.2.2", - "@typescript-eslint/eslint-plugin": "^2.33.0", - "@typescript-eslint/parser": "^2.33.0", - "jest": "^25.0.0", - "prepush": "^3.1.11", - "ts-jest": "^25.5.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" - }, - "prepush": { - "tasks": ["yarn lint", "yarn test"] - } -} diff --git a/tests/unit/examples.spec.ts b/tests/unit/examples.spec.ts deleted file mode 100644 index 0c7e7b2..0000000 --- a/tests/unit/examples.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import clip from "../../src"; - -test("examples: test examples from the README", () => { - expect(clip("foo", 3)).toBe("foo"); - expect(clip("foo", 2)).toBe("fâŚ"); - expect(clip("foo bar", 5)).toBe("foo âŚ"); - expect(clip("foo\nbar", 5)).toBe("foo"); -}); diff --git a/tests/unit/examples.test.ts b/tests/unit/examples.test.ts new file mode 100644 index 0000000..9c82a6a --- /dev/null +++ b/tests/unit/examples.test.ts @@ -0,0 +1,10 @@ +import { assertEquals } from "jsr:@std/assert"; + +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\nbar", 5), "foo"); +}); diff --git a/tests/unit/html.spec.ts b/tests/unit/html.spec.ts deleted file mode 100644 index 86aaa43..0000000 --- a/tests/unit/html.spec.ts +++ /dev/null @@ -1,452 +0,0 @@ -import clip from "../../src"; - -test("html: test basic HTML", () => { - const options = { html: true }; - - expect(clip("
Lorum ipsum
", 5, options)).toBe("Loru\u2026
"); - expect(clip("Lorum ipsum
", 5, options)).toBe("Loru\u2026
"); - expect(clip("Lorum ipsum
", 6, options)).toBe("Lorum\u2026
"); - expect(clip("Lorum ipsum
", 7, options)).toBe("Lorum \u2026
"); - expect(clip("Lorum\nipsum
", 5, options)).toBe("Lorum
"); - expect(clip("Lorum
ipsum
Lorum
"); - - expect(clip("Lorum
", 5, options)).toBe("Lorum
"); - - expect(clip("Loruma
", 5, options)).toBe("Loru\u2026
"); - expect(clip("Lorum
a", 5, options)).toBe("Loru\u2026
"); - expect(clip("Loruma
", 6, options)).toBe("Loruma
"); - expect(clip("Lorum
a", 6, options)).toBe("Lorum
a"); - expect(clip("LorumaA
", 6, options)).toBe("Lorum\u2026
"); - expect(clip("Lorum
aA", 6, options)).toBe("Lorum
\u2026"); - expect(clip("Loruma
", 7, options)).toBe("Loruma
"); - expect(clip("Lorum
a", 7, options)).toBe("Lorum
a"); - expect(clip("LorumaA
", 7, options)).toBe("LorumaA
"); - expect(clip("Lorum
aA", 7, options)).toBe("Lorum
aA"); - - expect(clip("Lorum
", 5, options)).toBe("Loru\u2026
"); - expect(clip("Lorum
", 5, options)).toBe("Loru\u2026
"); - expect(clip("Lorum
", 6, options)).toBe("Lorum
"); - expect(clip("Lorum
", 6, options)).toBe("Lorum
"); - expect(clip("Lorum
", 6, options)).toBe("Lorum\u2026
"); - expect(clip("Lorum
", 6, options)).toBe("Lorum
\u2026"); - expect(clip("Lorum
", 7, options)).toBe("Lorum
"); - expect(clip("Lorum
", 7, options)).toBe("Lorum
"); - expect(clip("Lorum
", 7, options)).toBe("Lorum
"); - expect(clip("Lorum
", 7, options)).toBe("Lorum
"); - - expect(clip("Lorum", 4, options)).toBe("Lor\u2026"); - expect(clip("Lo" + - 'test\n" + - "
", - 9, - { html: true, imageWeight: 5 }, - ), - ).toBe( - "" + - 'test" + - "
", - ); -}); - -test("html: test unicode surrogate pairs", () => { - const options = { html: true }; - - expect(clip("Lorum đ", 7, options)).toBe("Lorum đ"); - expect(clip("đđđđ", 4, options)).toBe("đđđđ"); - expect(clip("đđđđ", 3, options)).toBe("đđâŚ"); - expect(clip("đđđđđ§", 6, options)).toBe("đđđđđ§"); - expect(clip("đđđđđ§", 5, options)).toBe("đđđđđ§"); - expect(clip("đđđđđ§", 4, options)).toBe("đđđâŚ"); - expect(clip("đđđđđ§", 3, options)).toBe("đđâŚ"); -}); - -test("html: test plain text", () => { - const options = { html: true }; - - expect(clip("Lorum ipsum", 5, options)).toBe("Loru\u2026"); - expect(clip("Lorum ipsum", 6, options)).toBe("Lorum\u2026"); - expect(clip("Lorum ipsum", 7, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 8, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 9, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 10, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 11, options)).toBe("Lorum ipsum"); - - expect(clip("Lorum\nipsum", 10, options)).toBe("Lorum"); - - expect(clip("Lorum i", 7, options)).toBe("Lorum i"); - expect(clip("Lorum \u2026", 7, options)).toBe("Lorum \u2026"); -}); - -test("html: test word breaking", () => { - const options = { breakWords: true, html: true }; - - expect(clip("Lorum ipsum", 5, options)).toBe("Loru\u2026"); - expect(clip("Lorum ipsum", 6, options)).toBe("Lorum\u2026"); - expect(clip("Lorum ipsum", 7, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 8, options)).toBe("Lorum i\u2026"); - expect(clip("Lorum ipsum", 9, options)).toBe("Lorum ip\u2026"); - expect(clip("Lorum ipsum", 10, options)).toBe("Lorum ips\u2026"); - expect(clip("Lorum ipsum", 11, options)).toBe("Lorum ipsum"); -}); - -test("html: test word breaking without indicator", () => { - const options = { breakWords: true, html: true, indicator: "" }; - - expect(clip("Lorum ipsum", 5, options)).toBe("Lorum"); - expect(clip("Lorum ipsum", 6, options)).toBe("Lorum "); - expect(clip("Lorum ipsum", 7, options)).toBe("Lorum i"); - expect(clip("Lorum ipsum", 8, options)).toBe("Lorum ip"); - expect(clip("Lorum ipsum", 9, options)).toBe("Lorum ips"); - expect(clip("Lorum ipsum", 10, options)).toBe("Lorum ipsu"); - expect(clip("Lorum ipsum", 11, options)).toBe("Lorum ipsum"); -}); - -test("html: test max lines", () => { - expect(clip("Lorum\nipsum", 100, { html: true, maxLines: 2 })).toBe("Lorum\nipsum"); - expect(clip("Lorum\nipsum", 100, { html: true, maxLines: 1 })).toBe("Lorum"); - expect(clip("LorumLorem ipsum
Lorem ipsum
", 100, { - html: true, - maxLines: 2, - }), - ).toBe("Lorem ipsum
Lorem ipsum
"); - expect( - clip("Lorem ipsum
Lorem ipsum
", 100, { - html: true, - maxLines: 1, - }), - ).toBe("Lorem ipsum
"); - expect( - clip("foo > bar
", 9, options)).toBe("foo > bar
"); - expect(clip("Lorum>>> ipsum
", 7, options)).toBe( - "Lorum>\u2026
", - ); -}); - -test("html: test ampersand", () => { - const options = { html: true }; - - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("<", 1, options)).toBe("<"); - expect(clip("<", 2, options)).toBe("<"); - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("&
", 1, options)).toBe(""); - expect(clip("&
", 2, options)).toBe("&
"); - expect(clip("<
", 1, options)).toBe(""); - expect(clip("<
", 2, options)).toBe("<
"); - expect(clip("&
", 1, options)).toBe(""); - expect(clip("&
", 2, options)).toBe("&
"); - - expect(clip("foo & bar", 5, options)).toBe("foo \u2026"); - expect(clip("foo & bar", 9, options)).toBe("foo & bar"); - expect(clip("foo&bar", 5, options)).toBe("foo&\u2026"); - expect(clip("foo&bar", 7, options)).toBe("foo&bar"); - expect(clip("foo&&& bar", 5, options)).toBe("foo&\u2026"); - expect(clip("foo&&& bar", 10, options)).toBe("foo&&& bar"); - - expect(clip('foo', 3, options)).toBe( - 'foo', - ); - expect(clip("&123", 4, options)).toBe("&123"); - expect(clip("&abc", 4, options)).toBe("&abc"); - expect(clip("foo &0 bar", 10, options)).toBe("foo &0 bar"); - expect(clip("foo &lolwat bar", 15, options)).toBe("foo &lolwat bar"); -}); - -test("html: test ampersand without indicator", () => { - const options = { html: true, indicator: "" }; - - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("<", 1, options)).toBe("<"); - expect(clip("<", 2, options)).toBe("<"); - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("&
", 1, options)).toBe("&
"); - expect(clip("&
", 2, options)).toBe("&
"); - expect(clip("<
", 1, options)).toBe("<
"); - expect(clip("<
", 2, options)).toBe("<
"); - expect(clip("&
", 1, options)).toBe("&
"); - expect(clip("&
", 2, options)).toBe("&
"); - - expect(clip("foo & bar", 5, options)).toBe("foo &"); - expect(clip("foo & bar", 9, options)).toBe("foo & bar"); - // Ideally "bar" wouldn't have been broken, but we accept this - // limitation when encountering tags during backtracking: - expect(clip("foo&bar", 5, options)).toBe("foo&b"); - expect(clip("foo&bar", 7, options)).toBe("foo&bar"); - expect(clip("foo&&& bar", 5, options)).toBe("foo&&"); - expect(clip("foo&&& bar", 10, options)).toBe("foo&&& bar"); - - expect(clip('foo', 3, options)).toBe( - 'foo', - ); - expect(clip("&123", 4, options)).toBe("&123"); - expect(clip("&abc", 4, options)).toBe("&abc"); - expect(clip("foo &0 bar", 10, options)).toBe("foo &0 bar"); - expect(clip("foo &lolwat bar", 15, options)).toBe("foo &lolwat bar"); -}); - -test("html: test ampersand without indicator and break words", () => { - const options = { breakWords: true, html: true, indicator: "" }; - - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("<", 1, options)).toBe("<"); - expect(clip("<", 2, options)).toBe("<"); - expect(clip("&", 1, options)).toBe("&"); - expect(clip("&", 2, options)).toBe("&"); - expect(clip("&
", 1, options)).toBe("&
"); - expect(clip("&
", 2, options)).toBe("&
"); - expect(clip("<
", 1, options)).toBe("<
"); - expect(clip("<
", 2, options)).toBe("<
"); - expect(clip("&
", 1, options)).toBe("&
"); - expect(clip("&
", 2, options)).toBe("&
"); - - expect(clip("foo & bar", 5, options)).toBe("foo &"); - expect(clip("foo & bar", 9, options)).toBe("foo & bar"); - expect(clip("foo&bar", 5, options)).toBe("foo&b"); - expect(clip("foo&bar", 7, options)).toBe("foo&bar"); - expect(clip("foo&&& bar", 5, options)).toBe("foo&&"); - expect(clip("foo&&& bar", 10, options)).toBe("foo&&& bar"); - - expect(clip('foo', 3, options)).toBe( - 'foo', - ); - expect(clip("&123", 4, options)).toBe("&123"); - expect(clip("&abc", 4, options)).toBe("&abc"); - expect(clip("foo &0 bar", 10, options)).toBe("foo &0 bar"); - expect(clip("foo &lolwat bar", 15, options)).toBe("foo &lolwat bar"); -}); - -test("html: test edge cases", () => { - const options = { breakWords: true, html: true, indicator: "..." }; - - expect(clip('one two - threeone two - three
four five
one two - three
four five
one...
", - ); -}); - -test("html: issue #12: split tables", () => { - const html = `fb | -fbfbfb | -
intel | -amazon | -
fb | -fbfbfb | -
intel |
fb | -fbfbfb | -
int\u2026 |
fb | -fbfbfb | -
Image and such
'; - expect(clip(htmlWithImage, 12, { html: true, stripTags: [] })).toBe( - clip(htmlWithImage, 12, { html: true }), - ); - expect(clip(htmlWithImage, 12, { html: true, stripTags: ["img"] })).toBe( - "Image and \u2026
", - ); - expect(clip(htmlWithImage, 12, { html: true, stripTags: ["img", "p"] })).toBe( - "Image and \u2026", - ); - expect(clip(htmlWithImage, 12, { html: true, stripTags: true })).toBe("Image and \u2026"); - expect(clip(htmlWithImage, 15, { html: true, stripTags: ["img"] })).toBe( - "Image and such
", - ); - - // Links are stripped (but content is preserved): - const htmlWithLink = 'foo'; - expect(clip(htmlWithLink, 3, { html: true, stripTags: ["a"] })).toBe("foo"); - expect(clip(htmlWithLink, 3, { html: true, stripTags: ["b"] })).toBe(htmlWithLink); - - // Same for tables, but whitespace is also simplified: - const htmlWithTable = `hellofb | -fbfbfb | -
intel | -amazon | -
" + - 'test\n" + - "
"; - expect(clip(htmlWithSvg, 3, { html: true, stripTags: ["svg"] })).toBe("te\u2026
"); - expect(clip(htmlWithSvg, 4, { html: true, stripTags: ["svg"] })).toBe("test
"); -}); diff --git a/tests/unit/html.test.ts b/tests/unit/html.test.ts new file mode 100644 index 0000000..2c7480f --- /dev/null +++ b/tests/unit/html.test.ts @@ -0,0 +1,486 @@ +import { assertEquals } from "jsr:@std/assert"; + +import clip from "../../src/index.ts"; + +Deno.test("html: test basic HTML", () => { + const options = { html: true }; + + 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\nipsum
", 5, options), "Lorum
"); + assertEquals(clip("Lorum
ipsum
Lorum
"); + + assertEquals(clip("Lorum
", 5, options), "Lorum
"); + + assertEquals(clip("Loruma
", 5, options), "Loru\u2026
"); + assertEquals(clip("Lorum
a", 5, options), "Loru\u2026
"); + assertEquals(clip("Loruma
", 6, options), "Loruma
"); + assertEquals(clip("Lorum
a", 6, options), "Lorum
a"); + assertEquals(clip("LorumaA
", 6, options), "Lorum\u2026
"); + assertEquals(clip("Lorum
aA", 6, options), "Lorum
\u2026"); + assertEquals(clip("Loruma
", 7, options), "Loruma
"); + assertEquals(clip("Lorum
a", 7, options), "Lorum
a"); + assertEquals(clip("LorumaA
", 7, options), "LorumaA
"); + assertEquals(clip("Lorum
aA", 7, options), "Lorum
aA"); + + assertEquals(clip("Lorum
", 5, options), "Loru\u2026
"); + assertEquals(clip("Lorum
", 5, options), "Loru\u2026
"); + assertEquals(clip("Lorum
", 6, options), "Lorum
"); + assertEquals(clip("Lorum
", 6, options), "Lorum
"); + assertEquals(clip("Lorum
", 6, options), "Lorum\u2026
"); + assertEquals(clip("Lorum
", 6, options), "Lorum
\u2026"); + assertEquals(clip("Lorum
", 7, options), "Lorum
"); + assertEquals(clip("Lorum
", 7, options), "Lorum
"); + assertEquals(clip("Lorum
", 7, options), "Lorum
"); + assertEquals(clip("Lorum
", 7, options), "Lorum
"); + + assertEquals(clip("Lorum", 4, options), "Lor\u2026"); + assertEquals(clip("Lo" + + 'test\n" + + "
", + 9, + { html: true, imageWeight: 5 }, + ), + "" + + 'test" + + "
", + ); +}); + +Deno.test("html: test unicode surrogate pairs", () => { + const options = { html: true }; + + assertEquals(clip("Lorum đ", 7, options), "Lorum đ"); + assertEquals(clip("đđđđ", 4, options), "đđđđ"); + assertEquals(clip("đđđđ", 3, options), "đđâŚ"); + assertEquals(clip("đđđđđ§", 6, options), "đđđđđ§"); + assertEquals(clip("đđđđđ§", 5, options), "đđđđđ§"); + assertEquals(clip("đđđđđ§", 4, options), "đđđâŚ"); + assertEquals(clip("đđđđđ§", 3, options), "đđâŚ"); +}); + +Deno.test("html: test plain text", () => { + const options = { html: true }; + + 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", 11, options), "Lorum ipsum"); + + assertEquals(clip("Lorum\nipsum", 10, options), "Lorum"); + + assertEquals(clip("Lorum i", 7, options), "Lorum i"); + assertEquals(clip("Lorum \u2026", 7, options), "Lorum \u2026"); +}); + +Deno.test("html: test word breaking", () => { + const options = { breakWords: true, html: true }; + + 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 i\u2026"); + assertEquals(clip("Lorum ipsum", 9, options), "Lorum ip\u2026"); + assertEquals(clip("Lorum ipsum", 10, options), "Lorum ips\u2026"); + assertEquals(clip("Lorum ipsum", 11, options), "Lorum ipsum"); +}); + +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", 7, options), "Lorum i"); + assertEquals(clip("Lorum ipsum", 8, options), "Lorum ip"); + assertEquals(clip("Lorum ipsum", 9, options), "Lorum ips"); + assertEquals(clip("Lorum ipsum", 10, options), "Lorum ipsu"); + assertEquals(clip("Lorum ipsum", 11, options), "Lorum ipsum"); +}); + +Deno.test("html: test max lines", () => { + assertEquals(clip("Lorum\nipsum", 100, { html: true, maxLines: 2 }), "Lorum\nipsum"); + assertEquals(clip("Lorum\nipsum", 100, { html: true, maxLines: 1 }), "Lorum"); + assertEquals(clip("LorumLorem ipsum
Lorem ipsum
", 100, { + html: true, + maxLines: 2, + }), + "Lorem ipsum
Lorem ipsum
", + ); + assertEquals( + clip("Lorem ipsum
Lorem ipsum
", 100, { + html: true, + maxLines: 1, + }), + "Lorem ipsum
", + ); + assertEquals( + clip("foo > bar
", 9, options), "foo > bar
"); + assertEquals( + clip("Lorum>>> ipsum
", 7, options), + "Lorum>\u2026
", + ); +}); + +Deno.test("html: test ampersand", () => { + const options = { html: true }; + + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("<", 1, options), "<"); + assertEquals(clip("<", 2, options), "<"); + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("&
", 1, options), ""); + assertEquals(clip("&
", 2, options), "&
"); + assertEquals(clip("<
", 1, options), ""); + assertEquals(clip("<
", 2, options), "<
"); + assertEquals(clip("&
", 1, options), ""); + assertEquals(clip("&
", 2, options), "&
"); + + 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"); + assertEquals(clip("foo&&& bar", 5, options), "foo&\u2026"); + assertEquals(clip("foo&&& bar", 10, options), "foo&&& bar"); + + assertEquals( + clip('foo', 3, options), + 'foo', + ); + assertEquals(clip("&123", 4, options), "&123"); + assertEquals(clip("&abc", 4, options), "&abc"); + assertEquals(clip("foo &0 bar", 10, options), "foo &0 bar"); + assertEquals(clip("foo &lolwat bar", 15, options), "foo &lolwat bar"); +}); + +Deno.test("html: test ampersand without indicator", () => { + const options = { html: true, indicator: "" }; + + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("<", 1, options), "<"); + assertEquals(clip("<", 2, options), "<"); + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("&
", 1, options), "&
"); + assertEquals(clip("&
", 2, options), "&
"); + assertEquals(clip("<
", 1, options), "<
"); + assertEquals(clip("<
", 2, options), "<
"); + assertEquals(clip("&
", 1, options), "&
"); + assertEquals(clip("&
", 2, options), "&
"); + + assertEquals(clip("foo & bar", 5, options), "foo &"); + assertEquals(clip("foo & bar", 9, options), "foo & bar"); + // Ideally "bar" wouldn't have been broken, but we accept this + // limitation when encountering tags during backtracking: + assertEquals(clip("foo&bar", 5, options), "foo&b"); + assertEquals(clip("foo&bar", 7, options), "foo&bar"); + assertEquals(clip("foo&&& bar", 5, options), "foo&&"); + assertEquals(clip("foo&&& bar", 10, options), "foo&&& bar"); + + assertEquals( + clip('foo', 3, options), + 'foo', + ); + assertEquals(clip("&123", 4, options), "&123"); + assertEquals(clip("&abc", 4, options), "&abc"); + assertEquals(clip("foo &0 bar", 10, options), "foo &0 bar"); + assertEquals(clip("foo &lolwat bar", 15, options), "foo &lolwat bar"); +}); + +Deno.test("html: test ampersand without indicator and break words", () => { + const options = { breakWords: true, html: true, indicator: "" }; + + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("<", 1, options), "<"); + assertEquals(clip("<", 2, options), "<"); + assertEquals(clip("&", 1, options), "&"); + assertEquals(clip("&", 2, options), "&"); + assertEquals(clip("&
", 1, options), "&
"); + assertEquals(clip("&
", 2, options), "&
"); + assertEquals(clip("<
", 1, options), "<
"); + assertEquals(clip("<
", 2, options), "<
"); + assertEquals(clip("&
", 1, options), "&
"); + assertEquals(clip("&
", 2, options), "&
"); + + assertEquals(clip("foo & bar", 5, options), "foo &"); + assertEquals(clip("foo & bar", 9, options), "foo & bar"); + assertEquals(clip("foo&bar", 5, options), "foo&b"); + assertEquals(clip("foo&bar", 7, options), "foo&bar"); + assertEquals(clip("foo&&& bar", 5, options), "foo&&"); + assertEquals(clip("foo&&& bar", 10, options), "foo&&& bar"); + + assertEquals( + clip('foo', 3, options), + 'foo', + ); + assertEquals(clip("&123", 4, options), "&123"); + assertEquals(clip("&abc", 4, options), "&abc"); + assertEquals(clip("foo &0 bar", 10, options), "foo &0 bar"); + assertEquals(clip("foo &lolwat bar", 15, options), "foo &lolwat bar"); +}); + +Deno.test("html: test edge cases", () => { + const options = { breakWords: true, html: true, indicator: "..." }; + + assertEquals(clip('one two - threeone two - three
four five
one two - three
four five
one...
", + ); +}); + +Deno.test("html: issue #12: split tables", () => { + const html = `fb | +fbfbfb | +
intel | +amazon | +
fb | +fbfbfb | +
intel |
fb | +fbfbfb | +
int\u2026 |
fb | +fbfbfb | +
Image and such
'; + assertEquals( + clip(htmlWithImage, 12, { html: true, stripTags: [] }), + clip(htmlWithImage, 12, { html: true }), + ); + assertEquals( + clip(htmlWithImage, 12, { html: true, stripTags: ["img"] }), + "Image and \u2026
", + ); + assertEquals( + clip(htmlWithImage, 12, { html: true, stripTags: ["img", "p"] }), + "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
", + ); + + // Links are stripped (but content is preserved): + const htmlWithLink = 'foo'; + assertEquals(clip(htmlWithLink, 3, { html: true, stripTags: ["a"] }), "foo"); + assertEquals(clip(htmlWithLink, 3, { html: true, stripTags: ["b"] }), htmlWithLink); + + // Same for tables, but whitespace is also simplified: + const htmlWithTable = `hellofb | +fbfbfb | +
intel | +amazon | +
" + + 'test\n" + + "
"; + assertEquals(clip(htmlWithSvg, 3, { html: true, stripTags: ["svg"] }), "te\u2026
"); + assertEquals(clip(htmlWithSvg, 4, { html: true, stripTags: ["svg"] }), "test
"); +}); diff --git a/tests/unit/plain-text.spec.ts b/tests/unit/plain-text.spec.ts deleted file mode 100644 index d11fd65..0000000 --- a/tests/unit/plain-text.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import clip from "../../src"; - -test("plain-text: test basics", () => { - expect(clip("Lorum ipsum", 5)).toBe("Loru\u2026"); - expect(clip("Lorum ipsum", 6)).toBe("Lorum\u2026"); - expect(clip("Lorum ipsum", 7)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 8)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 9)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 10)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 11)).toBe("Lorum ipsum"); - - expect(clip("Lorum\nipsum", 10)).toBe("Lorum"); - - expect(clip("Lorum i", 7)).toBe("Lorum i"); - expect(clip("Lorum \u2026", 7)).toBe("Lorum \u2026"); -}); - -test("plain-text: test unicode surrogate pairs", () => { - expect(clip("Lorum đ", 7)).toBe("Lorum đ"); - expect(clip("đđđđ", 4)).toBe("đđđđ"); - expect(clip("đđđđ", 3)).toBe("đđâŚ"); - expect(clip("đđđđđ§", 6)).toBe("đđđđđ§"); - expect(clip("đđđđđ§", 5)).toBe("đđđđđ§"); - expect(clip("đđđđđ§", 4)).toBe("đđđâŚ"); - expect(clip("đđđđđ§", 3)).toBe("đđâŚ"); -}); - -test("plain-text: test word breaking", () => { - const options = { breakWords: true }; - - expect(clip("Lorum ipsum", 5, options)).toBe("Loru\u2026"); - expect(clip("Lorum ipsum", 6, options)).toBe("Lorum\u2026"); - expect(clip("Lorum ipsum", 7, options)).toBe("Lorum \u2026"); - expect(clip("Lorum ipsum", 8, options)).toBe("Lorum i\u2026"); - expect(clip("Lorum ipsum", 9, options)).toBe("Lorum ip\u2026"); - expect(clip("Lorum ipsum", 10, options)).toBe("Lorum ips\u2026"); - expect(clip("Lorum ipsum", 11, options)).toBe("Lorum ipsum"); -}); - -test("plain-text: test word breaking without indicator", () => { - const options = { breakWords: true, indicator: "" }; - - expect(clip("Lorum ipsum", 5, options)).toBe("Lorum"); - expect(clip("Lorum ipsum", 6, options)).toBe("Lorum "); - expect(clip("Lorum ipsum", 7, options)).toBe("Lorum i"); - expect(clip("Lorum ipsum", 8, options)).toBe("Lorum ip"); - expect(clip("Lorum ipsum", 9, options)).toBe("Lorum ips"); - expect(clip("Lorum ipsum", 10, options)).toBe("Lorum ipsu"); - expect(clip("Lorum ipsum", 11, options)).toBe("Lorum ipsum"); -}); - -test("plain-text: test max lines", () => { - expect(clip("Lorum\nipsum", 100, { maxLines: 2 })).toBe("Lorum\nipsum"); - expect(clip("Lorum\nipsum", 100, { maxLines: 1 })).toBe("Lorum"); - expect(clip("Lorum\n\nipsum", 100, { maxLines: 2 })).toBe("Lorum\n"); - - expect(clip("Lorum\nipsum\n", 100, { maxLines: 2 })).toBe("Lorum\nipsum"); - expect(clip("Lorum\nipsum\n\n", 100, { maxLines: 2 })).toBe("Lorum\nipsum"); -}); - -test("plain-text: test edge cases", () => { - const options = { breakWords: true, html: true, indicator: "..." }; - - expect(clip("one two - three \nfour five", 0, options)).toBe("..."); - expect(clip("one two - three \nfour five", 6, options)).toBe("one..."); -}); diff --git a/tests/unit/plain-text.test.ts b/tests/unit/plain-text.test.ts new file mode 100644 index 0000000..3304b97 --- /dev/null +++ b/tests/unit/plain-text.test.ts @@ -0,0 +1,68 @@ +import { assertEquals } from "jsr:@std/assert"; + +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", 11), "Lorum ipsum"); + + assertEquals(clip("Lorum\nipsum", 10), "Lorum"); + + assertEquals(clip("Lorum i", 7), "Lorum i"); + assertEquals(clip("Lorum \u2026", 7), "Lorum \u2026"); +}); + +Deno.test("plain-text: test unicode surrogate pairs", () => { + assertEquals(clip("Lorum đ", 7), "Lorum đ"); + assertEquals(clip("đđđđ", 4), "đđđđ"); + assertEquals(clip("đđđđ", 3), "đđâŚ"); + assertEquals(clip("đđđđđ§", 6), "đđđđđ§"); + assertEquals(clip("đđđđđ§", 5), "đđđđđ§"); + assertEquals(clip("đđđđđ§", 4), "đđđâŚ"); + assertEquals(clip("đđđđđ§", 3), "đđâŚ"); +}); + +Deno.test("plain-text: test word breaking", () => { + const options = { breakWords: true }; + + 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 i\u2026"); + assertEquals(clip("Lorum ipsum", 9, options), "Lorum ip\u2026"); + assertEquals(clip("Lorum ipsum", 10, options), "Lorum ips\u2026"); + assertEquals(clip("Lorum ipsum", 11, options), "Lorum ipsum"); +}); + +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", 7, options), "Lorum i"); + assertEquals(clip("Lorum ipsum", 8, options), "Lorum ip"); + assertEquals(clip("Lorum ipsum", 9, options), "Lorum ips"); + assertEquals(clip("Lorum ipsum", 10, options), "Lorum ipsu"); + assertEquals(clip("Lorum ipsum", 11, options), "Lorum ipsum"); +}); + +Deno.test("plain-text: test max lines", () => { + assertEquals(clip("Lorum\nipsum", 100, { maxLines: 2 }), "Lorum\nipsum"); + assertEquals(clip("Lorum\nipsum", 100, { maxLines: 1 }), "Lorum"); + assertEquals(clip("Lorum\n\nipsum", 100, { maxLines: 2 }), "Lorum\n"); + + assertEquals(clip("Lorum\nipsum\n", 100, { maxLines: 2 }), "Lorum\nipsum"); + assertEquals(clip("Lorum\nipsum\n\n", 100, { maxLines: 2 }), "Lorum\nipsum"); +}); + +Deno.test("plain-text: test edge cases", () => { + const options = { breakWords: true, html: true, indicator: "..." }; + + assertEquals(clip("one two - three \nfour five", 0, options), "..."); + assertEquals(clip("one two - three \nfour five", 6, options), "one..."); +}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index cda1be3..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": true, - "declaration": true, - "esModuleInterop": true, - "lib": ["es2017"], - "module": "commonjs", - "moduleResolution": "node", - "outDir": "dist", - "strict": true, - "target": "ES5" - }, - "include": ["src/**/*"] -} diff --git a/tsup.config.ts b/tsup.config.ts deleted file mode 100644 index f9b4730..0000000 --- a/tsup.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import fsp from "node:fs/promises"; -import path from "node:path"; - -import { defineConfig } from "tsup"; - -export default defineConfig({ - format: ["cjs", "esm"], // generate cjs and esm files - entry: ["src/index.ts"], - clean: true, // rimraf dis - dts: true, // generate dts file for main module - skipNodeModulesBundle: true, - splitting: true, - target: "es2017", - cjsInterop: true, -});