From 6d305d280d69a7af57c3e2f5ad02a6e818473b5b Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Wed, 10 Jul 2024 19:43:01 +0200 Subject: [PATCH] Bye ESLint+Prettier, welcome Biome --- .eslintrc | 62 ------------- .prettierrc | 9 -- biome.json | 42 +++++++++ package.json | 26 ++---- src/index.ts | 23 ++--- tests/perf/.eslintignore | 1 - tests/perf/benchmark/baseline.js | 141 +++++++++++++++--------------- tests/perf/benchmark/benchmark.js | 10 ++- tests/unit/html.spec.ts | 37 +++++--- 9 files changed, 159 insertions(+), 192 deletions(-) delete mode 100644 .eslintrc delete mode 100644 .prettierrc create mode 100644 biome.json delete mode 100644 tests/perf/.eslintignore diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 115a9ff..0000000 --- a/.eslintrc +++ /dev/null @@ -1,62 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module" - }, - "env": { - "jest/globals": true - }, - "extends": [ - "plugin:@typescript-eslint/recommended", - "prettier/@typescript-eslint", - "plugin:prettier/recommended" - ], - "plugins": ["jest", "prettier"], - "root": true, - "rules": { - "constructor-super": "error", - "curly": "error", - "eqeqeq": "error", - "linebreak-style": ["error", "unix"], - "new-cap": "error", - "no-class-assign": "error", - "no-console": 0, - "no-const-assign": "error", - "no-delete-var": "error", - "no-dupe-class-members": "error", - "no-label-var": "error", - "no-negated-condition": "error", - "no-return-assign": "error", - "no-return-await": "error", - "no-this-before-super": "error", - "no-undef-init": "error", - "no-undef": "error", - "no-unused-vars": "error", - "no-use-before-define": ["error", { "classes": false, "functions": false }], - "no-useless-constructor": "error", - "no-var": "error", - "prefer-const": "error", - "prefer-template": "error", - "prettier/prettier": [ - "error", - { - "bracketSpacing": true, - "jsxBracketSameLine": false, - "printWidth": 100, - "semi": true, - "singleQuote": false, - "tabWidth": 4, - "trailingComma": "all" - } - ], - "require-await": "error", - "require-yield": "error", - "semi": ["error", "always"], - "spaced-comment": ["error", "always", { "markers": ["/"] }], - "strict": ["error", "global"], - "yoda": "error", - "quotes": ["error", "double", { "avoidEscape": true }], - "@typescript-eslint/no-use-before-define": ["error", { "functions": false }] - } -} diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index c6bb9aa..0000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bracketSpacing": true, - "jsxBracketSameLine": false, - "printWidth": 100, - "semi": true, - "singleQuote": false, - "tabWidth": 4, - "trailingComma": "all" -} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c3ff62d --- /dev/null +++ b/biome.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 4, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto" + }, + "files": { + "ignore": ["dist"] + }, + "organizeImports": { "enabled": true }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "off", + "noUselessElse": "off", + "useNumberNamespace": "off" + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "quoteStyle": "double", + "attributePosition": "auto" + } + }, + "overrides": [{ "include": ["tests/perf/benchmark/*"], "linter": { "enabled": false } }] +} diff --git a/package.json b/package.json index 8f7a252..7c72fca 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "build": "tsup", - "lint": "eslint mod.ts src/*.ts tests/*/*.ts", + "lint": "biome check", "prepublish": "yarn build", "test": "jest" }, @@ -22,38 +22,22 @@ "type": "git", "url": "https://github.com/arendjr/text-clipper.git" }, - "files": [ - "dist" - ], - "keywords": [ - "clip", - "html", - "string", - "text", - "trim", - "truncate" - ], + "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", - "eslint": "^6.0.0", - "eslint-config-prettier": "^6.11.0", - "eslint-plugin-jest": "^23.11.0", - "eslint-plugin-prettier": "^3.1.3", "jest": "^25.0.0", "prepush": "^3.1.11", - "prettier": "^2.0.5", "ts-jest": "^25.5.1", "tsup": "^7.2.0", "typescript": "^5.2.2" }, "prepush": { - "tasks": [ - "yarn lint", - "yarn test" - ] + "tasks": ["yarn lint", "yarn test"] } } diff --git a/src/index.ts b/src/index.ts index 248db86..2ad37b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -184,7 +184,9 @@ function clipHtml(string: string, maxLength: number, options: ClipHtmlOptions): const tagStack: Array = []; // Stack of currently open HTML tags. const popTagStack = (result: string) => { - let tagName; + let tagName: string | undefined; + // biome-ignore lint/style/noCommaOperator: Otherwise we need to pop... + // biome-ignore lint/suspicious/noAssignInExpressions: ... in two places. while (((tagName = tagStack.pop()), tagName !== undefined)) { if (!shouldStrip(tagName)) { result += ``; @@ -623,15 +625,16 @@ function isCharacterReferenceCharacter(charCode: number): boolean { } function isLineBreak(string: string, index: number): boolean { - const firstCharCode = string.charCodeAt(index); - if (firstCharCode === NEWLINE_CHAR_CODE) { - return true; - } else if (firstCharCode === TAG_OPEN_CHAR_CODE) { - const newlineElements = `(${BLOCK_ELEMENTS.join("|")}|br)`; - const newlineRegExp = new RegExp(`^<${newlineElements}[\t\n\f\r ]*/?>`, "i"); - return newlineRegExp.test(string.slice(index)); - } else { - return false; + switch (string.charCodeAt(index)) { + case NEWLINE_CHAR_CODE: + return true; + case TAG_OPEN_CHAR_CODE: { + const newlineElements = `(${BLOCK_ELEMENTS.join("|")}|br)`; + const newlineRegExp = new RegExp(`^<${newlineElements}[\t\n\f\r ]*/?>`, "i"); + return newlineRegExp.test(string.slice(index)); + } + default: + return false; } } diff --git a/tests/perf/.eslintignore b/tests/perf/.eslintignore deleted file mode 100644 index 945c9b4..0000000 --- a/tests/perf/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/tests/perf/benchmark/baseline.js b/tests/perf/benchmark/baseline.js index 3460d2f..031e887 100644 --- a/tests/perf/benchmark/baseline.js +++ b/tests/perf/benchmark/baseline.js @@ -100,7 +100,9 @@ var SIMPLIFY_WHITESPACE_REGEX = /\s{2,}/g; * @return The clipped string. */ function clip(string, maxLength, options) { - if (options === void 0) { options = {}; } + if (options === void 0) { + options = {}; + } if (!string) { return ""; } @@ -111,7 +113,12 @@ function clip(string, maxLength, options) { } exports.default = clip; function clipHtml(string, maxLength, options) { - var _a = options.imageWeight, imageWeight = _a === void 0 ? 2 : _a, _b = options.indicator, indicator = _b === void 0 ? "\u2026" : _b, _c = options.maxLines, maxLines = _c === void 0 ? Infinity : _c; + var _a = options.imageWeight, + imageWeight = _a === void 0 ? 2 : _a, + _b = options.indicator, + indicator = _b === void 0 ? "\u2026" : _b, + _c = options.maxLines, + maxLines = _c === void 0 ? Infinity : _c; var numChars = indicator.length; var numLines = 1; var i = 0; @@ -125,13 +132,14 @@ function clipHtml(string, maxLength, options) { i += nextBlockSize; if (!isUnbreakableContent) { if (shouldSimplifyWhiteSpace(tagStack)) { - numChars += simplifyWhiteSpace(nextBlockSize === rest.length ? rest : rest.slice(0, nextIndex)).length; + numChars += simplifyWhiteSpace( + nextBlockSize === rest.length ? rest : rest.slice(0, nextIndex), + ).length; if (numChars > maxLength) { i -= nextBlockSize; // We just cut off the entire incorrectly placed text... break; } - } - else { + } else { numChars += nextBlockSize; if (numChars > maxLength) { i = Math.max(i - numChars + maxLength, 0); @@ -149,17 +157,17 @@ function clipHtml(string, maxLength, options) { if (isSpecialTag && string.substr(i + 2, 2) === "--") { var commentEndIndex = string.indexOf("-->", i + 4) + 3; i = commentEndIndex - 1; // - 1 because the outer for loop will increment it - } - else if (isSpecialTag && string.substr(i + 2, 7) === "[CDATA[") { + } else if (isSpecialTag && string.substr(i + 2, 7) === "[CDATA[") { var cdataEndIndex = string.indexOf("]]>", i + 9) + 3; i = cdataEndIndex - 1; // - 1 because the outer for loop will increment it // note we don't count CDATA text for our character limit because it is only // allowed within SVG and MathML content, both of which we don't clip - } - else { + } else { // don't open new tags if we are currently at the limit - if (numChars === maxLength && - string.charCodeAt(i + 1) !== FORWARD_SLASH_CHAR_CODE) { + if ( + numChars === maxLength && + string.charCodeAt(i + 1) !== FORWARD_SLASH_CHAR_CODE + ) { numChars++; break; } @@ -177,36 +185,36 @@ function clipHtml(string, maxLength, options) { if (charCode_1 === attributeQuoteCharCode) { isAttributeValue = false; } - } - else { + } else { if (isWhiteSpace(charCode_1)) { isAttributeValue = false; - } - else if (charCode_1 === TAG_CLOSE_CHAR_CODE) { + } else if (charCode_1 === TAG_CLOSE_CHAR_CODE) { isAttributeValue = false; endIndex--; // re-evaluate this character } } - } - else if (charCode_1 === EQUAL_SIGN_CHAR_CODE) { + } else if (charCode_1 === EQUAL_SIGN_CHAR_CODE) { while (isWhiteSpace(string.charCodeAt(endIndex + 1))) { endIndex++; // skip whitespace } isAttributeValue = true; var firstAttributeCharCode = string.charCodeAt(endIndex + 1); - if (firstAttributeCharCode === DOUBLE_QUOTE_CHAR_CODE || - firstAttributeCharCode === SINGLE_QUOTE_CHAR_CODE) { + if ( + firstAttributeCharCode === DOUBLE_QUOTE_CHAR_CODE || + firstAttributeCharCode === SINGLE_QUOTE_CHAR_CODE + ) { attributeQuoteCharCode = firstAttributeCharCode; endIndex++; - } - else { + } else { attributeQuoteCharCode = 0; } - } - else if (charCode_1 === TAG_CLOSE_CHAR_CODE) { + } else if (charCode_1 === TAG_CLOSE_CHAR_CODE) { var isEndTag = string.charCodeAt(i + 1) === FORWARD_SLASH_CHAR_CODE; var tagNameStartIndex = i + (isEndTag ? 2 : 1); - var tagNameEndIndex = Math.min(indexOfWhiteSpace(string, tagNameStartIndex), endIndex); + var tagNameEndIndex = Math.min( + indexOfWhiteSpace(string, tagNameStartIndex), + endIndex, + ); var tagName = string .slice(tagNameStartIndex, tagNameEndIndex) .toLowerCase(); @@ -243,23 +251,22 @@ function clipHtml(string, maxLength, options) { } } } - } - else if (VOID_ELEMENTS.includes(tagName) || - string.charCodeAt(endIndex - 1) === FORWARD_SLASH_CHAR_CODE) { + } else if ( + VOID_ELEMENTS.includes(tagName) || + string.charCodeAt(endIndex - 1) === FORWARD_SLASH_CHAR_CODE + ) { if (tagName === "br") { numLines++; if (numLines > maxLines) { break; } - } - else if (tagName === "img") { + } else if (tagName === "img") { numChars += imageWeight; if (numChars > maxLength) { break; } } - } - else { + } else { tagStack.push(tagName); if (tagName === "math" || tagName === "svg") { isUnbreakableContent = true; @@ -273,19 +280,16 @@ function clipHtml(string, maxLength, options) { break; } } - } - else if (charCode === AMPERSAND_CHAR_CODE) { + } else if (charCode === AMPERSAND_CHAR_CODE) { var endIndex = i + 1; var isCharacterReference = true; while (true /* eslint-disable-line */) { var charCode_2 = string.charCodeAt(endIndex); if (isCharacterReferenceCharacter(charCode_2)) { endIndex++; - } - else if (charCode_2 === SEMICOLON_CHAR_CODE) { + } else if (charCode_2 === SEMICOLON_CHAR_CODE) { break; - } - else { + } else { isCharacterReference = false; break; } @@ -299,8 +303,7 @@ function clipHtml(string, maxLength, options) { if (isCharacterReference) { i = endIndex; } - } - else if (charCode === NEWLINE_CHAR_CODE) { + } else if (charCode === NEWLINE_CHAR_CODE) { if (!isUnbreakableContent && !shouldSimplifyWhiteSpace(tagStack)) { numChars++; if (numChars > maxLength) { @@ -311,8 +314,7 @@ function clipHtml(string, maxLength, options) { break; } } - } - else { + } else { if (!isUnbreakableContent) { numChars++; if (numChars > maxLength) { @@ -330,13 +332,14 @@ function clipHtml(string, maxLength, options) { var nextChar = takeHtmlCharAt(string, i); if (indicator) { var peekIndex = i + nextChar.length; - while (string.charCodeAt(peekIndex) === TAG_OPEN_CHAR_CODE && - string.charCodeAt(peekIndex + 1) === FORWARD_SLASH_CHAR_CODE) { + while ( + string.charCodeAt(peekIndex) === TAG_OPEN_CHAR_CODE && + string.charCodeAt(peekIndex + 1) === FORWARD_SLASH_CHAR_CODE + ) { var nextPeekIndex = string.indexOf(">", peekIndex + 2) + 1; if (nextPeekIndex) { peekIndex = nextPeekIndex; - } - else { + } else { break; } } @@ -370,12 +373,10 @@ function clipHtml(string, maxLength, options) { // 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) { i = j; break; - } - else if (isWhiteSpace(charCode)) { + } else if (isWhiteSpace(charCode)) { i = j + (indicator ? 1 : 0); break; } @@ -391,8 +392,7 @@ function clipHtml(string, maxLength, options) { } return result; } - } - else if (numLines > maxLines) { + } else if (numLines > maxLines) { var result = string.slice(0, i); while (tagStack.length) { var tagName = tagStack.pop(); @@ -403,7 +403,10 @@ function clipHtml(string, maxLength, options) { return string; } function clipPlainText(string, maxLength, options) { - var _a = options.indicator, indicator = _a === void 0 ? "\u2026" : _a, _b = options.maxLines, maxLines = _b === void 0 ? Infinity : _b; + var _a = options.indicator, + indicator = _a === void 0 ? "\u2026" : _a, + _b = options.maxLines, + maxLines = _b === void 0 ? Infinity : _b; var numChars = indicator.length; var numLines = 1; var i = 0; @@ -419,8 +422,7 @@ function clipPlainText(string, maxLength, options) { if (numLines > maxLines) { break; } - } - else if ((charCode & 0xfc00) === 0xd800) { + } else if ((charCode & 0xfc00) === 0xd800) { // high Unicode surrogate should never be separated from its matching low surrogate var nextCharCode = string.charCodeAt(i + 1); if ((nextCharCode & 0xfc00) === 0xdc00) { @@ -434,8 +436,7 @@ function clipPlainText(string, maxLength, options) { var peekIndex = i + nextChar.length; if (peekIndex === string.length) { return string; - } - else if (string.charCodeAt(peekIndex) === NEWLINE_CHAR_CODE) { + } else if (string.charCodeAt(peekIndex) === NEWLINE_CHAR_CODE) { return string.slice(0, i + nextChar.length); } } @@ -447,16 +448,14 @@ function clipPlainText(string, maxLength, options) { i = j; nextChar = "\n"; break; - } - else if (isWhiteSpace(charCode)) { + } else if (isWhiteSpace(charCode)) { i = j + (indicator ? 1 : 0); break; } } } return string.slice(0, i) + (nextChar === "\n" ? "" : indicator); - } - else if (numLines > maxLines) { + } else if (numLines > maxLines) { return string.slice(0, i); } return string; @@ -473,26 +472,28 @@ function indexOfWhiteSpace(string, fromIndex) { return length; } function isCharacterReferenceCharacter(charCode) { - return ((charCode >= 48 && charCode <= 57) || + return ( + (charCode >= 48 && charCode <= 57) || (charCode >= 65 && charCode <= 90) || - (charCode >= 97 && charCode <= 122)); + (charCode >= 97 && charCode <= 122) + ); } function isLineBreak(string, index) { var firstCharCode = string.charCodeAt(index); if (firstCharCode === NEWLINE_CHAR_CODE) { return true; - } - else if (firstCharCode === TAG_OPEN_CHAR_CODE) { + } else if (firstCharCode === TAG_OPEN_CHAR_CODE) { var newlineElements = "(" + BLOCK_ELEMENTS.join("|") + "|br)"; var newlineRegExp = new RegExp("^<" + newlineElements + "[\t\n\f\r ]*/?>", "i"); return newlineRegExp.test(string.slice(index)); - } - else { + } else { return false; } } function isWhiteSpace(charCode) { - return (charCode === 9 || charCode === 10 || charCode === 12 || charCode === 13 || charCode === 32); + return ( + charCode === 9 || charCode === 10 || charCode === 12 || charCode === 13 || charCode === 32 + ); } /** * Certain tags don't display their whitespace-only content. In such cases, we @@ -532,12 +533,10 @@ function takeHtmlCharAt(string, index) { var nextCharCode = string.charCodeAt(index); if (isCharacterReferenceCharacter(nextCharCode)) { char += String.fromCharCode(nextCharCode); - } - else if (nextCharCode === SEMICOLON_CHAR_CODE) { + } else if (nextCharCode === SEMICOLON_CHAR_CODE) { char += String.fromCharCode(nextCharCode); break; - } - else { + } else { break; } } diff --git a/tests/perf/benchmark/benchmark.js b/tests/perf/benchmark/benchmark.js index 123be95..fdbdd71 100644 --- a/tests/perf/benchmark/benchmark.js +++ b/tests/perf/benchmark/benchmark.js @@ -1,4 +1,4 @@ -const { performance } = require("perf_hooks"); +const { performance } = require("node:perf_hooks"); const clip = require("../../../dist").default; const baseline = require("./baseline").default; @@ -31,7 +31,7 @@ const benchmarks = { "text-clipper-baseline": (string, limit) => baseline(string, limit, { html: true }), "trim-html": (string, limit) => trimHtml(string, { limit }), "truncate-html": truncateHtml, - "html-truncate": htmlTruncate + "html-truncate": htmlTruncate, }; for (const [name, fn] of Object.entries(benchmarks)) { @@ -42,7 +42,7 @@ for (const [name, fn] of Object.entries(benchmarks)) { const begin = performance.now(); const measures = []; - + for (let i = 0; i < MAX_NUM_MEASURES; i++) { const start = performance.now(); for (let j = 0; j < NUM_ITERATIONS_PER_MEASURE; j++) { @@ -67,7 +67,9 @@ for (const [name, fn] of Object.entries(benchmarks)) { const average = measures.reduce((sum, measure) => sum + measure, 0) / measures.length; - console.log(`Average ops/s: ${toOps(average)} (fastest: ${toOps(fastest)}, slowest: ${toOps(slowest)})`); + console.log( + `Average ops/s: ${toOps(average)} (fastest: ${toOps(fastest)}, slowest: ${toOps(slowest)})`, + ); } function toOps(measure) { diff --git a/tests/unit/html.spec.ts b/tests/unit/html.spec.ts index 1c0f0de..86aaa43 100644 --- a/tests/unit/html.spec.ts +++ b/tests/unit/html.spec.ts @@ -184,17 +184,29 @@ test("html: test max lines", () => { expect(clip("Lorum\nipsum\n", 100, { html: true, maxLines: 2 })).toBe("Lorum\nipsum"); expect(clip("Lorum\nipsum\n\n", 100, { html: true, maxLines: 2 })).toBe("Lorum\nipsum"); - expect(clip("

Lorem 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("
Lorem ipsum
Lorem ipsum
", 100, { html: true, maxLines: 2 }), + clip("

Lorem 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("
Lorem ipsum
Lorem ipsum
", 100, { + html: true, + maxLines: 2, + }), ).toBe("
Lorem ipsum
Lorem ipsum
"); expect( - clip("
Lorem ipsum
Lorem ipsum
", 100, { html: true, maxLines: 1 }), + clip("
Lorem ipsum
Lorem ipsum
", 100, { + html: true, + maxLines: 1, + }), ).toBe("
Lorem ipsum
"); }); @@ -333,8 +345,7 @@ test("html: issue #12: split tables", () => { `; - expect(clip(html, 26, { html: true, breakWords: true })) - .toBe(` + expect(clip(html, 26, { html: true, breakWords: true })).toBe(`
@@ -347,8 +358,7 @@ test("html: issue #12: split tables", () => {
fb
intel
`); - expect(clip(html, 25, { html: true, breakWords: true })) - .toBe(` + expect(clip(html, 25, { html: true, breakWords: true })).toBe(`
@@ -361,8 +371,7 @@ test("html: issue #12: split tables", () => {
fb
int\u2026
`); - expect(clip(html, 25, { html: true, breakWords: true, maxLines: 2 })) - .toBe(` + expect(clip(html, 25, { html: true, breakWords: true, maxLines: 2 })).toBe(`
fb